下載 :: Game 2048 with Animation
今天的任務是給2048加上動畫。別看用js的transition就搞定的事情,用GDI來實現是有難度的。
2048的游戲實現機制
雖然代碼抄自gabrielecirulli/2048: A small clone of 1024 (https://play.google.com/store/apps/details?id=com.veewo.a1024) ,但是將js代碼用lua去實現還是多費了點工夫。當然兩者都是動態語言,坑比較少。
現在來講下2048是怎樣實現的。
首先,我們看界面,它就是個4x4的方塊,顯而易見,用4x4數組或1x16數組就能搞定。那么數組里面存什么呢?我們分析2048中的數值:空方格、2的冪次數值方塊,就兩種。解決方案是只要存int——空方格對應0;2的冪就對應它的冪。所以:空=0,2=1,以此類推,2048=11。因此數據結構非常簡單,隨便用偽代碼表示:[2048-Table] = array<int>(4x4)。
知道了數據結構,那么接下來就是算法,這是難的部分。
算法需要解決一些問題:
- 處理每次的上下左右動作。這個是核心功能,有幾個方面。一、判斷是否要合並;二、不合並的話就將方塊向前推到邊上,直到推不動為止;三、隨機位置增加新方塊。
- 確定當前是否為死局。這個簡單,只要遍歷4x4數組,看是否存在相鄰的連續數值的方塊。
下面解決主要問題。
一、判斷是否要合並
假如當前按下Left鍵,方塊向左移動,假如某行是“2-2-4-4”,那么結果是“4-8-0-0”,因為只需要合並相鄰的方塊;若是“2-2-2-2”,結果是“4-4-0-0”,不會是“8-0-0-0”,這是由算法決定的。
那么問題變簡單了,正如memcpy所做的,如果memcpy(src,dst),其中src和dst有交界部分。若src在dst前面,那么應該從后向前復制,反之是從前向后。
同理,方向為Left時,對於“2-2-4-4”,是從左向右遍歷,先確定“2-2”,將其換為4,變成“4-0-4-4”,然后處理右邊兩個4。此時,左起第一個4其實已經合並過了,將其排除,所以當前只要處理“0-4-4”,那么再將當中的4移至最左,成為“4-0-4”,再處理第二個4,由於第一個4尚未合並過,因此兩個4再進行合並,成為8。最后結果“4-8-0-0”。
整理一個過程:Left,2-2-4-4,加[]表示已合並過,無需再次合並。2-2-4-4 => [4]-0-4-4 => [4]-4-0-4 => [4]-[8]-0-0。
二、對不能合並的方塊進行移動
如“2-4-6-0”,方向Right,從右向左遍歷。過程為:2-4-6-0 => 2-4-0-6 => 2-0-4-6 => 0-2-4-6。解釋略。
三、隨機增加新方塊
增加新方塊,添加2和4的概率比為9比1,用隨機數實現。
然后需要尋找空位添加,這簡單,遍歷4x4數組,找到數值為0的將其位置記錄,接着隨機抽位子。
---------------------------------------
2048的動畫實現機制
花了一天實現了方塊的移動,也有難度。
項目相關:布局只支持新增GUI對象,不支持刪除,因此需要一開始就創建好。
思路:原4x4中每個方塊對應一個GUI對象(記作origin),另外再新建4x4的GUI對象(記作anime),用於實現動畫。
例:當前2-2-4-4,方向Right,結果為0-0-4-8。設計動畫,假設[n]代表從左起第n個位置。
方塊移動:[1,2,3,4] => [3,3,4,4]。那么當第一個2(位置為[1])進行移動時,這時應該將origin方塊隱藏,將替身anime方塊代替origin位置並顯現,隨后播放動畫,將anime的位置從[1]逐漸移動到[3],移動完畢后,主角origin上場,替身anime下場。
總結一下:當origin需要移動時,召喚替身anime到指定位置,替身移動,最后替身消失,origin在替身消失的地方出現。替身移動其實就是插值思想。
另外,為防止動畫沒有播放完程序仍接受游戲指令導致邏輯亂套,因此在動畫播放期間用戶輸入無效,不然動畫就會出問題。
======我是分割線======
02/23更新
寫在前面
經過堅持不懈的努力,第一個游戲已經制作完成。
游戲邏輯全部用Lua實現,發揮Lua的特長。
前期的辛苦,到第一個游戲完成時,已經煙消雲散了,想必制作游戲的人們都是這個心情吧。
游戲簡介
2048這個游戲很經典,當前它的游戲算法我是抄的:),因為沒足夠時間去思考,但是它的算法難度不算太高,我覺得沒有Popstar implementation(MFC)難寫(當然Popstar我也寫過才敢這么說)。
不過,由於游戲框架功能有限,2048中的動態滑動效果尚未實現,降低了美觀度。
那么搭建這個游戲的框架所具備的最小功能有哪些呢?
- 交互層(Win32):攔截Win32消息,處理好程序與系統的交互。
- 邏輯層(Lua):根據交互層傳來的消息,實現界面或游戲邏輯,保證程序正常運行。
- 渲染層(Direct2D):聽從邏輯層命令,進行窗口的渲染。
從上面可以看出:Lua將交互層與渲染層相解耦,是十分重要的膠水語言。
這樣做的好處有哪些?
- 腳本語言的優勢,如lambda函數、動態類型、yield等
- 只需修改腳本,無需再次編譯程序
- 有出錯提示,便於debug
- 垃圾回收
- 模塊化
初次使用Lua就體會到它強大之處,在意料之中。
實現思路
總結一下編寫簡單的游戲框架並用其實現2048整個過程中的問題。
- Lua的坑。由於對它不熟悉,走了些彎路。Lua中統一用double保存整型與浮點,所以輸出整數就要進行轉換。再者就是類的問題,目前的問題是切換場景后,文本框的內容沒有重置。
- D2D的坑。D2D的文檔比較少,學着費勁。要注意RenderTarget無效問題,無效后必須馬上重新創建,連帶之前的畫刷、圖片、文字等對象要全部重建。
- Win32的坑。這已經見怪不怪了,一個字,略。
那么整體的思路是:
- 交互層,將Win32消息進行封裝,對每個消息調用Lua進行處理。這里比較重要的是窗口改變大小問題,將大小改變時,UI也要跟着改變、跟着放縮,不過UI的問題我全部用Lua腳本去做,省去不少麻煩。
- 邏輯層,主要是Lua腳本的模塊化構建。我的思路:根為場景(Scene),場景下有GDI對象(GdiObject),每個GDI對象可以包含其他GDI對象,組成樹結構。場景切換時,GDI樹銷毀並創建新的。還有一個就是布局(Layout),這里的思路比較經典,掌握基本的遞歸方法就可以寫成。布局里有絕對布局、線性布局、表格布局,都比較簡單,這些布局會自動調整成員的大小。
- 渲染層,不多說,能用現成的就用。像畫純色矩形算簡單,涉及富文本就復雜了,因此我暫時不考慮富文本情況,畫畫基本的幾何圖形就足夠。這方面不算重要的內容。