Home > 腳本編程 > 平台遊戲的遊戲性實驗

平台遊戲的遊戲性實驗

經歷接近一年,這是本 Blog 第一個可「玩」的小測試。測試程式(包括 Lua 源程序)可在本文章末下載。

由於 MilStudio 等著 MUD 模組才好繼續,就先來做一個平台遊戲的遊戲性實驗,過程非常有趣 (可能是因為很多個月裡只是在做引擎和工具),所以這星期一口氣完成了 10 個 stories。

實驗內容

  1. 控制角色,做出走路和跳躍兩個動作。
  2. 繞著角色迴轉的攝影機,角度會按主角移動而自動迴轉,玩家也可以改變角度。
  3. 加入一些箱子,可與角色作物理互動 (角色能推動箱子,箱子也能影響角色)。
  4. 加入移動平台,它們會在兩點中循環移動。角色站在平台上會受平台的慣性影響。例如角色站在水平均速移動的平台向上跳,他掉下來時在平台的位置應該和跳的時候一樣。

實驗過程

由於 MilStudio 並未能提供遊戲世界的永續性 (Persistency),這個實驗的內容 (除了角色的模型和所有材質),全部內容都需要由程序生成。引擎只是每幀呼叫 Lua 裡的兩個全域函數,目前的遊戲循環 (Game Loop) 是這樣的:

  1. 呼叫 Lua runtime 的 BeforeUpdate() 全域函數
  2. 物理模擬
  3. 呼叫 Lua runtime 的 AfterUpdate() 全域函數
  4. 圖像渲染
  5. 回到 1

角色物理

Mil 現時採用 Bullet 物理引擎 2.73 版本。看過官方利用運動學 (Kinematics) 去操控角色的例子後,發覺不太好用,而且例子裡和其他物件碰撞會 crash,所以便想利用剛體動力學 (Rigid Body Dynamics) 去操控角色。

首先,加入 Capsule 作為角色的碰撞體。

之後,要解決角色不會倒下來的問題,最後找到的方法是:

// btRigidBody *mRigidBody;
mRigidBody->setAngularFactor(0);
mRigidBody->setInvInertiaDiagLocal(btVector3(0, 0, 0));
mRigidBody->updateInertiaTensor();

接下來,實驗了不同方法去移動角色,現時的方法是加入衝量 (impulse):

function MainActor:Run(direction)
    local timeStep = Engine.Game_Instance():GetTimeStep()

    -- move
    -- uncomment the following line to disable moving in the air
    --if self.groundTouch then
        local speed = 5
        local factor = 10
        local velocity = direction * speed
        local impulse = (self.groundVelocity + velocity - self.bodyPhysical:GetRigidBody():GetLinearVelocity()) * factor * timeStep
        impulse.y = 0
        self.bodyPhysical:GetRigidBody():ApplyImpulse(impulse, Math.Vector3(0, 0, 0))
    --end

    -- ...
end

如果角色不著地,物理上是不容許角色改便線性動量或角動量的。但是為了容易操控,便把這個限制關掉了。

衝量是速度的變化,這裡把衝量設為目標速度向量 (velocity) 減去角色本身的速度向量。factor * timeStep 是用來控制角色能到達目標速度向量的時間。後來考慮了移動平台,就再加上地上物件的速度。

停下來的時候,也要利用加入相反方向的衝量去加快減速。

角色的旋轉不使用動力學 (為了解決角色會倒下來),所以只用了角度插值 (interpolation)。由於引擎現在沒有 Quaternion,要寫比較多的代碼:

targetOrientation = math.atan2(direction.x, -direction.z)

local pi = 3.14159
local rotateSpeed = 5

if self.orientation - targetOrientation > pi then
    self.orientation = self.orientation - pi * 2
end

if self.orientation - targetOrientation < -pi then
    self.orientation = self.orientation + pi * 2
end

self.orientation = lerp(self.orientation, targetOrientation, math.min(timeStep * rotateSpeed, 1))

跳躍本來以為是最簡單的,只是向上加個固定沖量就行了。不過,跳躍一定要檢測角色是否著地,否測角色就像可以在空中二度 (無限度) 跳躍。

要處理這個問題,需要取得角色和環境的碰撞資訊。為了不額外儲存這些資訊,一般的做法是透過 Callback 函數取得,而且我希望是登記了 Callback 的 Entity 才會收到這些資訊。所以就花了一整個晚上 (我的晚上通常是指九時至二時左右) 看 Bullet 和研究怎樣 callback Lua 的函數。Bullet 的部份找到了,但最後發現,目前的引擎並不容易加入 Lua 的 callback 函數。其中一個原因是,除了腳本模組外,其他模組的 C++ 代碼是不知道有 Lua 存在的,自然不能直接產生 callback。我認為要腳本模組提供一個完整的事件系統才能解決。

之後的一個晚上,放棄了那些碰撞資訊,加入了一個簡單的光線偵測函數,讓 Lua 裡主動檢測角色腳下是否有物件:

function MainActor:OnBeforeUpdate()
    local t = self.body:GetWorldTransform()
    local from = Math.Vector3(t.m03, t.m13, t.m23) 
    local to = from + Math.Vector3(0, -1.2, 0)
    local results = Physics.RayTestResultList()      
    
    if Engine.Game_Instance():GetDynamicsWorld():RayTest(from, to, results) then
        self.groundTouch = true
        self.groundNormal = results[0].normal
        self.groundVelocity = results[0].rigidBody:GetLinearVelocity()
    else
        self.groundTouch = false
    end
end

這個做法的壞處是,當主角在物件邊緣,重心點已在物件外,便算成不著地,也就不能跳。所以最好還是取得碰撞資訊,檢查碰撞的法線是否大致向上(也可檢測是否在斜坡上滑下來),來決定角色是否著地。

移動平台是 Bullet 裡的運動學剛體 (Kinematic Rigid Body),他們的位置完全由程序設定,不會受其他物件影響。

Xbox360 控制器

用鍵盤玩遊戲很不爽,就決定買了一個 Xbox360 控制器 (Controller) 的 PC 接收器。更爽的是第一次用的 XInput 來讀 Xbox 360 控制器的狀態,API 實在太簡單了,只要一個呼叫一個函數 (XInputGetState()) 就足夠了!

但 Xbox360 控制器可以隨時連接和斷線,所以加入了 Controller::IsConnected() 虛擬函數。花了些時間在修改 C# 寫的單元測試程式,可偵測連線狀況去加入或刪去控制器的檢示頁面。

最後,在 Lua 加入了 18 行代碼處理 Xbox360 控制器,就可以使用類比 (Analog) 式的遙桿控制角色移動和視角,操控感覺大大提升!

Lua 的使用

我覺得這星期才真正開始使用 Lua 編程。之前的都只是寫一些單元測試,並不是用 Lua 來解決問題。因為還是不太熟悉 Lua,大家如果看到代碼有甚麼可改進的地方請告之。

首先,使用腳本程式能加快遊戲性的開發。在 MilStudio 裡,加幾行 Lua 代碼,按 F5,有時候有編譯錯誤,改一改再按 F5,就可以測試。除了現時 Mil 的一些錯誤處理和資源管理問題,用 Lua 實在很方便。

Lua 還有很多方便的語言結構,今次就用了 local function 和 closure 來編寫一個建立盒子 Geometry 的函式:

function CreateBoxGeometry(extent)
    local uScale = 0.5
    local vScale = 0.5
    
    local builder = Graphics.GeometryBuilder()
    builder:SetTexcoordCount(1)
    builder:SetTexcoordSize(0, 2)
    builder:SetNormalEnable(true)
    
    local function CreatePlane(n, b, t)
        builder:VertexNormal(n:GetNormalized())		
        
        local u = b:GetNormalized() * uScale
        local v = t:GetNormalized() * vScale
        
        local function CreateVertex(p)
            builder:VertexPosition(p)
            builder:VertexTexcoord(0, Math.Vector2(Math.Vector3_Dot(p, u), Math.Vector3_Dot(p, v)))
            return builder:AddVertex()
        end
        
        builder:AddQuad(
            CreateVertex(n - b - t), 
            CreateVertex(n - b + t),
            CreateVertex(n + b - t),
            CreateVertex(n + b + t))
    end
    
    local h = extent * 0.5
    CreatePlane(Math.Vector3(0, h.y, 0), Math.Vector3(h.x, 0, 0), Math.Vector3(0, 0, h.z))
    CreatePlane(Math.Vector3(0, -h.y, 0), Math.Vector3(h.x, 0, 0), Math.Vector3(0, 0, -h.z))
    CreatePlane(Math.Vector3(h.x, 0, 0), Math.Vector3(0, 0, -h.z), Math.Vector3(0, -h.y, 0))
    CreatePlane(Math.Vector3(-h.x, 0, 0), Math.Vector3(0, 0, h.z), Math.Vector3(0, -h.y, 0))
    CreatePlane(Math.Vector3(0, 0, h.z), Math.Vector3(h.x, 0, 0), Math.Vector3(0, -h.y, 0))
    CreatePlane(Math.Vector3(0, 0, -h.z), Math.Vector3(-h.x, 0, 0), Math.Vector3(0, -h.y, 0))

    local geometry = Graphics.Geometry()
    builder:Build(geometry)
    
    return geometry
end

這個函數是用來建立在測試裡的所有盒子,它需要跟據 object space coordinate 來設定 texture coordinate。

Closure 是指是在 local function 裡能直接使用外部 context 的變數。這函式的 CreateVertex() 使用了 CreatePlane 裡的 builder、u 和 v 變數;CreatePlane() 使用了 builder、uScale 和 vScale。用 C++ 寫的話,就得以參數形式傳入。

GeometryBuilder 這個介面在之前的文章介紹過,這星期加入 Lua 的 SWIG,感覺用起來很方便。雖然在使用 SWIG 的過程中遇過不少困難 (現在還是未能掌握),但只要解決過的問題,加入一般模式的 API 是非常快捷。

MilPlayer

為了獨立執行這個測試程式,加入了一個只 MilPlayer 的項目,現時它是用來執行一個 .lua 的檔案。MilPlayer 是平台相關的,不同平台會建立一個不同的項目。這 Win32 的 MilPlayer 只有 200 行左右的代碼。

Mil 來自希臘符號 μ 的讀音,就是小的意思。這次測試的 EXE + DLL 壓縮後只有 220KB,385 行 Lua 代碼就可以做到這樣的測試,也算是不錯了。希望在不斷加新功能的同時,仍能保持架構和代碼精簡強健。

測試程式下載

執行方法:

  1. 下載 MilTest20090111.zip,解壓至一個目錄。
  2. 如系統沒有D3DX9_37.dll,下載D3DX9_37,並解壓到同一目錄。
  3. 執行 run.bat

MilPlayer 鍵盤操作:

  • 全螢幕/視窗模式: Alt+Enter
  • 遊戲重置: F6

鍵盤操作:

  • 角色移動: W、A、S、D
  • 跳躍: 空白鍵
  • 視角調整: 上、下、左、右方向鍵

Xbox360 控制器操作

  • 角色移動: 左搖桿
  • 跳躍: A
  • 視角調整: 右搖桿

後記

現時這小小的測試還有很多問題,例如角色可以黏著墙壁 (因為整個角色和其他物件都是同一個磨擦力,而角色又可以在空中施力)、站在平台上有抖動問題等。不過對於這個項目來說,我已得等不少的成功感及學到不少新知識。

請各位多給意見,有與趣可以試試改動 lua 文件,用有限的 API 實驗新的遊戲性和關卡。用編程創造出互動性可能是我覺得遊戲開發的最有趣的地方。

Comments:4

ricky 09-01-12 (一) 18:33

第一個來讚

半路 09-01-14 (三) 10:30

讚!下載回來嘗試了一下,真的相當不錯哪!

執行檔加上 DLL 如何達到這麼小的尺寸?
目前我的執行檔至少也有 1 MB 以上的大小。 Orz

我現在正在嘗試使用 ODE 物理引擎,看起來 Bullet 也很好用。
期待 MilStudio 與 MilPlayer 越來越有更多新的進展。 ^^

ricky 09-01-15 (四) 15:08

應該是用了 UPX 作壓縮啊. 不過本來的 size 仍然非常細小。
請問 Milo 兄還有使用什麼技巧嗎?

Milo 09-01-16 (五) 2:08

是使用了 UPX,不過這版本是有 C# Binding 的。去除 C# Binding 的 Static Library 版本會再小一些。

Compilation options 中我關掉 Exception、RTTI (暫時 C# Binding 一個功能需要這個),並 Optimize for size、去除 debug info 等等 。所有 Third-party libraries 也是這樣設定。

因為今次做引擎是希望 Light-weight 一些,設計上盡量簡化 API,可以用 script 做到的、不影響效能的 (例如 input mapping) 就不用 C++ 實現。沒必要的 function 也不會預先做好。希望少些代碼,容易些重構,也會多點時間去增加代碼的質量。

另外,最重要的是……這個引擎現在就是沒有甚麼功能。現時可能一半的 code size 都是來至 Lua 和 Bullet,加入真正的 Graphics Engine 會用不少空間吧。這幾天加上了 freetype2,UPX-ed 的 Mil.dll 又增加了 40KB了。

Comment Form
Remember personal info

Trackbacks:0

Trackback URL for this entry
http://miloyip.seezone.net/wp-trackback.php?p=78
Listed below are links to weblogs that reference
平台遊戲的遊戲性實驗 from Milo的遊戲開發

Home > 腳本編程 > 平台遊戲的遊戲性實驗

Search
Feeds
Meta

Return to page top