Unity ECS實例:制作俯視角射擊游戲!

(本文作者 @對馬騎馬使用炎拳 )
大家好,我是炎拳。
這次我們來使用Unity ECS系統(tǒng)制作一個俯視角度的射擊游戲。雖然現(xiàn)在網上有不少ECS的資料和項目,但是制作時又和實際游戲需求有較大差距。在制作這個小游戲的過程中我遇到了很多ECS特有的問題,也給出了還可以的解決方案,相信能通過實例讓大家了解到ECS的優(yōu)缺點是什么。
(文章不會再解釋Unity DOTS的一些基本概念,感興趣的朋友可以查閱文檔了解)。
本游戲具體玩法如下:
1:完全使用鍵盤控制,WASD鍵控制角色方向移動,j 鍵控制射擊。(這樣做主要為了簡化游戲輸入邏輯)
2:玩家有手槍和霰彈槍兩種武器形態(tài),按Q切換。
3:當敵人低于一定量,會在玩家一定距離周圍生成敵人。敵人會朝玩家移動并射擊玩家。
4:玩家和敵人都有生命值,中彈后生命減少,減為0的時候死亡
這里放下Unity和相關Package版本,以免誤導后來者:
Unity 版本:Unity2020.3.3f1,Universal Render Pipeline
Hybrid Renderer: Version 0.11.0-preview.44
Unity Physics: Version 0.6.0-preview.3
Jobs: ?Version 0.8.0-preview.23
Entities: 0.17.0-preview.41
1:準備工作
這里不過多贅述,可以移步我的上一篇文章,前四步都是一樣的:

2.創(chuàng)建主角
先簡單搭建場景,再創(chuàng)建主角。
首先建一個平面,扔上貼圖,再建個圓圓胖胖的主角,添加物理組件Physics Shape 和Physics Body:

Physic Shape的碰撞框同樣可以在場景中進行編輯,你也可以點擊Fit to Enabled Meshs來直接適配:

以及實體轉換組件,Convert To Entity:

我們需要其中主角能被敵人的子彈打中并獲取碰撞事件,所以點擊Collision Response,選擇Raise Trigger Event ( 開啟觸發(fā)器事件),并點擊PhysicBody的Motion Type,選擇Kinematic :

3:主角移動和攝像機跟隨
首先為主角創(chuàng)建一個Component,包含初始速度:
將組件掛到主角身上,speed設為10,再單獨創(chuàng)建一個System,控制主角移動:
相機不支持轉換為Entity,所以我們還是用老辦法做一個跟隨腳本,通過查找包含CharacterComponent的Entity,獲取其Translation,得到主角位置,進行跟隨,代碼如下:
最后給主角手里整把槍,OK,現(xiàn)在主角已經能跑了:

4:實現(xiàn)敵人角色
敵人造型和玩家基本一致,由于玩家需要隨時找到并攻擊玩家角色,所以需要在定義它的Componnet 中存一個玩家Entity的引用:
首先我們需要在主角身旁一定范圍外生成這些這些敵人,方便起見,我們可以在場景中創(chuàng)建一個管理類,存一個已經轉換成實體的的敵人預制體,每次生成的時候直接按照這個模版生成即可,代碼如下:
EnemySystem負責控制敵人追蹤主角,并在敵人數(shù)量少于一定量時生成新的敵人:
點擊運行,敵人也生成出來并開始工作了:

5:子彈,死亡,機器人
接下來我們要定義武器和子彈。雖然Convert to Entity會把面板的物體的子物體也轉換為Entity,并在Entity Debugger中可以看到,但目前GameObject 方便的父子關系還不能在Unity ECS中使用,所以我們需要先記錄槍口的位置。
首先定義武器:
接著定義子彈組件,制作子彈預制體的流程和上文一樣,這里就不贅述了:
再定義一個作刪除標簽功能的組件:DeleteTag,為了盡量避免頻繁的結構性變化(增刪組件等),我們需要在可以被刪除的物體的預制件上添加這個組件,并將其lifeTime設置為1 :
這樣的話,我們就可以定下規(guī)則,當物體身上DeleteTag組件的lifeTime<=0時,系統(tǒng)會將其刪除:
子彈的生命會不斷減少,所以BulletSystem中需要自行對lifeTime 做減法:
WeaponSystem,不同槍械的子彈生命周期也不同,手槍子彈為1s,霰彈槍0.5f:
在主角和敵人身上分別掛上Weapon組件,主角便可以使用兩種武器了,敵人也能自動發(fā)射子彈了:

接下來就要用到ECS中新版的物理組件了,我們先在組件中設置子彈和敵人的碰撞層級,保證同類物體不會觸發(fā)碰撞事件,只有子彈和敵人碰撞會觸發(fā)事件:

這里搜索資料后發(fā)現(xiàn)比較簡單的做法是去定義一個Job繼承ITriggerEventsJob接口,去接收事件,但由于Job中是并行處理數(shù)據(jù),遇到了新的問題,由于代碼比較長,上部分偽代碼來說明:
圖中代碼的意思大概是這樣:當接收到世界中發(fā)生的碰撞事件后,首先Job會判斷碰撞物屬于哪個ComponentGroup,如果Enemy,扣一滴血;包含Bullet,則直接銷毀子彈實體,但實際上寫完運行確遇到了這樣的問題:

可以很明顯的看到,第二次子彈打中物體時,觸發(fā)了兩次碰撞事件。造成這個原因是因為:在Unity ECS系統(tǒng)中,刪除子彈實體的操作并不是立即執(zhí)行的,對于使用EntityCommandBuffer執(zhí)行刪除操作的時序問題,有疑惑的小伙伴可以看這篇文章:
https://zhuanlan.zhihu.com/p/328218005
刪除子彈實體的操作并非立即執(zhí)行,同時刪除子彈實體的操作和TriggerJob也是并行的(不在同一線程,兩者先后順序不確定),所以可能會出現(xiàn)圖中的狀況(箭頭長度代表時間長度):

為了解決這個問題,我首先的思路是為子彈增加一個bool值記錄它的狀態(tài),如果接觸到敵人,再次觸發(fā)碰撞事件時會直接返回,代碼如下:
結果連續(xù)觸發(fā)碰撞事件時,直接報錯The entity does not exist,bullet Group 中并不包含這個引發(fā)碰撞的子彈:

造成這個的原因也比較好猜,當我們執(zhí)行刪除子彈實體的代碼時,子彈實體并不會立即刪除,而是要等到EntityCommandBufferSystem回放命令時統(tǒng)一調度,所以已經子彈可能已經被系統(tǒng)標記為空,自然不在BulletGroup中了,自然也找不到該實體。
解決問題思路還有很多,我們當然可以在代碼中修改Collision Filter,或是關閉子彈的碰撞事件來達成效果。。但實際上這兩種操作都非常麻煩,目前Dots還沒有這么的自由。
在嘗試過上述做法后,我所想到的一個簡單的思路:在發(fā)生碰撞時,將子彈挪到一個看不見位置去,這樣就不會造成多次觸發(fā)碰撞事件;
同時每個子彈都有自己的生命周期,所以也可能發(fā)生子彈生命到了,被標記刪除,但又剛好觸發(fā)碰撞的情況。為了避免這樣的沖突,我們需要在每個Group中都對子彈進行HasComponent判定,子彈刪除代碼如下:
最后再做個敵人被擊退的效果,給敵人添加BeatBack組件,每次被子彈擊中時,敵人都會獲得一個持續(xù)衰減的速度,被連續(xù)擊中時,獲得的加速度也會逐漸衰減:
BeatBackSystem :

完整TriggerEventSystem代碼如下:
6:粒子與音效
目前Particle System 也能正常的轉換為Entity ,但和physic shape等組件一樣,它們還并沒有那么方便使用,所以這里采用了和子彈組件一樣的策略,寫了一個粒子生命周期的組件,在單獨的系統(tǒng)去處理,也不過多贅述了。
至于聲音,沒必要轉換為實體,正常使用就好了~
最后來個全家福,1000個大漢圍攻主角(最初版本,除了物理碰撞基本沒跑多線程,但還是不卡,就是玩):

工程地址:
https://github.com/ydwj/Unity-ECS-FpsGame
ps:工程里面下的商店的免費素材有點大~
歡迎加入游戲開發(fā)群歡樂攪基:1082025059
對游戲開發(fā)感興趣的童鞋可戳這里進一步了解:http://www.levelpp.com/
我們的公眾號:“皮皮關"
B站:“皮皮關做游戲”