角色控制是游戲設計中必不可少的一個設計環節,這一節我們講一講如何制作基本的角色運動控制交互邏輯。
因為是簡單實例教程,所以一律不涉及角色動畫控制,只談運動控制。
Demo演示:Simple Movement Control
常見的運動控制交互設計有這么幾種:
- 方向控制類
- 使用單方向控制角色在特定平面內自由運動,角色始終面朝運動方向
- 使用雙方向分別控制角色在特定平面內自由運動的位置以及運動方向(“雙搖桿”射擊)
- 使用單方向控制角色在棋盤格內非自由運動移動
- 目的地控制類
- 設置目的地讓角色自動運動過去(“點擊移動”式)
與運動控制息息相關的是攝影機運動控制,通常會有這么幾種形式:
- 大鳥瞰視角攝影機不運動
- 固定視角攝影機(正俯視、斜俯視、正平視)跟隨或半跟隨角色
- 第三人稱可變視角跟隨攝影機
- 第一人稱主視角攝影機
在Unity3D中,控制游戲物體的運動主要有這樣幾種實現方法:
- 完全依靠物理解算,基本不控制:比如我們在PlayMaker簡單實例(1):PM_Cube中最后做的發射小方塊的例子,設置力量、初始速度,然后就都交給物理引擎去計算了。
- 完全通過transform參數控制位移和旋轉:比如我們讓游戲物體永遠跟隨鼠標運動。
- 借助物理引擎進行部分控制:
- 對Rigidbody持續施加動力(force)或持續設定速度(velocity);
- 對Character Controller設置運動(move);
- 對NavMesh Agent設置運動(move)或設置目的地(set destination)。
完全依靠物理解算不適合用來進行準確操控,完全通過位移和旋轉參數進行控制很難處理碰撞問題,所以最后一種方式是我們常用的。方向控制類通常借助Rigidbody或者Character Controller,目的地控制類通常借助NavMesh Agent。
范例01:單方向運動控制
分析:
單方向運動控制是持續不斷給運動物體指明其運動方向和速度,且運動物體始終面向運動方向的一種控制方法,我們用鍵盤方向鍵、手柄搖桿、甚至用鼠標指針位置來為物體指引方向。
“速度”本質上是一個矢量,其方向代表了運動的方向,其長度代表了運動的快慢。
如果是鍵盤或者手柄的話,使用input axis就可以獲取一個二維的矢量輸入作為方向指示,再添加一個自定義的speed屬性就可以了;如果是鼠標,則可以用鼠標相對于運動物體的位置作為其方向指示,鼠標離運動物體的距離長短作為速度指示。
準備場景
依舊沿用PM_Cube的項目文件,新建一個名叫Movement的場景。創建地面,并拼一個簡易的Player角色出來。
Player角色由一個方塊身體,一個方塊頭部,再加一個黑球以指示正面方向。其實用什么模型都可以,我習慣于用一個Player
空物體作為頂級節點,把其他的視覺元素都放在Player
下方。三個視覺元素的Collider
組件我都刪掉了,不需要它們進行碰撞解算,然后在Player
上添加了一個Box Collider
組件,調整到合適位置。
FSM_Movement
為Player
添加Fsm,改名為FSM_Movement
。
在State 1
中添加一個Get Axis Vector
的行為以獲得Axis Input,再添加一個Set Velocity
的行為給Player設置速度。
出現這個提示代表我們缺少Rigidbody
組件,Set Velocity
只能作用于Rigidbody,點擊提示自動添加。
在Get Axis Vector
中,默認已經把Horizontal Axis
和Vertical Axis
填好了,這個名稱是和Input面板中的設置一一對應的,大家可以從菜單Edit
> Project Settings
> Input
查看。設置Store Vector
參數為一個新的變量input axis
,讓Get Axis Vector
把輸入矢量儲存到這個變量中。
比如我們按下AWSD鍵,由于A
和D
被map到了Horizontal Axis
,W
和D
被map到了Vertical Axis
,因此會造成Horizontal Axis
和Vertical Axis
的輸入值從0快速變成1或者-1(W和D代表正向,為1,A和S代表反向,為-1),然后由于Get Axis Vector
中Map To Plane設置的是“XZ”,所以輸出矢量的X值會取得Horizontal Axis
值,輸出矢量的Z值會取得Vertical Axis
值,輸出矢量的Y為0。
在Set Velocity
中可以直接將input axis
變量賦給Vector
參數,然后設置運動坐標系(Space
)為World,勾選上Every Frame
選項。這樣,游戲每時每刻都會監控Horizontal Axis和Vertical Axis的輸入,并以此調整游戲物體的速度。
Get Axis Vector
的Multiplier
參數會將輸出矢量放大,所以我們也可以為Multiplier
設置一個新變量speed
,并在Variable面板中設置speed
為10,并使其在Inspector中可見。
測試場景,Player在場景中翻滾,這是因為我們給了速度,但物體很輕,地面摩擦力造成其重心不穩容易傾倒(畢竟是剛體物理解算啊)。
一個解決方法是對其Rigidbody的旋轉予以約束,不讓其沿著x軸和z軸旋轉。
另一個辦法是讓地面變得沒有摩擦力。新建一個Physic Material,命名為slippery
,拖到場景中賦給Ground
地面物體,然后修改slippery
的運動摩擦(Dynamic Friction
)和靜態摩擦(Static Friction
)為0。
我這里選擇第一種方法,也就是約束Rigidbody的x軸和z軸旋轉。同時我把Player
的speed
值修改為5,10米/秒的運動速度對于一個1米25高的Player來說有點太快了。
接下來添加Player旋轉的控制。
在Action Browser里輸入“rotate”,會出現很多一些關于旋轉的Action,但這都不是我們會用到的。實際上,適合我們需求的Action的名稱并不包含“rotate”,而是包含“look at”。所以說,對于常用的Action命令還是要記憶一下,否則連關鍵字都打不出來就不好了。
在Transform類別下有很多種“Look At”,看名字知道帶“2d”字眼的肯定是用于二維游戲制作,而帶“Smooth”字眼的很有可能是平滑過渡,而不帶“Smooth”的則可能是突然變化。
選擇Look At
或者Smooth Look At
都可以。
Look At
:
讓Game Object
的正面(Z軸正方向)立刻指向目標物體(Target Object
)或目標坐標(Target Position
),如果Keep Vertical
被勾選,則保證該物體僅繞自身縱軸旋轉,否則使用Up Vector
中設置的方向作為上方。
勾選Draw Debug Line
的話,會在場景視圖中顯示一條Debug Line Color
中所指定顏色的直線以方便判斷是否正確。
Smooth Look At
:
讓Game Object
的正面(Z軸正方向)按一定速度(Speed
)轉向指向目標物體(Target Object
)或目標坐標(Target Position
),如果Keep Vertical
被勾選,則保證該物體僅繞自身縱軸旋轉,否則使用Up Vector
中設置的方向作為上方。
當游戲物體正向與目標所在方向達到Finish Tolerance
允許的接近程度時,觸發Finish Event
中指定的事件。
勾選Debug
時,會在場景視圖中顯示調試用指向箭頭。
現在的問題是如何獲得這個Target Object
或者Target Position
。提供一個思路給大家參考:
如果將Player
自身位置作為坐標原點的話,這個方向實際就是我們Input Axis所指向的方向。所以Target Position = Player Position + Input Axis。在Get Axis Vector
中我們已經得到了一個放大過的input axis
變量了,所以這里直接使用這個值。
在Set Velocity
下方添加一個Get Position
和一個Vector3 Add
。在Get Position
中獲取Player
的位置,儲存在player position
中,然后在Vector3 Add
中把input axis
加給player position
,現在這個player position
值就是我們的目標點位置。
PlayMaker中用Action來進行數學計算就是這么混亂,習慣了就好了。
注意,這個Get Position
和Vector3 Add
都是每幀運行的。
最后,將這個player position
值設置成Smooth Look At
的Target Position
。
更簡單一點的辦法是使用
Smooth Look At Direction
行為,直接把input axis
指定給Target Direction
就好了。
攝影機跟隨
通常這樣的操作方式都會采用俯視攝影機跟隨的方式來設置視角。
最簡單的一個做法其實是把主攝像機當做Player的子物體就好了,但這樣做沒有擴展性,比如以后Player加上跳躍的話,相機視角就會跟著跳起來。
于是更常見的操作是實時根據Player位置去設置主攝像機的位置,直接給Player Position加上一個偏移量(offset)就好了。
選擇Main Camera,添加Fsm(修改名稱為“FSM_Follow”)
State 1
是用來做初始設置的,State 2
才是真正用來設置相機跟隨的狀態。
用專門的初始狀態來做預設定是PlayMaker的常見做法,比如這個例子里面我們需要在游戲場景中尋找一個叫做“Player”的游戲物體,這個操作只需要在游戲初始化的時候做一次就行了,所以放在專門的State里面,執行完畢就轉換到其他State,以后就不用反復執行這種查找操作了。
Find Game Object
常常被用作在場景中根據名稱(Object Name
)來尋找特定游戲物體,同時還可以按照特定標簽(With Tag
)來過濾。找到的游戲物體可以保存(Store
)在一個變量中(這個例子里面我新建了一個player
變量)。
但要記住的是,如果場景中同時有多個同名游戲物體(比如實時生成的克隆物體就都是一樣的名字),
Find Game Object
只會返回所找到的第一個游戲物體,而不會把所有的物體信息都保留下來。所以一定要確保場景中不會出現同名的查找對象。
在State 2
中:
- 添加
Get Position
以讀取player
物體的位置信息,儲存在player position
中; - 然后使用
Vector3 Operator
來計算player position
變量和一個新建的camera offset
變量的相加結果,并儲存在一個新建的變量camera position
中; - 最后添加
Set Position
把變量camera position
變量指定給當前物體(也就是Main Camera
)。
Vector3 Operator
可以對兩個Vector3類型的變量做很多操作:相加、相減、獲取夾角、獲取距離等等,并將結果儲存于第三個變量。我們以后可能會經常用到這個行為。具體這個例子中,也可以直接用
Vector3 Add
來做這個相加的計算,但Vector3 Add
不能指定第三個變量來儲存結果,而是直接改變第一個變量值,所以如果使用Vector3 Add
的話,后面的Set Position
就需要使用player position
這個變量了(現在的player position
變量所代表的位置,已經不是Player所處的位置了,而是偏移之后的位置。
運行測試,在測試狀態下可以實時修改camera offset值(因為之前已經設置其在Inspector中可見了),實時調整合適的相機位置。
一個比較理想的相機位置是Y = 10,Z = -5,然后相機自身的Rotation X = 60。
(紅色界面表示當前處于運行模式)
要注意,運行模式下所做的修改都在退出運行模式后都不會得到保存。一個不是特別完美的解決方案是在點擊相應組件(Component)的設置按鈕(小齒輪圖標),然后選擇
Copy Component
,等退出運行模式以后再同樣點擊“設置”,選擇Paste Component Values
,這樣就可以把運行模式下該組件的參數信息應用到正常模式下了。
范例02:“雙搖桿”式運動控制
分析:
“雙搖桿”是單方向控制的升級版,單獨將運動物體的面對方向拿出來用第二個搖桿或者鼠標指針來予以控制。
搖桿的話,還是使用input axis就可以了;鼠標的話,大多利用鼠標相對于運動物體的位置矢量做指示,讓物體始終朝向這個鼠標的方向。
鼠標是在攝影機平面運動的,要用鼠標來指示運動物體的朝向需要將鼠標平面坐標轉換成三維坐標,因此需要使用Raycast。
如果角色有動畫,“雙搖桿”式控制就要根據兩個方向之間的角度來切換運動動畫,比如人往前走往后走以及側身走的動畫都是不一樣的。
Rigidbody + 雙搖桿
我們可以在上一個范例的基礎上將其改造成“雙搖桿”式運動控制。
將Player
保存成一個prefab,更名為“Player_A”,這時候場景中的Player
也被自動更名為“Player_A”了。
斷開場景中Player_A
與prefab的聯接(菜單GameObject
> Break Prefab Instance
),再將其名稱改回“Player”。
在FSM_Movement中,為State 1
添加一個Mouse Pick
行為。
Mouse Pick
可以幫助我們獲取鼠標指針下的游戲物體的相關信息,我們這里需要獲得的是鼠標指針下地面物體上對應點的三維空間位置。
所以我們設置Mouse Pick
中Store Point
為新建變量mouse point
,設置Layer Mask
為1,并在新出現的Element 0
參數中選擇“Add Layer...”以新建一個叫做“Ground”的“層”,并選擇這個Ground
層為Element 0
參數的值。
當然我們也可以直接在工具欄的Layer設置中直接選擇
Edit Layers...
,或者選擇Ground
物體之后在其Layer設置中選擇Add Layer...
,都可以打開Layer編輯面板。
別忘了,不管用什么辦法打開層編輯面板添加的新層,都需要將
Ground
物體指定到這個層中才會起作用。初學者經常指定了新層,然后設置了層遮罩,卻忘記將游戲物體指定到新層中。
做這樣的設置是為讓Mouse Pick
行為中發射的ray只探測Ground層中的物體,這樣避免探測到我們的主角、房屋、甚至UI物體,造成混亂。
此外,這里這樣做是比較簡化的設計,默認鼠標永遠都會探測到地面物體,永遠都會有數值返回給
Store Point
。
假如鼠標探測不到任何有效的物體,會將Store Point
數值設置為<<0, 0, 0>>
,出現跳幀現象。
想偷懶的話,就把地面設大一點,或者干脆設置一個很大的專門的地面物體不顯示,只用來接受探測。
Mouse Pick
需要被設置成每幀運行。
獲得了鼠標位置點以后,就可以將mouse point
變量賦給一個Look At
行為的Target Position
參數,讓Player永遠面向這個位置。
使用Smooth Look At
也可以,但鼠標的移動和角色的轉向之間就有一點延遲效果了,如果需要這種效果可以選擇Smooth Look At
行為。
我將原來的Smooth Look At Direction
前面的小√去掉了,這樣這個行為就不會起作用,大家也可以將它刪掉。
運行測試,感覺使用Smooth Look At
的效果更順滑一些,覺得轉向太慢可以將Speed
值設置大一些。為了顯示方便,我勾選了Smooth Look At
的Debug
選項,并做了一個小球當做指示物體(再添加一個Set Position
,用來指示小球的位置為mouse point
即可)。
最終完整的Action結構如下圖:
Character Controller + 雙搖桿
Character Controller是Unity3D提供的一個專門用于角色控制的組件:
-
Slope Limit
:角色能夠爬上多大角度的坡 -
Step Offset
:多高(小于設定值)的平臺會被認為是臺階 -
Skin Width
:兩個角色的碰撞體(Capsule Collider)之間能夠相互穿透多大距離 -
Min Move Distance
:可以移動的最小距離(用來避免輕微抖動) -
Center
:角色的Capsule Collider中心高度 -
Radius
:角色的Capsule Collider的截面半徑 -
Height
:角色的Capsule Collider的高度
Character Controller實際上就是一個不可見的Capsule物體,角色模型則被放置在這個Capsule物體內部,隨之而動。
下面我們把上面的例子改造成由Character Controller
組件來控制的角色。
將上面例子中的Player
做成Player_B.prefab
,然后斷開場景中Player的Prefab聯接,名稱改回Player
。
刪除掉Box Collider
組件和Rigidbody
組件,添加Character Controller
組件,設置好Capsule碰撞體的大小和位置(按上圖)。
然后在FSM_Movement
中,刪除Set Velocity
行為,換成Controller Simple Move
行為,把input axis
變量指定給Move Vector
參數,然后把speed
變量指定給Speed
參數。
Controller Simple Move
給Character Controller指定一個方向Move Vector
,并設定一個速度Speed
,Character Controller就可以勻速運動了。
角色指向部分的設置不需要改變。
(折疊的Action都沒有修改)
運行測試,發現角色一跳一跳的,但從場景視圖來看,角色本身的運動是平滑的,問題貌似發生在攝影機的跟隨交互邏輯中。
但是,攝影機的邏輯很清晰啊:獲取Player位置,添加offset,然后設置Camera位置。
真正的問題出在Character Controller
組件的設置里。Min Move Distance
= 0.001的默認設置讓角色的真實運動和其Position屬性并不是完全一致的,這樣攝影機在運動剛開始的幾幀里面就會比角色快。修改Min Move Distance
參數為0就可以保持同步了。
另一個解決方案是在Player內添加一個空物體
Camera Point
,然后讓攝影機不指定Player
而指定這個Camera Point
為其跟隨對象。
運行測試場景,一切正常。
在場景中新建一個Cube物體當做障礙物,Player可以和障礙物的Collider相互碰撞。這是因為Character Controller
組件自己就可以被當成是一個Capsule Collider。只不過以后要做碰撞檢測的時候需要使用專門的Controller Collision行為。
為了不要頻繁地修改
Player
名字,我把Main Camera
的Fsm中的State 1
中的Find Game Object
修改了一下,不指定名字,而是指定Tag,意思是讓其尋找場景中有Player
這個Tag的物體,通常這個物體都只會有一個。這樣一來,只要我將Player的Tag指定為Player,不論它具體名稱是什么,都能被這個行為找到。
為場景物體或者Prefab添加Tag都是在Inspector中左上角進行添加的,如果需要增加新的Tag,點擊
Add Tag...
,跟添加新的Layer是一樣的操作。
把這個Player保存成Player_C.prefab
。
范例03:“點擊移動”式運動控制
分析:
“點擊移動”是給定一個目的地,然后讓運動物體自動移動過去。如果不考慮障礙物的因素,這個目標很容易達成,但如果場景中有障礙物,“點擊移動”就變得很復雜了。
這是因為如果希望運動物體繞開障礙物,就必須讓其具有一定的智能,在游戲設計中我們稱之為“尋路系統”。Unity3D內置一個比較簡單的“尋路系統”:Navigation,主要由Nav Mesh和Nav Agent兩個部分組成。Nav Mesh生成一個可以行走的范圍區域,Nav Agent為運動物體計算出一條可行的路線。
要實現“點擊移動”,需要將Player設置成一個Nav Agent,然后鼠標點擊實際上是告訴這個Nav Agent你的目的地在哪里。
Nav Agent不是Rigidbody,不能使用Set Velocity這樣的行為,但Nav Agent自身也提供了很多方便的函數給我們使用。
要在PlayMaker中運用這套“尋路系統”,需要加載一套專門的命令庫。
老規矩先準備場景。斷開Player的prefab聯接,然后刪掉Character Controller組件和Fsm組件,并在場景中用大方塊拼一些障礙物出來。
為Player添加Fsm,建立如下Graph:
在State 1
中添加Get Mouse Button Down
,設置當鼠標左鍵按下時,觸發事件LMB Down
。
在State 2
中添加Mouse Pick
,設置從鼠標位置對Ground
層中的物體發射Ray,將獲取的點位置儲存在mouse point
中,這個mouse point
就是我們設置的“目的地”。
再添加一個Move Towards
,設置Target Position
為mouse point
,設置Finish Event
為FINISHED
,就可以實現“點擊鼠標左鍵后Player移動到相應點的位置”的需求了。
但是,Player是沿著直線運動的,不會回避障礙物。
Move Towards
是一個不依賴任何物理學計算純粹改變物體位移屬性的Action。它會在物體靠近目的地(距離 <Finish Distance
參數值)時觸發Finish Event
中所指定的事件以跳轉到其他狀態。一旦跳轉狀態,
Move Towards
就不再起效果了,物體會立刻停止移動。
刪掉這個Move Towards,下面我們來建立一個簡單尋路系統。
首先給Player添加一個Nav Mesh Agent組件。
-
Agent Type
:設置Agent類型,主要是控制Agent能跨上多高的臺階,爬上多陡的坡 -
Base Offset
:Agent離地高度 -
Speed
:運動速度 -
Angular Speed
:轉身速度 -
Acceleration
:最快加速度(值越大,啟動越快) -
Stopping Distance
:接近目標距離多近時停止運動 -
Auto Braking
:如果勾選,Agent在接近目標時會自動減速 -
Radius
:Agent碰撞體的截面半徑 -
Height
:Agent碰撞體的高度 -
Quality
:回避障礙物的計算精度,這個質量設置越高越耗費CPU資源,設置成None
就不會回避障礙物而是直接撞上去了 -
Priority
:計算資源不夠時優先度低的Agent會降低回避障礙質量 -
Auto Traverse Off Mesh
:如果需要自行處理角色在Off Mesh Link處的行為的話,不勾選此選項 -
Auto Repath
:讓Agent到達分路徑末端時會自動重新計算路徑 -
Area Mask
:可以設置部分區域不允許該Agent通過
這個組件中可以通過Radius
和Height
參數設置Agent的碰撞體大小。與Character Controller不同,Nav Mesh Agent的碰撞體是圓柱體。
從菜單Window
> Navigation
打開Navigation面板,
選擇地面,勾選Navigation Static
,并將其設置成Walkable
。
選擇所有的障礙物,勾選Navigation Static
,并將其設置成Not Walkable
。
到Bake面板中點擊Bake
按鈕進行Nav Mesh的烘焙。
-
Agent Radius
:這個值越高,障礙物邊緣不可行走的空檔就越大,最好設置成所有Agent中最小的那個的半徑值 -
Agent Height
:這個值越高,半空中有障礙物的區域就越可能不可走過,最好設置成所有Agent中最高的那個的高度 -
Max Slope
:最大可行走坡度 -
Step Height
:最大可行走臺階高度 -
Drop Height
:如果這個值為正,就在小于這個高度的兩塊Nav Mesh之間創建“掉落”類型的Off Mesh Link -
Jump Distance
:如果這個值為正,就在小于這個距離的兩塊Nav Mesh之間創建“跳躍”類型的Off Mesh Link
按照上述設置而烘焙成的Nav Mesh如下圖所示:
藍色區域就是可以行走的區域。
要注意,參與創建Nav Mesh的可行走表面和不可行走表面都需要設置成
Navigation Static
,一旦被設置成Navigation Static
,該物體就不能再移動了,也不能播放動畫。
在場景中選擇Player
,繼續設置Fsm。
想要Nav Mesh Agent運動,可以直接給它設置一個目標,它就會自動尋找合適路線走過去。我們在State 2
中的Mouse Pick
后面添加一個Set Agent Destination
,將Destination
參數指定為mouse point
變量。
注意,
Set Agent Destination
這個Action并不包含在PlayMaker的初始安裝文件中,需要自己去官方WIKI下載適合版本的PathFinding.unitypackage
安裝以后才能正常使用。此外,如果希望能夠在
5.6
版本中就使用最新2017.1
的Navigation系統,可以去Github上相關頁面去下載核心腳本,解壓后放進工程文件夾就可以了。
運行場景,我們可以看到Player現在可以自己找路了。
最終的Demo場景我添加了一個Manager,讓每次載入場景的時候都會隨機選擇不同的Player.prefab來載入,這一部分的具體制作過程就不寫了,搞成隨機的主要原因還是我懶得分成4個不同的場景而已。
項目工程文件(不包括PlayMaker插件及其uGUI擴展Action包)