• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            loop_in_codes

            低調(diào)做技術(shù)__歡迎移步我的獨(dú)立博客 codemaro.com 微博 kevinlynx

            #

            開始記錄編程方面的技巧

            原文地址:http://codemacro.com/2012/07/18/start-to-write-tips/

            回首上篇博客的發(fā)表時(shí)間,又2個(gè)月時(shí)間過去了。在我博客生涯的過去兩三年里,總會有好幾個(gè)月的時(shí)間沒有編寫任何文章。我覺得這真是一個(gè)不好的習(xí)慣。這個(gè)情況的產(chǎn)生,有很多原因。例如自己太懶、工作偶爾忙、自己偶爾處于混沌時(shí)期、自己偶爾懷疑人生,如是種種。但最大的原因還是,不敢寫。

            在剛出來工作那會,作為一個(gè)懵懂的青年,接觸個(gè)什么新技術(shù)都內(nèi)心激動驕傲,然后就特別有動力將所學(xué)記錄下來,注意下言辭還能折騰個(gè)像那么回事的教程出來。后來慢慢地,我就覺得,這些東西太膚淺。先別說教人用個(gè)什么IDE,配置個(gè)什么數(shù)據(jù)庫,就算你是學(xué)個(gè)新語言,好好研究下TCP,甚至還能折騰個(gè)IOCP框架,這些都還是他媽的特膚淺。你說任何一個(gè)有那么點(diǎn)經(jīng)驗(yàn)和學(xué)習(xí)能力的程序員,誰花點(diǎn)時(shí)間整不出來這些?誰他媽要你在這里裝逼賣萌,甚至貽笑大方。除此之外,我個(gè)人也覺得無聊了。

            另一方面我覺得寫博客還有個(gè)好處就是幫助自己記錄技術(shù),以便將來萬一又需要曾經(jīng)學(xué)習(xí)過的技術(shù)了,回頭溫習(xí)一下就好。但是后來慢慢地我又覺得,這也是沒必要的事情。因?yàn)榉凑枰@個(gè)技術(shù)的時(shí)候,也花不了多少時(shí)間。

            基于這些亂七八糟的原因,我雖然經(jīng)常打開自己的博客,看看有沒人評論啊,留言啊,但發(fā)表博客的頻率始終上不去。

            后來呢,在google reader上斷斷續(xù)續(xù)也看了些別的程序員的故事。例如有傻逼堅(jiān)持1年每天一篇博客,后來竟然寫了本書;例如有傻逼堅(jiān)持每天翻譯一篇英文文章。我琢磨著這些人該有多么大的毅力啊,就算是翻譯文章,這從找文章篩選文章到最好發(fā)表出來這尼瑪又該睡覺了啊親。心生佩服之余,我覺得自己應(yīng)該向這些傻逼們學(xué)習(xí)。作為一個(gè)已經(jīng)沒有那么多青年時(shí)光的青年,試想以后每天下班回家?guī)薜娜兆樱?jīng)竟碌碌無為地磨過每一個(gè)工作日耍過每一個(gè)工作日晚上,這是件比帶娃更悲劇的事情。

            所以,我也決定堅(jiān)持干一件雖一日不用一次但也望每周那么幾次的事情。我決定在博客上記錄一些編程方面的技巧(tips),集中于某個(gè)小問題的解決、某個(gè)小功能的實(shí)現(xiàn)。這些技巧相比前文說的,就更膚淺了,膚淺到你一google出來的結(jié)果你都嚇一跳的程度。但是我依然覺得這是有用的,就像我用rails做網(wǎng)站,每一個(gè)小功能我都得google一遍,然后積累于心,然后一段時(shí)間后忘掉。為了不忘掉,為了查閱起來簡單,我決定記錄下來。但是僅靠我自己的經(jīng)驗(yàn),是肯定無法做到頻繁地更新的,所以,我決定上stackoverflow上隨機(jī)找些問題/答案翻譯出來。stackoverflow非常適合滿足這種需求,我發(fā)現(xiàn)我google某個(gè)rails技巧時(shí),基本是從stackoverflow上獲取下來的。

            這樣,我的博客http://codemacro.com的rss輸出可能會繁雜點(diǎn),這對于某些人而言估計(jì)會起到惡心的效果。而我自己的博客可能也會變得不那么像個(gè)人博客。我也想過單獨(dú)做個(gè)網(wǎng)站出來,但仔細(xì)想想還是制止自己少瞎折騰了。如有建議歡迎批評。

            好,就這樣,沒了。

            posted @ 2012-07-18 17:52 Kevin Lynx 閱讀(3166) | 評論 (7)編輯 收藏

            tolua的tolua_toxxx系列API設(shè)計(jì)

            原文鏈接:http://codemacro.com/2012/05/10/tolua-api/

            我們使用tolua++手工綁定c/c++接口到lua中,在綁定的接口實(shí)現(xiàn)里,就需要取出傳入的參數(shù)。tolua++中提供了一系列tolua_toxxx函數(shù),例如:

            lua_Number tolua_tonumber(lua_State *L, int narg, lua_Number def)
            const char *tolua_tostring(lua_State *L, int narg, const char *def)
            

            這些函數(shù)都有一個(gè)def參數(shù)。乍一看,這些函數(shù)使用起來很簡單。傳入lua_State,傳入?yún)?shù)在棧中的位置,然后再傳一個(gè)失敗后返回的默認(rèn)值。

            我重點(diǎn)要說的是這里這個(gè)失敗,按正常程序員的理解,針對lua而言,什么情況下算失敗呢?lua語言里函數(shù)參數(shù)支持不傳,此時(shí)實(shí)參為nil,將nil轉(zhuǎn)換為一個(gè)c類型必然失敗;參數(shù)類型不正確算不算失敗?你傳一個(gè)user data,c里按數(shù)字來取,這也算失敗。

            這么簡單的API還需要多糾結(jié)什么呢?然后我們浩浩蕩蕩地寫了上百個(gè)接口,什么tolua_tostring/tolua_tonumber的使用少說也有500了吧?

            然后有一天,服務(wù)器宕機(jī)了,空指針:

            /* 失敗返回"",還能省空指針的判斷 */
            const char *name = tolua_tostring(L, 1, "");
            if (name[0] == '\0') { /* 空串總得判斷吧 */
             ...
            }
            

            跟蹤后發(fā)現(xiàn),腳本里傳入的是nil,這里的name取出來是NULL,而不是”“(的地址)。然后吐槽了一下這個(gè)API,辛苦地修改了所有類似代碼,增加對空指針的判斷。我沒有多想。

            故事繼續(xù),有一天服務(wù)器雖然沒宕機(jī),但功能不正常了:

            float angle = (float) tolua_tonumber(L, 1, 2 * PI);
            ...
            

            這個(gè)意思是,這個(gè)函數(shù)的參數(shù)1默認(rèn)是2*PI,什么是默認(rèn)?lua里某函數(shù)參數(shù)不傳,或傳nil就是使用默認(rèn)。因?yàn)椴粋鞯脑挘@個(gè)實(shí)參本身就是nil。但,tolua_tonumber的行為不是這樣的,它的實(shí)現(xiàn)真是偷懶:

            TOLUA_API lua_Number tolua_tonumber (lua_State* L, int narg, lua_Number def)
            {
             return lua_gettop(L)<abs(narg) ? def : lua_tonumber(L,narg);
            }
            TOLUA_API const char* tolua_tostring (lua_State* L, int narg, const char* def)
            {
             return lua_gettop(L)<abs(narg) ? def : lua_tostring(L,narg);
            }
            

            意思是,只有當(dāng)你不傳的時(shí)候,它才返回默認(rèn)值,否則就交給lua的API來管,而lua這些API是不支持應(yīng)用層的默認(rèn)參數(shù)的,對于lua_tonumber錯(cuò)誤時(shí)就返回0,lua_tostring錯(cuò)誤時(shí)就返回NULL。

            這種其行為和其帶來的common sense不一致的API設(shè)計(jì),實(shí)在讓人蛋疼。什么是common sense呢?就像一個(gè)UI庫里的按鈕,我們都知道有click事件,hover事件,UI庫的文檔甚至都不需要解釋什么是click什么是hover,因?yàn)榇蠹铱吹竭@個(gè)東西,就有了共識,無需廢話,這就是common sense。就像tolua的這些API,非常普通,大家一看都期待在意外情況下你能返回def值。但它竟然不是。實(shí)在不行,你可以模仿lua的check系列函數(shù)的實(shí)現(xiàn)嘛:

            LUALIB_API lua_Number luaL_checknumber (lua_State *L, int narg) {
             lua_Number d = lua_tonumber(L, narg);
             if (d == 0 && !lua_isnumber(L, narg)) /* avoid extra test when d is not 0 */
             tag_error(L, narg, LUA_TNUMBER);
             return d;
            }
            

            即,根本不用去檢查棧問題,直接在lua_tonumber之后再做包裝檢查。何況,lua需要你去檢查棧嗎?當(dāng)你訪問了棧外的元素時(shí),lua會自動返回一個(gè)全局常量luaO_nilobject:

            static TValue *index2adr(lua_State *L, int idx) {
             ...
             if (o >= L->top) return cast(TValue*, luaO_nilobject);
            }
            

            另,程序悲劇也來源于臆想。

            posted @ 2012-05-10 15:38 Kevin Lynx 閱讀(5299) | 評論 (0)編輯 收藏

            談?wù)勎覀兊挠螒蜻壿嫹?wù)器實(shí)現(xiàn)(二)

            原文鏈接:http://codemacro.com/2012/04/25/game-server-info-2/

            上一篇談了一些關(guān)鍵技術(shù)的實(shí)現(xiàn)方案。本篇描述一些遇到的問題。

            在策劃制作完了幾個(gè)職業(yè)后(主要是技能制作),大概去年年底公司內(nèi)部進(jìn)行了一次混戰(zhàn)測試。30個(gè)角色在一個(gè)場景進(jìn)行混戰(zhàn),測試結(jié)果從技術(shù)上來說非常不理想。首先是客戶端和服務(wù)器都巨卡無比。服務(wù)器CPU一直是滿負(fù)載狀態(tài)。而客戶端又頻繁宕機(jī)。

            我們關(guān)注的主要問題,是服務(wù)器CPU滿負(fù)載問題。最開始,我通過日志初步定位為網(wǎng)絡(luò)模塊問題,因?yàn)檫壿嬀€程表現(xiàn)不是那么差。然后考慮到技能過程中的特效、動作都是通過服務(wù)器消息驅(qū)動,并且本身特效和動作就比一般網(wǎng)游復(fù)雜,通過逐一屏蔽這一部分功能,最終確認(rèn)確為網(wǎng)絡(luò)模塊導(dǎo)致。然后團(tuán)隊(duì)決定從兩方面努力:重寫網(wǎng)絡(luò)模塊,改善性能;改善技能實(shí)現(xiàn)機(jī)制,將表現(xiàn)類邏輯移到客戶端。

            至于網(wǎng)絡(luò)模塊,在后來才發(fā)現(xiàn),雖然網(wǎng)絡(luò)流量過高,但導(dǎo)致網(wǎng)絡(luò)線程CPU滿的原因竟然是網(wǎng)絡(luò)模塊自身的流量限制導(dǎo)致。而技能實(shí)現(xiàn)機(jī)制的改善,考慮到改動的成本,最終使用了一種RPC機(jī)制,讓服務(wù)器腳本可以調(diào)用客戶端腳本,并且支持傳入復(fù)雜參數(shù)。然后策劃通過一些關(guān)鍵數(shù)據(jù)在客戶端計(jì)算出特效、動作之類。

            此外,程序?qū)⒏嗟募寄軐傩詮V播給客戶端,一個(gè)客戶端上保存了周圍角色的技能數(shù)據(jù),從而可以進(jìn)行更多的客戶端邏輯。這一塊具體的修改當(dāng)然還是策劃在做(我們的腳本策劃基本就是半個(gè)程序員)。后經(jīng)測試,效果改善顯著。

            在策劃制作了一個(gè)PVP競技副本后,服務(wù)器在10V10測試過程中又表現(xiàn)出CPU負(fù)載較高的情況。這個(gè)問題到目前為止依然存在,只不過情況略微不同。

            首先是觸發(fā)器生命周期的問題。觸發(fā)器自身包含最大觸發(fā)次數(shù)、存留時(shí)間等需求,即當(dāng)觸發(fā)一定次數(shù),或超過存留時(shí)間后,需要由程序自動刪除;另一方面,觸發(fā)器可以是定時(shí)器類型,而定時(shí)器也決定了觸發(fā)器的生命周期。這一塊代碼寫的非常糟糕,大概就是管理職責(zé)劃分不清,導(dǎo)致出現(xiàn)對象自己刪除自己,而刪除后還在依賴自己做邏輯。

            但這樣的邏輯,最多就是導(dǎo)致野指針的出現(xiàn)。不過,這種混亂的代碼,也更容易導(dǎo)致BUG。例如,在某種情況下觸發(fā)器得不到自動刪除了。但這個(gè)BUG并不是直接暴露的,直接暴露的,是CPU滿了。我們的怪物AI在腳本中是通過定時(shí)器類觸發(fā)器驅(qū)動的,每次AI幀完了后就注冊一個(gè)觸發(fā)器,以驅(qū)動下一次AI幀。由于這個(gè)BUG導(dǎo)致觸發(fā)器沒有被刪除,從而導(dǎo)致服務(wù)器上觸發(fā)器的數(shù)量急劇增加。但,這也就導(dǎo)致內(nèi)存增長吧?

            另一個(gè)巧合的原因在于,在當(dāng)時(shí)的版本中,觸發(fā)器是保存一個(gè)表里的,即定時(shí)器類觸發(fā)器、屬性類觸發(fā)器、移動類觸發(fā)器等都在一個(gè)表里。每次任意觸發(fā)器事件發(fā)生時(shí),例如屬性改變,都會遍歷這個(gè)表,檢查其是否觸發(fā)。

            基于以上原因,悲劇就發(fā)生了。在這個(gè)怪物的AI腳本里,有行代碼設(shè)置了怪物的屬性。這會導(dǎo)致程序遍歷該怪物的所有觸發(fā)器。而這個(gè)怪物的觸發(fā)器數(shù)量一直在增長。然后就出現(xiàn)了在很多游戲幀里出現(xiàn)過長的遍歷操作,CPU就上去了。

            找到這個(gè)問題了幾乎花了我一天的時(shí)間。因?yàn)槟_本代碼不是我寫的,觸發(fā)器的最初版本也不是我寫的。通過逐一排除可能的代碼,最終竟然發(fā)現(xiàn)是一行毫不起眼的屬性改變導(dǎo)致。這個(gè)問題的查找流程,反映了將大量邏輯放在腳本中的不便之處:查起問題來實(shí)在吃力不討好。

            修復(fù)了這個(gè)BUG后,我又對觸發(fā)器管理做了簡單的優(yōu)化。將觸發(fā)器列表改成二級表,將觸發(fā)器按照類型保存成幾個(gè)列表。每次觸發(fā)事件時(shí),找出對應(yīng)類型的表遍歷。

            改進(jìn)

            除了修改觸發(fā)器的維護(hù)數(shù)據(jù)結(jié)構(gòu)外,程序還實(shí)現(xiàn)了一套性能統(tǒng)計(jì)機(jī)制,大概就是統(tǒng)計(jì)某個(gè)函數(shù)在一段時(shí)間內(nèi)的執(zhí)行時(shí)間情況。最初這套機(jī)制僅用于程序,但考慮到腳本代碼在整個(gè)項(xiàng)目中的比例,又決定將其應(yīng)用到腳本中。

            這個(gè)統(tǒng)計(jì)需要在函數(shù)進(jìn)入退出時(shí)做一些事情,C++中可以通過類對象的構(gòu)建和析構(gòu)完成,但lua中沒有類似機(jī)制。最初,我使用了lua的調(diào)試庫來捕獲函數(shù)進(jìn)入/退出事件,但后來又害怕這種方式本身存在效率消耗,就取消了。我們使用lua的方式,不僅僅是全局函數(shù),還包括函數(shù)對象。而函數(shù)對象是沒有名字標(biāo)示的,這對于日志記錄不是什么好事。為了解決這個(gè)問題,我只好對部分功能做了封裝,并讓策劃顯示填入函數(shù)對于的字符串標(biāo)示。

            除此之外,因?yàn)橛|發(fā)器是一種重要的敏感資源,我又加入了一個(gè)專門的觸發(fā)器統(tǒng)計(jì)模塊,分別統(tǒng)計(jì)觸發(fā)器的類型數(shù)量、游戲?qū)ο髶碛械挠|發(fā)器數(shù)量等。

            END

            到目前為止,導(dǎo)致服務(wù)器CPU負(fù)載過高,一般都是由BUG導(dǎo)致。這些BUG通常會造成一個(gè)過長的列表,然后有針對這個(gè)列表的遍歷操作,從而導(dǎo)致CPU負(fù)載過高。更重要的,我們使用了這么多的腳本去開發(fā)這個(gè)游戲,如何找到一個(gè)更有效合理的監(jiān)測方法,如何讓程序框架更穩(wěn)定,則是接下來更困難而又必須去面對的事情。

            posted @ 2012-04-25 16:55 Kevin Lynx 閱讀(4193) | 評論 (2)編輯 收藏

            談?wù)勎覀兊挠螒蜻壿嫹?wù)器實(shí)現(xiàn)(一)

            原文鏈接:http://codemacro.com/2012/04/23/game-server-info-1/

            我們的邏輯服務(wù)器(Game Server,以下簡稱GS)主要邏輯大概是從去年夏天開始寫的。因?yàn)楹芏嗷A(chǔ)模塊,包括整體結(jié)構(gòu)沿用了上個(gè)項(xiàng)目的代碼,所以算不上從頭開始做。轉(zhuǎn)眼又快一年,我覺得回頭總結(jié)下對于經(jīng)驗(yàn)的積累太有必要。

            整體架構(gòu)

            GS的架構(gòu)很大程度取決于游戲的功能需求,當(dāng)然更受限于上個(gè)項(xiàng)目的基礎(chǔ)架構(gòu)。基礎(chǔ)架構(gòu)包括場景、對象的關(guān)系管理,消息廣播等。

            需求

            這一回,程序員其實(shí)已經(jīng)不需要太過關(guān)心需求。因?yàn)槲覀儧Q定大量使用腳本。到目前為止整個(gè)項(xiàng)目主要還是集中在技能開發(fā)上。而這個(gè)使用腳本的度,就是技能全部由策劃使用腳本制作,程序員不會去編寫某個(gè)具體技能,也不會提供某種配置方式去讓策劃通過配置來開發(fā)技能。這真是一個(gè)好消息,不管對于程序員而言,還是對于策劃而言。但后來,我覺得對于這一點(diǎn)還是帶來了很多問題。

            實(shí)現(xiàn)

            基于以上需求,程序員所做的就是開發(fā)框架,制定功能實(shí)現(xiàn)方案。腳本為了與整個(gè)游戲框架交互,我們制定了“觸發(fā)器“這個(gè)概念,大概就是一種事件系統(tǒng)。

            這個(gè)觸發(fā)器系統(tǒng),簡單來說,就是提供一種“關(guān)心“、”通知“的交互方式,也就是一般意義上的事件機(jī)制。例如,腳本中告訴程序它關(guān)心某個(gè)對象的移動,那么當(dāng)程序中該對象產(chǎn)生移動時(shí),就通知腳本。腳本中可以關(guān)心很多東西,包括對象屬性,其關(guān)心的方式包括屬性值改變、變大、變小,各種變化形式;對象開始移動,移動停止;對象碰撞,這個(gè)會單獨(dú)談?wù)劊欢〞r(shí)器等。

            除了觸發(fā)器系統(tǒng)外,還有個(gè)較大的系統(tǒng)是游戲?qū)ο蟮膶傩韵到y(tǒng)。對象的屬性必然是游戲邏輯中策劃最關(guān)心最容易改動的模塊。既然我們程序的大方向是盡可能地不關(guān)心策劃需求,所以對象屬性在設(shè)計(jì)上就不可能去編寫某個(gè)具體屬性,更不會編寫這個(gè)屬性相關(guān)的邏輯功能。簡單來說,程序?yàn)槊總€(gè)對象維護(hù)一個(gè)key-value表,也就是屬性名、屬性值表。該表的內(nèi)容由腳本填入,腳本享有存取權(quán)限。然后腳本中就可以圍繞某個(gè)屬性來編寫功能,而程序僅起存儲作用。

            第三,怪物AI模塊。AI模塊的設(shè)計(jì)在開發(fā)周期上靠后。同樣,程序不會去編寫某類AI的實(shí)現(xiàn)。程序提供了另一種簡單的事件系統(tǒng),這個(gè)系統(tǒng)其實(shí)就是一個(gè)調(diào)用腳本的方案。當(dāng)關(guān)于某個(gè)怪物發(fā)生了某個(gè)事件時(shí),程序調(diào)用腳本,傳入事件類型和事件參數(shù)。這個(gè)事件分為兩類:程序類和腳本類。腳本類程序不需關(guān)心,僅提供事件觸發(fā)API。程序類事件非常有限:怪物創(chuàng)建、出生、刪除。

            除了以上三塊之外,還有很多零散的腳本交互。例如游戲?qū)ο髮傩猿跏蓟巧M(jìn)入游戲,角色進(jìn)入場景等。這些都無關(guān)痛癢。

            接下來談一些關(guān)鍵模塊的實(shí)現(xiàn)。

            定時(shí)器

            整個(gè)GS的很多邏輯模塊都基于這個(gè)定時(shí)器來實(shí)現(xiàn)。這個(gè)定時(shí)器接收邏輯模塊的注冊,在主循環(huán)中傳入系統(tǒng)時(shí)間,定時(shí)器模塊檢查哪些定時(shí)器實(shí)例超時(shí),然后觸發(fā)調(diào)用之。這個(gè)主循環(huán)以每幀5ms的速率運(yùn)行,也即幀率1000/5。

            這個(gè)定時(shí)器是基于操作系統(tǒng)的時(shí)間。隨著幀率的不同,它在觸發(fā)邏輯功能時(shí),就必然不精確。游戲客戶端(包括單機(jī)游戲)在幀率這塊的實(shí)現(xiàn)上,一般邏輯功能的計(jì)算都會考慮到一個(gè)dt(也就是一幀的時(shí)間差),例如移動更新,一般都是x = last_x + speed * dt。但,我們這里并沒有這樣做。我們的幾乎所有邏輯功能,都沒有考慮這個(gè)時(shí)間差。

            例如,我們的移動模塊注冊了一個(gè)固定時(shí)間值的定時(shí)器,假設(shè)是200ms。理想情況下,定時(shí)器模塊每200ms回調(diào)移動模塊更新坐標(biāo)。但現(xiàn)實(shí)情況肯定是大于200ms的更新頻率,悲劇的是,移動模塊每次更新坐標(biāo)都更新一個(gè)固定偏移。這顯然是不夠精確的。

            更悲劇的是,定時(shí)器的實(shí)現(xiàn)中,還可能出現(xiàn)跳過一些更新幀。例如,理論情況下,定時(shí)器會在系統(tǒng)時(shí)間點(diǎn)t1/t2/t3/t4分別回調(diào)某個(gè)邏輯模塊。某一幀里,定時(shí)器大概在t1回調(diào)了某邏輯模塊,而當(dāng)該幀耗時(shí)嚴(yán)重時(shí),下一幀定時(shí)器模塊在計(jì)算時(shí),其時(shí)間值為t,而t大于t4,此時(shí)定時(shí)器模塊跳過t2/t3。相當(dāng)于該邏輯模塊少了2次更新。這對于移動模塊而言,相當(dāng)于某個(gè)對象本來在1秒的時(shí)間里該走5格,但實(shí)際情況卻走了1格。

            當(dāng)然,當(dāng)游戲幀率無法保證時(shí),邏輯模塊運(yùn)行不理想也是情有可原的。但,不理想并不包含BUG。而我覺得,這里面是可能出現(xiàn)BUG的。如何改善這塊,目前為止我也沒什么方案。

            移動

            有很多更上層的模塊依賴移動。我們的移動采用了一種分別模擬的實(shí)現(xiàn)。客戶端將復(fù)雜的移動路徑拆分為一條一條的線段,然后每個(gè)線段請求服務(wù)器移動。然后服務(wù)器上使用定時(shí)器來模擬在該線段上的移動。因?yàn)榉?wù)器上的阻擋是二維格子,這樣服務(wù)器的模擬也很簡單。當(dāng)然,這個(gè)模塊在具體實(shí)現(xiàn)上復(fù)雜很多,這里不細(xì)談。

            碰撞檢測

            我們的技能要求有碰撞檢測,這主要包括對象與對象之間的碰撞。在最早的實(shí)現(xiàn)中,當(dāng)腳本關(guān)心某個(gè)對象的碰撞情況時(shí),程序就為該對象注冊定時(shí)器,然后周期觸發(fā)檢測與周圍對象的距離關(guān)系,這個(gè)周期低于100ms。這個(gè)實(shí)現(xiàn)很簡單,維護(hù)起來也就很簡單。但它是有問題的。因?yàn)樗诹艘粋€(gè)不精確的定時(shí)器,和一個(gè)不精確的移動模塊。

            首先,這個(gè)檢測是基于對象的當(dāng)前坐標(biāo)。前面分析過在幀率掉到移動更新幀都掉幀的情況下,服務(wù)器的對象坐標(biāo)和理論情況差距會很大,而客戶端基本上是接近正確情況的,這個(gè)時(shí)候做的距離檢測,就不可能正確。另一方面,就算移動精確了,這個(gè)碰撞檢測還是會帶來BUG。例如現(xiàn)在檢測到了碰撞,觸發(fā)了腳本,腳本中注冊了關(guān)心離開的事件。但不幸的是,在這個(gè)定時(shí)器開始檢測前,這兩個(gè)對象已經(jīng)經(jīng)歷了碰撞、離開、再碰撞的過程,而定時(shí)器開始檢測的時(shí)候,因?yàn)樗诹水?dāng)前的對象坐標(biāo),它依然看到的是兩個(gè)對象處于碰撞狀態(tài)。

            最開始,我們直覺這樣的實(shí)現(xiàn)是費(fèi)時(shí)的,是不精確的。然后有了第二種實(shí)現(xiàn)。這個(gè)實(shí)現(xiàn)基于了移動的實(shí)現(xiàn)。因?yàn)閷ο蟮囊苿邮腔谥本€的(服務(wù)器上)。我們就在對象開始移動時(shí),根據(jù)移動方向、速度預(yù)測兩個(gè)對象會在未來的某個(gè)時(shí)間點(diǎn)發(fā)生碰撞。當(dāng)然,對于頻繁的小距離移動而言,這個(gè)預(yù)測從直覺上來說也是費(fèi)時(shí)的。然后實(shí)現(xiàn)代碼寫了出來,一看,挺復(fù)雜,維護(hù)難度不小。如果效果好這個(gè)維護(hù)成本也就算了,但是,它依然是不精確的。因?yàn)椋惨蕾嚵诉@個(gè)定時(shí)器。

            例如,在某個(gè)對象開始移動時(shí),我們預(yù)測到在200ms會與對象B發(fā)生碰撞。然后注冊了一個(gè)200ms的定時(shí)器。但定時(shí)器不會精確地在未來200ms觸發(fā),隨著幀率的下降,400ms觸發(fā)都有可能。即便不考慮幀率下降的情況,它還是有問題。前面說過,我們游戲幀保證每幀至少5ms,本來這是一個(gè)限幀手段,目的當(dāng)然是避免busy-loop。這導(dǎo)致定時(shí)器最多出現(xiàn)5ms的延遲。如果策劃使用這個(gè)碰撞檢測去做飛行道具的實(shí)現(xiàn),例如一個(gè)快速飛出去的火球,當(dāng)這個(gè)飛行速度很快的時(shí)候,這5ms相對于這個(gè)預(yù)測碰撞時(shí)間就不再是個(gè)小數(shù)目。真悲劇。

            技能

            雖然具體的技能不是程序?qū)懙模绨褞缀跛芯唧w邏輯交給策劃寫帶來的悲劇一樣:這事不是你干的,但你得負(fù)責(zé)它的性能。所以有必要談?wù)劶寄艿膶?shí)現(xiàn)。

            技能的實(shí)現(xiàn)里,只有一個(gè)技能使用入口,程序只需要在客戶端發(fā)出使用技能的消息時(shí),調(diào)用這個(gè)入口腳本函數(shù)。然后腳本中會通過注冊一些觸發(fā)器來驅(qū)動整個(gè)技能運(yùn)作。程序員一直希望策劃能把技能用一個(gè)統(tǒng)一的、具體的框架統(tǒng)一起來,所謂的變動都是基于這個(gè)框架來變的。但策劃也一直堅(jiān)持,他們心目中的技能是無法統(tǒng)一的。

            我們的技能確實(shí)很復(fù)雜。一個(gè)技能的整個(gè)過程中,服務(wù)器可能會和客戶端發(fā)生多次消息交互。在最初的實(shí)現(xiàn)中,服務(wù)器甚至?xí)刂瓶蛻舳说募寄芴匦А⑨尫艅幼鞯雀鞣N細(xì)節(jié);甚至于服務(wù)器會在這個(gè)過程中依賴客戶端的若干次輸入。


            下一篇我將談?wù)勔恍┯龅降膯栴}。

            posted @ 2012-04-25 16:54 Kevin Lynx 閱讀(6361) | 評論 (0)編輯 收藏

            使用Github Page來寫博客

            原文鏈接http://codemacro.com/2012/04/20/blog-on-github/


            最開始知道Github Page,是通過codertrace上的某些注冊用戶,他們的BLOG就建立在Github Page上,并且清一色的干凈整潔(簡陋),這看起來很酷。

            Github提供了很多很合coder口味的東西,例如Gist,也包括這里提到的Page。Page并不是特用于建立博客的產(chǎn)品,它僅提供靜態(tài)頁面的顯示。它最酷的地方,是通過Git的方式來讓你管理這些靜態(tài)頁面。通過建立一個(gè)repository,并使用markdown語法來編寫文章,然后通過Git來管理這些文章,你就可以自動將其發(fā)布出去。

            當(dāng)然,要搭建一個(gè)像樣點(diǎn)的博客,使用Github Page還不太方便。這里可以使用Jekyll。Jekyll是一個(gè)靜態(tài)網(wǎng)頁生成器,它可以將你的markdown文件自動輸出為對應(yīng)的網(wǎng)頁。而Github Page也支持Jekyll。

            為了更方便地搭建博客,我還使用了Jekyll-bootstrap。jekyll-bootstrap其實(shí)就是一些模板文件,提供了一些博客所需的特殊功能,例如評論,訪問統(tǒng)計(jì)。

            基于以上,我就可以像在Github上做項(xiàng)目一樣,編寫markdown文章,然后git push即可。可以使用jekyll --server在本地開啟一個(gè)WEB SERVER,然后編寫文章時(shí),可以在本地預(yù)覽。

            Github Page還支持custom domain,如你所見,我將我的域名codemacro.com綁定到了Github Page所提供的IP,而不再是我的VPS。你可以通過kevinlynx.github.com或者codemacro.com訪問這個(gè)博客。


            當(dāng)然實(shí)際情況并沒有那么簡單,例如并沒有太多的theme可供選擇,雖然jekyll-bootstrap提供了一些,但還是太少。雖然,你甚至可以fork別人的jekyll博客,使用別人定制的theme,但,這對于一個(gè)不想過于折騰的人說,門檻也過高了點(diǎn)。

            jekyll-bootstrap使用了twitter的bootstrap css引擎,但我并不懂這個(gè),所以,我也只能定制些基本的頁面樣式。


            1年前我編寫了ext-blog,并且在我的VPS上開啟了codemacro.com這個(gè)博客。本來,它是一個(gè)ext-blog很好的演示例子,但維護(hù)這個(gè)博客給我?guī)碇T多不便。例如,每次發(fā)布文章我都需要使用更早前用lisp寫的cl-writer,我為什么就不愿意去做更多的包裝來讓cl-writer更好用?這真是一個(gè)垃圾軟件,雖然它是我寫的。另一方面,codemacro.com使用的主題,雖然是我抄的,但依然太丑,并且惡心。

            更別說那個(gè)消耗我VPS所有內(nèi)存的lisp解釋器,以及那惡心的兩位數(shù)字乘法的驗(yàn)證碼---你能想象別人得有多強(qiáng)烈的留言欲望,才愿意開一個(gè)計(jì)算器?


            說說codertrace.com。我其實(shí)寫了篇關(guān)于codertrace.com的總結(jié),但沒有作為博客發(fā)布。做這個(gè)事情的結(jié)果,簡單總結(jié)來說就是瞎JB折騰沒有任何結(jié)果。我真的是個(gè)苦逼雙子男,我每次做件事情都需要巨大的毅力才能讓自己專注下去。

            整個(gè)過程中,收到了些網(wǎng)友的郵件,看到了些評論,雖然不多。郵件/評論中有建議的,也有單純的交流的,也有單純鼓勵的。我想說的是,thanks guys。


            Anyway, try Github Page, save your VPS money :D.

            posted @ 2012-04-20 16:21 Kevin Lynx 閱讀(7759) | 評論 (5)編輯 收藏

            寫了個(gè)簡單的網(wǎng)站,codertrace.com

            簡介

            因?yàn)閷?ext-blog 的原因,慢慢喜歡上github_ 。然后突然有一天產(chǎn)生了一個(gè)想法:如果可以把自己的博客_ 和 github主頁 集中到一塊展示給別人,會不會是一種很方便的自我簡介方式?然后我就動手寫了 codertrace.com

            所以, codertrace.com 這個(gè)網(wǎng)站的作用就是用來集中讓程序員炫耀的。它通過RSS抓取,將你的博客,github主頁,或其他有RSS輸出的信息集中到一塊展示給別人。這些信息通常就可以代表一個(gè)程序員。

            如果你是程序員,也不妨試試。

            技術(shù)信息

            不知道哪個(gè)王八蛋說的,程序員每一年得學(xué)一門新語言。我2010年末接觸了Lisp,然后莫名其妙地寫了 ext-blog ,又莫名其妙地在2011年末接觸了Ruby。因?yàn)榇髮W(xué)期間太癡迷C++,我勤奮努力,幾乎通曉這門語言的各種細(xì)節(jié);后來又稍微實(shí)踐了下編譯原理。在這若干年間,斷斷續(xù)續(xù)也接觸過其他腳本類語言,我甚至在android上用java寫過幾個(gè) 小應(yīng)用 。基于這些積累,我發(fā)現(xiàn)我可以很快上手Ruby,然后再上手Rails,然后就有了 codertrace.com (當(dāng)然還做過一些小的 APP )

            所以, codertrace.com 就是一個(gè)Ruby on Rails的應(yīng)用。當(dāng)我用這貨來做WEB的時(shí)候,我才發(fā)現(xiàn)曾經(jīng)用Lisp寫博客是多么geek。這種感覺就像你在用匯編寫一個(gè)GUI程序一樣。我的意思是,ruby/rails的世界里有太多現(xiàn)成的東西,但lisp的世界里沒有。

            而且,ruby是一個(gè)很爽的語言。我太喜歡它的closure語法,簡潔,不需要加其他關(guān)鍵字就可以構(gòu)造(例如其他語言map(function (item) xxxx end),或者map(lambda (item) xxx ))。但我不喜歡在使用的地方通過yield去調(diào)用---這就像一個(gè)hack。我更不喜歡ruby用proc去封裝closure。好吧,這其實(shí)是我自我分裂,為什么我要把ruby看成一個(gè)函數(shù)式語言?

            腳本語言真是太酷了。

            服務(wù)器信息

            我很窮。不管你信不信,我真的舍不得花1000RMB買個(gè)VPS來架設(shè) codertrace.com 。目前, codertrace.com 架設(shè)在 heroku.com ,而且還使用的是免費(fèi)服務(wù)。免費(fèi)服務(wù)竟然只有5M數(shù)據(jù)庫。 codertrace.com 后臺為了異步抓取用戶提供的RSS,還使用了一個(gè)單獨(dú)的進(jìn)程(delayed_job ruby gem)。這也不是免費(fèi)的。

            但ruby的世界里有太多現(xiàn)成的東西了,甚至有很多現(xiàn)成的庫解決這里的兩個(gè)問題:heroku_external_db,這個(gè)gem可以讓codertrace使用heroku以外的數(shù)據(jù)庫,然后我就在我的VPS上搭了個(gè)mysql,這下流量和網(wǎng)站響應(yīng)速度悲劇了啊,你想你請求個(gè)頁面,這個(gè)頁面直接涉及到若干條數(shù)據(jù)庫查詢。而這些查詢的請求和回應(yīng)竟然是通過internet網(wǎng)絡(luò)傳輸?shù)摹?/p>

            workless,這個(gè)gem可以在有異步任務(wù)時(shí),例如codertrace上讀取RSS,就會自動開啟這個(gè)worker進(jìn)程,然后heroku開始計(jì)費(fèi),當(dāng)沒有任務(wù)時(shí),它又自動關(guān)閉這個(gè)進(jìn)程。雖然省了美元,但再一次讓網(wǎng)站的響應(yīng)速度打了折扣。

            為了實(shí)現(xiàn)自定義域名,我需要將 codertrace.com 指向 heroku.com 提供的IP。但也許你會同我一樣憤怒,因?yàn)樗峁┑膸讉€(gè)IP都被GFW墻了!所以,目前的實(shí)現(xiàn)方案是,我將 codertrace.com 指向了我博客對應(yīng)的VPS,然后在VPS上使用nginx反向代理到 heroku.com 提供的IP。即使如此,我最近甚至發(fā)現(xiàn) codertrace.com 竟然神奇般地會域名解析錯(cuò)誤,難道godaddy的name server也要被GFW和諧??

            故事

            作為一個(gè)宅男,在工作的若干年中,若干個(gè)假期我都用來打游戲,或者寫程序。

            所以,當(dāng)這個(gè)成為習(xí)慣的時(shí)候, codertrace.com ,就順理成章地消費(fèi)了我今年的春節(jié)假期。我發(fā)現(xiàn)一個(gè)人窩在租的小房子里寫代碼是件很爽的事情。在當(dāng)前這個(gè)社會環(huán)境下,你可以專注地去干件喜歡的事情,還不用處理各種生活瑣事,真是太爽了。

            但為什么我平時(shí)得不到這種感覺?因?yàn)椋遥且粋€(gè)沒錢的程序員。我和我老婆租在一個(gè)標(biāo)間里。在這樣狹小的空間里,多個(gè)人就是多幾倍干擾。這太殘酷了。

            末了

            曾經(jīng)我以為我很牛逼,曾經(jīng)我以為程序員很牛逼。后來我慢慢發(fā)現(xiàn)自己很垃圾。我沒有寫出來過牛逼的程序,大概也沒能力寫。還記得那個(gè)程序員的故事嗎?就是有個(gè)傻逼也以為程序員很牛逼,但不幸在一家非IT公司的IT部門工作,他的程序員同事的工作就是每周填個(gè)excel表格。他后來很絕望,因?yàn)樗麤]有為世界貢獻(xiàn)過任何代碼。后來,這貨丟下一切,坐上去某地的飛機(jī)走了。

            posted @ 2012-02-24 09:22 Kevin Lynx 閱讀(3969) | 評論 (17)編輯 收藏

            使用Lisp搭建獨(dú)立博客

            Author: Kevin Lynx
            Date: 9.29.2011
            Contact: kevinlynx at gmail dot com

            本文描述如何使用Lisp工具集搭建一個(gè)完整的個(gè)人博客站點(diǎn)。一個(gè)搭建好的例子站點(diǎn)可以參看我的個(gè)人博客:http://codemacro.com

            要搭建一個(gè)獨(dú)立博客,需要兩方面的支持。一是博客軟件,二是根據(jù)選擇的博客軟件取得必須的“硬件“。例如我這里使用的是Lisp工具集,就需要一個(gè)可以完全控制的服務(wù)器,所以這里我需要一個(gè)VPS。當(dāng)然,購買一個(gè)合適的域名也是必須的。以下將針對這些內(nèi)容做描述。

            獲取VPS及域名

            VPS提供商國內(nèi)國外都有很多。我選擇的是 rapidxen ,128M內(nèi)存,1年70來美元,算是國外比較便宜的,速度上還過得去。

            購買了VPS后,可以進(jìn)入后臺管理頁面安裝VPS操作系統(tǒng)。同樣,因?yàn)槲沂褂玫氖荓isp,我選擇安裝了Debian 6.0 squeeze (minimal)64位。實(shí)際上我更傾向于32位,因?yàn)槲业腜C系統(tǒng)就是32位,方便測試。安裝系統(tǒng)非常簡單,基本隨意設(shè)置下即可。值得注意的是,除了修改root密碼外,最好修改下ssh端口,具體設(shè)置方法可以另行搜索。此外,因?yàn)楹竺嫖視褂胣ginx作為HTTP前端服務(wù)器,為了方便安裝nginx,最好更新下軟件源列表,編輯etc/apt/source.list:

            deb http://ftp.us.debian.org/debian squeeze main
            deb http://packages.dotdeb.org stable all
            deb-src http://packages.dotdeb.org stable all
            deb http://php53.dotdeb.org stable all
            deb-src http://php53.dotdeb.org stable all

            購買VPS最主要的,就是獲取到一個(gè)獨(dú)立IP,如圖:

            imgs/vps.png

            然后可以去購買域名。同樣,也有很多域名服務(wù)商。這里我選擇的是 godaddy ,我選擇的域名codemacro.com一年11美元。購買了域名后,就需要將域名和VPS IP關(guān)聯(lián)起來。詳細(xì)設(shè)置也可以另行搜索。這里簡要提下:在成功登入godaddy后,選擇My Account,進(jìn)入自己的域名,選擇DNS Manager,然后添加域名映射即可,如圖:

            imgs/domain.png

            通過以上設(shè)置后,你購買的域名就成功指向你購買的VPS地址了。可以通過ping來觀察是否指向成功。

            使用Lisp構(gòu)建博客系統(tǒng)

            要在VPS上安裝軟件,首先需要SSH上你的VPS,例如:ssh -p 1234 root@codemacro.com。

            這里使用的軟件集包括:

            • nginx,Web服務(wù)器
            • SBCL ,Lisp編譯器實(shí)現(xiàn)
            • quicklisp ,可以方便自動下載、安裝Lisp庫的工具
            • hunchentoot ,Lisp實(shí)現(xiàn)的Web服務(wù)器(不用特意安裝)
            • ext-blog ,Lisp實(shí)現(xiàn)的博客系統(tǒng)

            實(shí)際上,可以完全使用Lisp作為Web服務(wù)器,但我擔(dān)心效率問題(對個(gè)人博客而言完全沒這回事),所以使用了nginx作為Web服務(wù)器前端,將hunchentoot放在后面。

            安裝nginx

            在設(shè)置好debian軟件源后,安裝非常簡單:

            apt-get install nginx

            安裝完后,因?yàn)橐獙TTP請求轉(zhuǎn)發(fā)給Lisp服務(wù)器,所以需要修改下配置:

            vi /etc/nginx/sites-avaiable/default

            將/請求派發(fā)給Lisp服務(wù)器(假設(shè)監(jiān)聽于8000端口):

            location / {
            proxy_pass http://127.0.0.1:8000;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            }

            然后可以啟動nginx了:

            nginx

            這個(gè)時(shí)候通過瀏覽器訪問,會得到503 bad gateway的錯(cuò)誤提示,因?yàn)閔unchentoot還沒開啟。

            安裝Lisp相關(guān)軟件

            SBCL 同樣可以通過apt直接安裝:

            apt-get instal sbcl

            裝好SBCL后,就可以進(jìn)一步安裝 quicklisp 。可以完全遵守quicklisp官方給的安裝方法進(jìn)行安裝。大概就是先獲取quicklisp.lisp文件,然后在SBCL中載入,根據(jù)提示即可。這里不再贅述。

            安裝好quicklisp后,就可以使用它安裝很多Lisp軟件/庫了。quicklisp在安裝一個(gè)Lisp庫時(shí),會自動下載并安裝依賴庫,就像apt-get一樣。因?yàn)閑xt-blog并未收入到quicklisp的軟件列表里,所以ext-blog需要手動安裝。首先,在本地(非VPS上)獲取ext-blog源碼:

            git clone git://github.com/kevinlynx/klprj.git

            上面的git是我個(gè)人存東西用的,暫時(shí)沒將ext-blog單獨(dú)放置。進(jìn)入到ext-blog目錄。該目錄下有幾個(gè)方便的腳本可以用于博客管理。首先將ext-blog打包并上傳到VPS上,例如:

            ./upload-dist.sh root@codemacro.com 1234 /home/test

            該腳本會調(diào)用make-dist.sh將ext-blog全部源碼打包,然后使用scp拷貝該文件及update-blog.sh到VPS指定的目錄里(這里是/home/test),然后ssh上VPS。期間會兩次輸入VPS系統(tǒng)的密碼。然后以下操作將在VPS上完成。

            首先進(jìn)入到剛才拷貝文件的目錄:

            cd /home/test

            解壓ext-blog.tar.gz:

            tar xvf ext-blog.tar.gz

            然后ext-blog被解壓到/home/test/dist目錄。進(jìn)入此目錄運(yùn)行SBCL:

            cd dist
            sbcl

            ext-blog目錄下dep.lisp會使用quicklisp安裝依賴庫,進(jìn)入SBCL后,載入該文件即可安裝所有依賴庫,這可能需要一點(diǎn)時(shí)間:

            (load "dep.lisp")

            在沒有其他問題下,可以暫時(shí)退出SBCL完成一些其他準(zhǔn)備工作。

            ext-blog在最近的版本中加入了驗(yàn)證碼生成功能,這需要一個(gè)pcf字體文件。因?yàn)樽煮w文件一般較大,所以upload-dist.sh腳本并沒有將該字體文件打包,所以這里需要手動復(fù)制,同樣在本地的ext-blog目錄下:

            scp -P 1234 data/wenquanyi_12ptb.pcf root@codemacro.com:/home/test/dist/data/

            另外,因?yàn)樾枰獙isp解釋器放置在系統(tǒng)后臺執(zhí)行,避免關(guān)掉SSH會話后終止SBCL進(jìn)程,所以這里需要個(gè)工具gnu screen。可以使用apt-get來安裝:

            apt-get install screen

            然后,一切就OK了。在VPS上可以使用ext-blog目錄下的run-blog.sh來運(yùn)行這個(gè)博客(首先確定VPS上的nginx開啟):

            ./run-blog.sh

            該腳本會使用screen在后臺開啟一個(gè)SBCL進(jìn)程,并自動載入ext-blog,然后在8000端口上開啟HTTP服務(wù)。這個(gè)啟動過程可能會使用幾十秒的時(shí)間,直接ctrl+z退出screen,這并不終止SBCL。一段時(shí)間后便可在瀏覽器里測試。

            設(shè)置博客

            如果一切正常,此時(shí)通過瀏覽器訪問你的站點(diǎn)時(shí),會被重定向到一個(gè)博客初始化頁面,如下:

            imgs/initblog.png

            上圖中我是在本機(jī)測試的,所以域名是localhost,希望不至于產(chǎn)生誤解。初始化僅需輸入用戶名和密碼即可,以后可通過該用戶名和密碼進(jìn)入博客后臺管理頁面。完成這一步后,就可以進(jìn)入博客后臺管理頁面做更多的設(shè)置,例如博客標(biāo)題等。

            ext-blog的管理頁面使用了emlog博客系統(tǒng)的CSS及其他資源,因此有同學(xué)覺得管理頁面很面熟就不奇怪了。ext-blog提供在線編輯博客功能,同時(shí)也支持簡單的metaweblog API,因此可以使用一些博客客戶端來發(fā)表文章(僅測過我自己寫的博客客戶端cl-writer)。

            最后

            本文描述較為粗略,主要是很多細(xì)節(jié)我自己也記不清。如有問題可以發(fā)郵件給我。

            posted @ 2011-09-29 17:19 Kevin Lynx 閱讀(6458) | 評論 (7)編輯 收藏

            用Lisp開發(fā)博客系統(tǒng)(WordPress馬甲)

            4月份的時(shí)候基于nuclblog寫過一個(gè)簡單的博客系統(tǒng),但是因?yàn)閷懙贸螅a耦合度高,又有很多硬編碼。當(dāng)然nuclblog本身就寫得不怎么樣,所以6月分的時(shí)候就用Lisp寫了新版的ext-blog。支持自定義主題,套個(gè)馬甲上去像模像樣。

            ext-blog是一個(gè)使用Common Lisp編寫的博客系統(tǒng)。基于之前基于nuclblog修改的經(jīng)驗(yàn),新的ext-blog最大程度地將博客本身的邏輯與前臺渲染分離開,并且添加了對主題 (theme)的支持。制作新的主題可以隨便找一個(gè)WordPress的主題,然后將php代碼翻譯成Lisp代碼即可。

            ext-blog底層代碼非常少,其實(shí)基本的博客系統(tǒng)功能本來就不多。大部分功能都是在6月初完成。那個(gè)時(shí)候公司每天加班,下班回去后還寫點(diǎn)Lisp代碼。后來越整越累,就實(shí)在沒那完善它的心情,一拖就拖到7月底,功能都還不算完善(至少還得加個(gè)rss導(dǎo)出吧?)。

            關(guān)于主題開發(fā)

            ext-blog主要有幾個(gè)頁面派發(fā),對每個(gè)頁面都派發(fā)給具體的主題模塊,讓其完成渲染。編寫一個(gè)主題本質(zhì)上就是生成html頁面。在Lisp的世界中有很多庫可以生成html。ext-blog的主題也不限制你使用哪一個(gè)html生成庫。目前我自己移植的2個(gè)WordPress主題,使用的都是google的closure-template的Lisp移植版本,即cl-closure-template。closure-template會從模板產(chǎn)生出 Lisp函數(shù),這一點(diǎn)是比同類庫中的html-template方便一點(diǎn)。當(dāng)然,作為一個(gè)模板語言,內(nèi)置判斷、循環(huán)則是必須的。

            關(guān)于網(wǎng)絡(luò)框架

            世界上很多流行的語言都有流行的Web開發(fā)框架。Lisp方面,我最開始選用的是Weblocks,我甚至用它為公司寫了個(gè)簡單的訂餐系統(tǒng)(這讓一個(gè)程序員頗有自豪感)。但終究覺得Weblocks太難用,復(fù)雜,但沒有實(shí)際功能。我甚至閱讀了它80%的源代碼,但依然獲取不到如何更好使用它的思想。然后恰好我看了些Rails例子,雖然我不懂Ruby語言(依然可以看到很多語言特性有Lisp的影子),但看懂例子還不是大問題。后來我決定自己寫個(gè) Web框架,因?yàn)槠鋵?shí)我主要需要的就是一個(gè)url派發(fā)(route),就像Rails那樣。我甚至為此做了些詳細(xì)設(shè)計(jì),結(jié)果后來不幸發(fā)現(xiàn)Lisp里已經(jīng)有一個(gè)類似的框架了,這就是Restas。ext-blog基于Restas。

            關(guān)于后臺管理

            后臺管理這東西其實(shí)可要可不要。就算沒有后臺管理,也可以通過增強(qiáng)RPC來實(shí)現(xiàn)。但并不是每個(gè)人都是Lisper,相信想了解ext-blog的人很大一部分都是想學(xué)習(xí)Lisp的人。綜合來看,擁有一個(gè)后臺管理功能,提供更友好的操作界面,也是非常有必要的。但我確實(shí)不擅長做前臺美化的工作。幸運(yùn)地是我將渲染和邏輯分離開了,后臺管理也算是主題的一種。然后,我抄了emlog博客系統(tǒng)的后臺管理,如前所說,也就是把php代碼(雖然我也不懂php)翻譯成lisp代碼。

            關(guān)于開源

            ext-blog是完全有理由發(fā)布到common-lisp.net上的,甚至還可以加入到quicklisp的庫列表里。但前提是排除盡可能多的 bug,寫一系列英文文檔,以及最重要的,對其進(jìn)行長期維護(hù)。不幸的是我目前沒有這個(gè)時(shí)間和精力。所以,只能暫時(shí)在這里發(fā)布下了。


            要圍觀效果的請移步至我的獨(dú)立博客:http://codemacro.com。關(guān)于ext-blog更正式的介紹請移步此篇:http://codemacro.com/view/8

            ps,之前訂閱我獨(dú)立博客的TX麻煩更換下rss地址:http://codemacro.com/feed,而博客主頁也最好換成http://codemacro.com


            posted @ 2011-08-05 16:43 Kevin Lynx 閱讀(5420) | 評論 (4)編輯 收藏

            MMO游戲?qū)ο髮傩栽O(shè)計(jì)

            MMO游戲?qū)ο髮傩栽O(shè)計(jì)

            Author: Kevin Lynx
            Date: 5.2.2011

            一般的MMORPG中,游戲?qū)ο笾饕ü治锖屯婕摇_@兩類對象在經(jīng)過游戲性方面的不斷“進(jìn)化”后,其屬性數(shù)量及與之相關(guān)的邏輯往往會變得很巨大。如何將這一塊做得既不損失效率,又能保證結(jié)構(gòu)的靈活、清晰、可維護(hù)?本文將提供一種簡單的結(jié)構(gòu)。

            原始結(jié)構(gòu)

            最原始的結(jié)構(gòu),極有可能為這樣:

            Player:     +---------------+
                        | property-1    |
                        +---------------+
                        | property-2    |
                        +---------------+
                        |     ...       |
                        +---------------+
                        | operator-1    |
                        +---------------+
                        | operator-2    |
                        +---------------+
                        | ...           |
                        +---------------+
            

            也就是,一個(gè)對象為一個(gè)C++類,然后里面直接塞滿了各種屬性名,然后是針對這個(gè)屬性的邏輯操作(函數(shù))。其結(jié)果就是Player成為巨類。針對這個(gè)情況,一直以來我覺得可以使用一種簡單的方法來拆分這個(gè)類。冠以官腔,稱之為Entity-Component-based Desgin。產(chǎn)生這種想法和我的個(gè)人技術(shù)積累有一定關(guān)系,見下文。

            Policy-based Design

            Policy-based Design,基于決策的設(shè)計(jì)。這個(gè)概念來源于<Modern C++ Design>。雖然這本書講述的是針對C++模板的使用及設(shè)計(jì)技巧。但這種思想依然被我潛意識般地用在其他地方。Policy大致來說就是一個(gè)小的組件(Component)。它努力不依賴于其他東西,它可能就是個(gè)簡單的類,它擁有極少的數(shù)據(jù)結(jié)構(gòu),及針對這些數(shù)據(jù)的極少操作接口。舉例而言,玩家MP的自動回復(fù)功能,就可封裝為一個(gè)Policy。將許多Policy組合起來,就可完成一個(gè)復(fù)雜的功能。

            這種思想還可指導(dǎo)很多程序結(jié)構(gòu)方面的設(shè)計(jì)。例如在做功能的接口拆分時(shí),就將每個(gè)函數(shù)設(shè)計(jì)得足夠小,小到單純地完成一個(gè)功能。一個(gè)功能的入口函數(shù),就將之前實(shí)現(xiàn)的小函數(shù)全部組合起來,然后共同完成功能點(diǎn)。

            當(dāng)然,<Modern C++ Design>里的Policy在表現(xiàn)形式上有所不同。但其核心思想相同,主要體現(xiàn)在 組合 特點(diǎn)上。

            Entity-Component-based Design

            Entity-Component-based Design按照google到的文章,嚴(yán)格來說算是與OOP完全不同的軟件設(shè)計(jì)方法。不過在這里它將按照我的意思重新被解釋。

            如果說Policy-based Design極大可能地影響著我們平時(shí)的細(xì)節(jié)編碼,那么Entity-Component則是直接對游戲?qū)ο蟮慕Y(jié)構(gòu)設(shè)計(jì)做直接的說明。 一個(gè)游戲?qū)ο缶褪且粋€(gè)Entity。 Entity擁有很少的屬性,也許僅包含一個(gè)全局標(biāo)示的ID。 一個(gè)Component則是Entity的某個(gè)行為、或者說某個(gè)組成部分。 其實(shí)說白了,以玩家為例,一個(gè)玩家對象就是一個(gè)Entity,而一個(gè)MP的自動回復(fù)功能就可被包裝為一個(gè)Component。這個(gè)Component可能包含若干與該功能相關(guān)的數(shù)據(jù),例如回復(fù)時(shí)間間隔,每次的回復(fù)量等。我們往玩家對象這個(gè)Entity添加各種Component,也就是給玩家添加各種邏輯功能。

            但是,Component之間可能會涉及到交互,玩家對象之外的模塊可能也會與玩家內(nèi)的某個(gè)Component交互。子功能點(diǎn)的拆分,不得不涉及到更多的膠水代碼,這也算一種代價(jià)。

            游戲?qū)ο髮傩栽O(shè)計(jì)

            這份屬性結(jié)構(gòu)設(shè)計(jì),基本就是參考了上面提到的設(shè)計(jì)思想。整個(gè)系統(tǒng)有如下組件:

            Entity:    +-------------------+
                       | property-table    |
                       +-------------------+
                       | component-table   |
                       +-------------------+
            Property:  +-------------------+
                       | observer-list     |
                       +-------------------+
            Component: +--------------------+
                       | logic-related data |
                       +--------------------+
                       | logic-related func |
                       +--------------------+
            

            意即,所有Entity都包含一個(gè)屬性表和組件表。這里的屬性表并非硬編碼的屬性數(shù)據(jù)成員集合,而是一個(gè)key-value形式的表。Property包含一個(gè)觀察者列表,其實(shí)就是一系列回調(diào)函數(shù),但是這些觀察者本質(zhì)上也是組件,后面會提到。Component正如上文描述,僅包含Component本身實(shí)現(xiàn)的功能所需要的數(shù)據(jù)和函數(shù)。整個(gè)結(jié)構(gòu)大致的代碼如下:

            class Entity {
            private:
                GUID id;
                std::map<std::string, IComponent*> components;
                std::map<std::string, Property*> properties;
            };
            class Property {
            private:
                std::string name;
                Value val;
                std::vector<IComponent*> observers;
            };
            class IComponent {
            public:
                virtual bool Operate (const Args &args) { return false; }
                virtual void OnNotify (const Property &property, const Args &args) {}
            protected:
                std::string name;
                Entity *entity;
            };
            

            屬性本身是抽象的,這完全是因?yàn)槲覀儗傩越y(tǒng)一地放在了一個(gè)表里。從而又導(dǎo)致屬性的值也需要繼續(xù)閱讀

            posted @ 2011-05-02 19:19 Kevin Lynx 閱讀(7044) | 評論 (18)編輯 收藏

            多重繼承和void*的糗事

            多重继承和void*的糗事

            Author:Kevin Lynx
            Date:4.30.2011

            C++为了兼容C,导致了不少语言阴暗面。Bjarne Stroustrup在<D&E>一书里也常为此表现出无奈。另一方面,强制转换也是C++的一大诟病。但是,因为我们的应用环境总是那么“不 纯”,所以也就常常导致各种问题。

            本文即描述了一个关于强制转换带来的问题。这个问题几年前我曾遇到过(<多线程下vc2003,vc2005对虚函数表处理的BUG?>),当时没来得及深究。深究C++的某些语法,实在是件辛苦事。所以,这里也不提过于诡异的用法。

            问题

            考虑下面非常普通的多重继承代码:

            class Left {
            public:
                virtual void ldisplay () {
                    printf ("Left::ldisplay\n");
                }
            };
            
            class Right {
            public:
                virtual void rdisplay () {
                    printf ("Right::rdisplay\n");
                }
            };
            
            class Bottom : public Left, public Right {
            public:
                virtual void ldisplay () {
                    printf ("Bottom::ldisplay\n");
                }
            };
            

            这样子的代码在我们的项目中很容易就会出现,例如:

            class BaseObject;
            class EventListener;
            class Player : public BaseObject, public EventListener
            

            别紧张,我当然不会告诉你这样的代码是有安全隐患的。但它们确实在某些时候会出现隐患。在我们的C++项目中,也极有可能会与一些纯C模块打交道。在C语言里,极有肯能出现以 下的代码:

            typedef void (*allocator) (void *u);
            void set_allocator (allocator alloc, void *u);
            

            之所以使用回调函数,是出于对模块的通用性的考虑。而在调用回调函数时,也通常会预留一个user data的指针,用于让应用层自由地传递数据。

            以上关于多重继承和void*的使用中,都属于很常规的用法。但是当它们遇到一起时,事情就悲剧了。考虑下面的代码:

            Bottom *bobj = new Bottom(); // we HAVE a bottom object
            Right *robj = bobj; // robj point to bobj?
            robj->rdisplay(); // display what ?
            void *vobj = bobj; // we have a VOID* pointer
            robj = (Right*) vobj; // convert it back
            robj->rdisplay(); // display what?
            

            这里的输出结果是什么呢?:

            Right::rdisplay
            Bottom::ldisplay // !!!!
            

            由void*转回来的robj调用rdisplay时,却调用了莫名其妙的Bottom::ldisplay!

            多重继承类的内存布局

            类对象的内存布局,并不属于C++标准。这里仅以vs2005为例。上面例子中,Bottom类的内存布局大概如下:

            +-------------+
            | Left_vptr   |
            +-------------+
            | Left data   |
            +-------------+
            | Right_vptr  |
            +-------------+
            | Right data  |
            +-------------+
            | Bottom data |
            +-------------+
            

            与单继承不同的是,多重继承的类里,可能会包含多个vptr。当一个Bottom对象被构造好时,其内部的两个vptr也被正确初始化,其指向的vtable分别为:

            Left_vptr --->  +---------------------+
                            | 0: Bottom::ldisplay |
                            +---------------------+
            
            Right_vptr ---> +---------------------+
                            | 0: Right::rdisplay  |
                            +---------------------+
            

            转换的内幕

            类体系间的转换

            隐式转换相比强制转换而言,一定算是优美的代码。考虑如下代码的输出:

            Bottom *bobj = new Bottom();
            printf ("%p\n", bobj);
            Right *robj = bobj;
            printf ("%p\n", robj);
            

            其输出结果可能为:

            003B5DA0
            003B5DA4
            

            结论就是,Right *robj = bobj;时,编译器返回了bobj的一个偏移地址。 从语言角度看,就是这个转换,返回了bobj中Right*的那一部分的起始地址。但编译器并不总是在bobj上加一个偏移,例如:

            bobj = NULL;
            Right *robj = bobj;
            

            编译器不会傻到给你一个0x00000004的地址,这简直比NULL更无理。

            void*转换

            编译器当然有理由做上面的偏移转换。那是因为在编译阶段,编译器就知道bobj和Right之间的关系。这个偏移量甚至不需要在运行期间动态计算,或是从某个地方取。如果你看过上面代码对应的汇编指令,直接就是:

            add eax, 4 ; 直接加 sizeof(Left),记住,Right在Left之后
            

            void*就没那么幸运了。void*和Bottom没有任何关系,所以:

            void *vobj = bobj; // vobj的地址和bobj完全相同
            

            然后当你将vobj转换到一个Right*使用时:

            robj = (Right*) vobj;  // 没有偏移转换,robj == vobj == bobj
            robj->rdisplay();
            

            robj指向的是Bottom的起始地址,天啊,在我们学习C++时,我们可以说Bottom就是一个Left,也是一个Right,所谓的is kind of。但这里的悲剧在于,按照上面的逻辑,我们在使用Right时,其实应该使用Bottom里Right那一部分。 但现在这个转换,却让robj指向了Bottom里Left那一部分。

            当调用 robj->rdisplay 时,编译器当然按照Right的内存布局,生成一个虚函数的调用指令,大概就是:

            mov vptr, robj->[0] ;; vptr在robj起始地址处
            mov eax, vptr[0] ;; rdisplay在vtable中位于第一个
            mov ecx, robj
            call eax
            

            总而言之, robj->rdisplay 就是使用偏移0处的值作为vptr,然后使用vptr指向的vtable中第一个函数作为调用。

            但,robj正指向bobj的起始地址,这个地址是放置Left_vptr的地方。这个过程,使用了Left_ptr,而Left_ptr指向的vtable中,第一个函数是什么呢?:

            Left_vptr --->  +---------------------+
                            | 0: Bottom::ldisplay |
                            +---------------------+
            

            正是Bottom::ldisplay!到这里,整个问题的原因就被梳理出来了。

            ;;END;;

            posted @ 2011-04-30 20:14 Kevin Lynx 閱讀(5380) | 評論 (12)編輯 收藏

            僅列出標(biāo)題
            共12頁: 1 2 3 4 5 6 7 8 9 Last 
            a级毛片无码兔费真人久久| 九九久久精品无码专区| 久久精品9988| 亚洲欧美久久久久9999| 久久综合给久久狠狠97色 | 久久这里只有精品首页| 久久久久久九九99精品| 久久天天躁狠狠躁夜夜不卡| 久久精品成人欧美大片| 国内精品久久久久久久久电影网| 久久国产免费直播| 人妻少妇精品久久| 久久精品国产精品青草| 亚洲AV日韩精品久久久久| 久久久久女教师免费一区| 国内精品久久国产大陆| 日韩精品久久久久久免费| 伊人热热久久原色播放www| 99久久精品久久久久久清纯| 亚洲AV无码久久寂寞少妇| 亚洲国产日韩欧美综合久久| 久久男人中文字幕资源站| 日本久久久精品中文字幕| 久久无码人妻一区二区三区午夜| 性做久久久久久久久久久| 品成人欧美大片久久国产欧美| 7777久久亚洲中文字幕| 久久精品国产99久久无毒不卡| 久久精品成人欧美大片| 亚洲中文久久精品无码ww16 | 亚洲国产日韩欧美综合久久| 国产三级观看久久| 国内精品久久久久国产盗摄| 久久国产一片免费观看| 精品久久久无码中文字幕天天| 久久九九有精品国产23百花影院| 97超级碰碰碰久久久久| 久久亚洲精品视频| 99精品久久久久久久婷婷| 久久国产精品视频| 久久精品国产欧美日韩99热|