文/李偉
在今年“Unreal Open Day虛幻引擎技術開放日”(12月2日-3日)大會上,eBrain Studio負責人李偉圍繞《生死輪迴》和戰鬥系統和隨機地圖系統,從開發設計到實現,展開了分享演講。
以下是整理後的演講實錄:
我叫李偉,是eBrain Studio的負責人。我和我的小夥伴在基於虛幻引擎4開發一款Cyberpunk題材的橫版動作Roguelite遊戲,叫做《生死輪迴》。這是一款故事驅動,以戰鬥,機關和解謎為遊戲性核心的作品,不久面向PC和Console釋出。
在這款遊戲的開發過程中,我擔任製作人的角色。主要負責編劇,設計和gameplay程式設計方面的工作。在此之前,我是orap games的ceo和製作人,以相同的職責基於UDK和虛幻引擎3分別負責了《將死之日》和《忍者龜ol》的開發。
今天我要講的主題,包含兩個部分,圍繞《生死輪迴》的“戰鬥系統”和“隨機地圖系統”展開討論。針對這兩個系統,探討我們是怎麼從設計到實現,遇到和解決問題,以及完成和最終打磨與擴充套件的。
一、戰鬥系統
作為一款動作遊戲,“戰鬥系統”的良好呈現是遊戲品質的關鍵。然而“戰鬥系統” 是一個很泛的主題,因為圍繞戰鬥系統的模組太多,邊界時常又是那麼模糊。所有內容詳細介紹規模太大,逐一涉及不現實,這裡我摘取“近戰武器”和“受擊者反饋”兩個核心版塊的內容進行陳述。
我會採用思考設計,系統羅列,然後逐個詳細的探討他們的技術實現的方式展開。
近戰武器設計 1:技術目標
《生死輪迴》的基礎遊戲性是玩家可以使用多樣的近戰武器與敵人戰鬥。為了豐富遊戲體驗和促進戰鬥追求,我們設計了超過30種特性迥異的近戰武器。可以看到武士刀,大錘,長棍,拳頭等近戰武器。
如何實現武器的基礎傷害功能,拉開不同武器的感受差異是我們的技術實現目標。基於該目標,我們進行分析,達成以上需要我們實現什麼功能特性。
近戰武器設計 2:特性分析
我們從近戰武器的自身特徵著手分析:
(1)武器形狀:每一種武器有其大小不一的形狀。我們需要精確的反應其揮舞時候的檢測結果。可以讓設計師根據武器的外形調整攻擊檢測範圍。
(2)目標角色反饋:擊中各角色反饋表現不同。如人類會飆血,機器人會蹦出火花。敵人的死亡效果也有所不同,如我們想追求硬核和真實,鈍器殺死的敵人會呈現布娃娃效果,利器可以將敵人的肢體切斷。
(3)表面材質反饋:除了擊中角色。我們想像《黑暗之魂》一樣,呈現足夠的細節,武器打到牆壁上,可以濺起火花。
黑暗之魂擊中牆壁
(4)利器和鈍器:利器和鈍器的擊打感受不同,毫無疑問武士刀和棒球棍在擊中目標時的頓感是不同的。前者類似切瓜,後者在擊中目標的時刻能感受到阻力。
武士刀和棒球棍的擊中
近戰武器框架
帶著以上的思考和疑問,我們談談近戰武器系統每一個環節的具體技術實現。
《生死輪迴》的Gameplay功能多是以C++實現,牽扯到視覺化配置的時候,開放給Blueprints子類。
武器類C++程式碼
武器類藍圖程式碼
這樣的好處是,C++可以方便高效的實現演算法和功能。Blueprints可以進行強大的視覺化定製及直觀的呼叫邏輯。武器系統的實現便是採用這種方式。對於武器類,他的父類繼承自AActor。
我們在C++中建立SkeletalMeshComponent元件,這樣在程式碼中就可以方便的訪問到該元件,實現其線性檢測等傷害功能演算法。
在藍圖中設定武器模型及其物理,以供設計師定製擊中粒子反饋,插槽及各項引數。為該武器的SkeletalMesh建立插槽,骨骼插槽可以用來指定每一把武器的檢測起始和結束範圍。
刀類近戰武器插槽
棍類近戰武器插槽
下面我們談談,近戰武器攻擊檢測的邏輯。
近戰武器檢測實現 1:線性檢測
近戰武器揮舞的時候,即是不斷的從插槽的一個端點朝目標端點,發出射線,檢測掃描碰到物件的過程。
實現武器對目標的檢測:虛幻引擎4提供了大量特性多樣的線性檢測API,可以在兩點之間發出射線,根據效率和需要的不同返回掃過的物件資訊。例如我們使用了該線性檢測函式,並對其進行定製封裝,
bool SweepMultiByChannel(TArray& OutHits, const FVector& Start, const FVector& End, const FQuat& Rot, ECollisionChannel TraceChannel, const FCollisionShape& CollisionShape, const FCollisionQueryParams& Params = FCollisionQueryParams::DefaultQueryParam, const FCollisionResponseParams& ResponseParam = FCollisionResponseParams::DefaultResponseParam) const;
簡單看看該函式,在該函式的執行結果中,我們可以返回幾個重要的引數:
- 陣列結構TArray& OutHits會返回一系列掃描到的物件資訊。這裡有一些可以讓我們獲取更多細節的引數,我們後面會展開講。
- 這裡的const FVector& Start, const FVector& End可以傳入武器插槽起始和結束位置。
這兩個引數可以提供給我們剛才提到的需求分析中,最核心的的資訊。OutHits可以幫助我們基於這些資訊設定擊中的反饋粒子,聲音等。Start和End兩個向量可以讓我們的設計師將建立的插槽位置資訊傳入,設定線性檢測的範圍。
虛幻引擎4提供了大量的視覺化Debug工具,如果想看到這條射線的檢測,可以借用DrawDebugline函式檢視。
玩家開啟Debug的線性檢測效果
近戰武器檢測實現2:由角色驅動的檢測通知
毫無疑問,角色在揮舞武器時進行的檢測是最為自然的邏輯。線性檢測本身比較耗費效能,根據使用者的攻擊動畫的執行情況,控制其開啟和關閉。如下我們自定義了攻擊檢測的通知節點,在攻擊動畫播放時呼叫程式碼,進而控制武器的檢測開始TraceStart和結束TraceEnd。
玩家的攻擊動畫AnimNotify通知
當檢測開始之時,在此期間將會不斷地trace。因為該trace既可以寫在Tick之內逐幀執行,也可以自定義一個Timer,讓其按照一定高頻的間隙進行檢測。頻率越高檢測越準確,但是耗能。頻率越低檢測不準確,但是省效能,這裡可以自行取捨。
玩家的Debug線稀疏情況
理論上來說,我們只需要在返回結果佇列中,判斷出Trace的是敵人,對其扣血即可,便實現了檢測傷害。這裡我們讓該武器呼叫ApplyPointDamage傳遞傷害給受擊目標,受擊目標自身進行實際的處理細節。
然而,如果你這樣做的話,會產生一個嚴重的錯誤。你可能會發現,一次武器揮舞,敵人有可能會承受很多次傷害,直至死亡。這就引入了受擊者佇列的話題。
近戰武器檢測實現 3:受擊者佇列
因為在上面我們提到,Trace是在Tick中不斷執行的,同一個敵人可能會在Trace過程中被多次檢測到,其會不斷受到傷害。我們只想在一次武器揮舞的過程中,讓同一個敵人一次承受一擊傷害。正確的做法是,我們需要建立一個佇列來提出這一次TraceStart到TraceEnd的目標,對加入的目標進行排重。
這裡需要澄清一次揮舞和同一條攻擊動畫有多次攻擊的區別,如果你想在一條動畫中執行兩次傷害,就再呼叫一次TraceStart和TraceEnd。
AnimNotify中有多次TraceStart和End
為什麼會有兩次或多次的情況,比如下面的長矛旋轉攻擊。我們想讓這條動畫中長矛每轉半圈產生一次傷害。這樣我們可以讓動畫師按照自己的想法制作一條完整的動畫,又可以讓設計師根據自己的需要設定傷害規律。
使用長矛時在Montage的旋轉動畫
近戰武器擊中反饋實現 1: 擊中目標
FHitResult hitResult;
HitResult含有豐富的擊中目標資訊,如獲取Actor,即是擊中的目標。
在上面的程式碼中,我們可以看到,如何判斷一個敵人被擊中並對其處理。
IsInHurtList就是我剛才談到的,在這次TraceStart和TraceEnd我們將同一個掃到的目標判斷一次,進行排重。IsIgnoreByTeamID用來避免友軍傷害,畢竟我們不想讓敵人之間互相擊中。
近戰武器擊中反饋實現2:飛濺的血液和清脆的金屬
我們也可以訪問到hitResult.impactpoint獲取擊中位置,在這裡播放粒子效果,例如設定飆血。和擊中聲音的位置。精確的反應出武器打到目標的點。
擊中目標的血跡
除此之外,HitResult也可以為我們返回物理材質。物理材質定義了材質表面的屬性,據此設定對應的反饋效果。如我們擊中了水泥地板,可以返回煙塵。木頭的話即是木屑。我們可以在武器中詳細的設定擊打不同物理材質,的反饋效果。
使用武士刀打擊地面火星
我們實現的子彈擊中目標也會根據物理材質,判斷擊中粒子。類似的,角色在移動時,腳步對地面進行線性檢測,也會判斷物理材質,反饋出對應的聲音。
近戰武器擊中反饋實現3:攝像機震動
談談攝像機搖晃和手柄震動。虛幻引擎4 為我們封裝了大量實用的功能,我們的玩家角色的Controller繼承自PlayerController,在該類中,為開發者們提供了攝像機震動的功能。
我們只需要在程式碼中,揮舞武器和擊中敵人的位置進行呼叫。再將對應的CameraShake變數在C++中暴露給藍圖,這樣設計師便可以根據攻擊感受調節對應武器擊中震動效果。讓不同的武器有所差異。
武器擊中目標後的攝像機震動效果
我們不僅僅給擊中目標添加了攝像機震動,設計師還可以根據每一把武器的揮舞起始對攝像機的震動情況進行定製。
手柄的震動反饋與此類似,我們在C++定義了攝像機震動的引用,這樣設計師可以在藍圖中進行定製。使得每一種武器有其獨有的質感表現。
為了拉開不同武器擊中差異感受,我們引入了擊停的功能。
近戰武器擊中反饋實現4:擊打頓感
上面我們提到,我們想讓利器如武士刀和鈍器棒球棍,擊中敵人後的頓感反饋不同。該怎麼實現頓感呢?理論上來說,即是在擊中敵人那一刻,讓系統稍作停頓。我們觀察一下Capcom的街霸:
街霸5
虛幻引擎4的強大功能庫,再次給了我們幫助,在GameplayStatics.h標頭檔案中,封裝了下面的函式。該函式可以設定整個遊戲世界的時間播放速率。
我們在擊中目標時,使其停滯,根據每一把武器設定停滯時間。同樣的將這些引數暴露給設計師,他們可以根據每一把武器,設定擊中敵人的停頓速率,以及持續時長。讓不同的武器擊中感受有所差異。透過肉眼可以看出下面的武士刀和大錘的擊中感受有所差異。
在程式碼中的執行過程是這樣的,武器線性檢測擊中敵人的那一刻,系統速率放慢。很短的時間後,再恢復正常。根據不同武器的特性,例如利器和鈍器的放慢速率和停滯時長有所不同即可。
二、受擊者反饋
受擊者特性分析:動畫,推力和濺血
由上面我們看到,為了呈現近戰武器擊中目標的反饋效果,我們為武器賦予了很多特徵。一個巴掌拍不響,我們還需要為受擊者作出對應的反饋。我們想透過3個方面呈現受擊者的被擊效果:第1條是給受擊目標一定的推力,就像下面這樣。
《空洞騎士》戰鬥,推動敵人
可以看到,《空洞騎士》中對敵人造成推力,使得攻擊極有力量。一方面,推力可以增強玩家的輸出力量反饋,這對於橫版遊戲來說簡直天作之和。
另一方面,我們在製作主角的攻擊動畫時,可以讓其在招式中墊步。玩家不斷的往前推進,敵人受擊後往後退,兩者亦步亦趨的狀態感受很不錯。打人就是要爽,這樣就會有壓著打人的感覺。
第2條是受擊動畫。敵人在受到攻擊時執行受擊動畫是最自然和直接的刺激反饋。第3條是受擊者身體和地面周邊牆壁濺射的血跡。毫無疑問,這可以增加細節感受。細節是我們想追求的。後面可以看到,我們為了呈現細節,實現了多少有趣的功能特性。
下來,我講講推力是怎麼實現的。
受擊者反饋1:推力
我們實現了角色基類ABaseCharacter,繼承自ACharacter。我們的玩家角色和敵人都繼承自ABaseCharacter。ABaseCharacter實現了角色和敵人的共有特性及功能。例如,基礎的傷害處理邏輯。
我們實現了一個角色在一段時間內,獲取受擊推力的系統。該系統可以讓角色獲取一個傳入的方向位移及力量,並且持續一段時間。實現的原理是這樣的,我們讓角色受擊時,開啟一個Timer,在一段間隙內。執行獲取的速度。
在上面提到的ACharacter中,MovementComponent掌管著角色的運動,其中Velocity可以定製我們想讓該角色保持的速度。
在近戰武器擊中敵人時,我們呼叫該功能即可。由於每一種武器有不同的重量感,例如武士刀和大錘,對敵人的擊中力量不同。我們同樣開放給了設計師,可以為每一種武器設定推力的權重。
同時,相同武器在不同招式下的發力大小是不同的,我們按照一定的資料結構,管理了攻擊者對應每一個招式的資料,傳入給該武器,例如其推力。由於敵人的重量感不同,每一種敵人有被推的權重引數,共同決定了受擊後所呈現的推力。
受擊者反饋2:受擊動畫反應
我們將受擊動畫劃分為3類,輕受擊,重受擊和被擊飛。前面我們看到了,武器基於該函式傳入給敵人傷害資訊。
其中我們擴充套件了DamageType,可以決定這次攻擊受擊者應該呈現怎樣的反應。
受擊角色根據自己的傷害型別及當前的狀態執行對應的受擊動畫邏輯。
受擊動畫的道理非常簡單,僅是執行其對應Montage即可。
在上面的程式碼中,我們看到使用了Timer來檢測該動畫是否已經執行完畢。這可以幫助我們追蹤受擊動畫結束事件。
值得一提的是,武器對目標造成什麼樣的擊打反映是由攻擊者決定,例如下面的敵人拿著農具,透過資料結構的配置。讓其最後一招可以將玩家擊飛。
同樣的,玩家也可以使用該武器,根據自己的動畫招式,配置對應的擊打效果。
玩家使用鐵鍬
受擊者反饋3:擊飛
與執行受擊動畫不同的是,被擊飛實現起來比較繁瑣。那麼怎麼實現角色被擊飛呢?為此我們拆分了擊飛這一過程的邏輯,構建了一個狀態機。
對Montage進行拆分管理。狀態大致分為:起飛,飛行(迴圈),落地,爬起。在這裡,飛行這一過程是迴圈執行的,因為我們不知道玩家和敵人是否飛出了平臺,導致其還在空中便執行了落地動畫,這樣會顯得很詭異。這個狀態機根據時間,動畫的執行狀態,動畫中的事件通知AnimNotify以及線性檢測來進行管理。
首先,讓敵人執行被擊飛動畫。這時,我們按照一定的飛行速度給其推力,方向分別為向後和向下。向下的原因是,敵人可能會飛出斜坡,這樣他會始終是一種貼地狀況。我們讓其飛行過程一直對地面進行線性檢測,防止其在過高的地方執行墜落動畫,並且朝其飛行的方向執行線性檢測,檢視是否有撞到牆壁。
如果滿足了飛行時間,或者撞到了飛行方向的牆壁,將會執行下落動畫。在下落動畫中,身體著地的那一刻,關閉其飛行方向的推力。待該條動畫執行完畢後,執行其站起即可。
受擊者反饋4:濺血等細節
為了使戰鬥呈現的更有細節,我們想讓敵人受擊的時候,其身體的傷痕加重,並且血液會濺在四周的牆壁與地面上。
實現過程如下:
我在角色的受傷C++函式中,開放了留給TA同學BP事件介面。他根據該介面,呼叫自己實現的生成濺血貼花BP函式,以及為敵人的材質製作血跡效果,隨著傷害的增多,呼叫材質例項的引數,血跡將會加重。
額外細節:利器擊中角色時,武器尾跡可以拉出細長的血絲。
劍類武器拖拽的血絲
角色死亡特性分析:死亡效果呈現
玩家與敵人戰鬥,成功殺死對方是需要獲得極大滿足感的。無數的精彩動作片告訴我們,敵人的死亡效果會放大這種喜悅。
碟中諜甩飛敵人
我們不想製作很多死亡動畫,還要根據死亡情景來匹配該播放哪段動畫。如判定其是在空中死亡還是地面。這著實在太費神。布娃娃的死亡效果可以滿足我們所需,讓我們免去製作死亡動畫,也省去操心以上各種情景的判斷。最重要的是,物理效果也太有趣了。那麼布娃娃效果怎麼實現呢?
布娃娃死亡
角色死亡實現1:布娃娃效果
- 為角色繫結好布娃娃效果。
布娃娃
- 在敵人死亡時,啟用該角色SkeletalMeshComponent的物理模擬。
在我們遊戲中,當該物理被啟用時,我為其賦予了一定的衝力。這樣敵人將有一種被揍飛的效果。
敵人死亡
物理引擎的使用可以讓遊戲自然,有趣,不可預測。
角色死亡實現 2: 斷肢
下來這一部分可能會令一些聽眾感到不適,我希望聽這一部分主題的聽眾都年滿18歲。《生死輪迴》中有種類繁多的武器。玩家和敵人會遭受鈍器擊傷,利器刺傷砍傷,爆炸等情況。
我們引入了斷肢系統來呈現多樣性。再次提到DamageType類。我們自定義了各種斷肢形式,可以確切的切掉左手,右腿,腦袋。或者隨機斷裂一件肢體,也有可能根據爆炸隨機斷幾處。也有可能讓整個角色四分五裂。
建立了一種資料結構,定義攻擊者的招式引數,在武器檢測的時候傳給武器傷害型別。受擊者會在死亡時根據自己承受的傷害型別,呈現對應的肢體斷裂效果。例如玩家在使用武士刀執行下面的回身砍時,將會對敵人進行斬首。
對敵人斬首效果
那麼斷肢系統是怎麼實現的呢?有兩種思路:
既然我們在上面實現了角色在死亡時執行布娃娃效果。自然而然的,我們會考慮布娃娃的骨骼連線處是否可以解除骨骼繫結,呈現斷裂。
這裡我就不賣關子了。我們並沒有使用該方法,而是採用了另一種思路,實現起來更簡單。
斷肢實現1:隱藏斷掉的肢體
虛幻引擎4的SkeletalMeshComponent類提供了HideBoneByName函式,該函式可以隱藏對應的骨骼。
例如我們傳入角色的腦袋骨骼,其腦袋便會消失。這其實已經解決了問題的一半。
斷肢實現2:生成有物理效果的肢體
我們再生成一個有很好的物理效果的飛出去的腦袋好了。
我們建立了一個叫做BDropActor.cpp的類,專門用於模擬飛出去的有物理效果的物件。
Actor中包含SkeletalMeshComponent元件,生成的時候便會啟用自己的物理效果。除了在這裡用到的斷肢,還有遊戲中玩家扔出去的彈夾,喝完的血瓶。
斷肢,彈夾再編輯器中的模擬
斷肢實現3:最佳化斷裂飛翔肢體的反饋效果
SkeletalMeshComponent綁定了PhysicsAsset,設定正確的碰撞可以接受碰撞事件。我們讓這些物件碰撞到物體表面的時候便可以播放聲音和粒子特效。
在C++中,我們繫結並實現該SkeletalMeshComponent的OnComponentHit的事件,該函式會在Mesh與物體碰撞時執行響應。
需要注意的是,因為OnComponentHit隨著物理會高頻的撞擊,如果我們只是粗暴的在撞擊的時候播放聲音和粒子,將會發現物理效果是非常不可控的。哪怕是輕微的撞擊都會播放粒子和聲音,這樣會持續的嘈雜不堪。
為此我們應該寫一些約束條件:一方面控制播放間隙。另一方面,可以根據撞擊的速度,縮放其撞擊的音量。
如此,反饋將極具細節。例如,墜落撞擊的速度達到一定的閾值才會濺起塵土,撞擊的快慢和聲音的大小匹配。
這樣的演算法我們也應用在布料中,玩家在穿過布料的時候,如果速度夠快,布料撞擊的聲音也會越大。
我們也採用這種方式,讓敵人死亡的時候丟掉自己手中的武器,武器撞擊會產生對應的粒子特效。玩家丟掉的彈夾同樣。
玩家丟掉彈夾,敵人的武器飛出去
在斷肢肢體的碰撞事件中,我們生成貼花來影響環境濺血。例如飛出去的腦袋,在撞擊天花板的時候可以濺起血跡貼花。
對敵人斬首效果
斷肢實現4:斷肢效果呈現
我們預設了角色各部分的肢體的斷裂模型,良好的組織和匹配各個肢體,便可以實現豐富的斷裂種類。無論是單個確定的部位,還是隨機幾個,亦或是下面的手雷可以將敵人炸的四分五裂。都是可能的。
手雷將敵人炸的四分五裂
近戰系統總結:
有了細節,構建了大量的引數對系統良好的配置。是構成近戰系統呈現品質的基礎。在我們的遊戲中,玩家和敵人都使用相同的武器。但是針對玩家和敵人,我們也會根據系統進行一些區分。下面想講一講我們是怎麼實現無縫隨機地圖的。
三、無縫Roguelite地圖
隨機地圖概述
《生死輪迴》一共有7個大關卡,每個關卡在每次進入時都會與上次不同,整個過程無縫連續,具有如同線性遊戲的連貫遊戲體驗。在分享我們的實現技術之前,我想先分析一些傳統隨機地圖的實現方案。這些方法的研究對我們自己的架構和實現,有很高的參考價值與啟發。
實現方案參考1:Procedural 動態生成
製作大批次的隨機房間,對其分類,如這裡是戰鬥房間,補給房間,走廊,還是呈現劇情的地方。定義好出口和入口,按照預設的演算法規則,在關卡初始化的時候。動態的將這些地圖的出口和入口進行連線生成。可能在生成之後還要避免地圖重疊,進行重新計算。
《死亡細胞》這款出色的橫版動作roguelite遊戲和不少俯視角地牢型別的遊戲,便是採用以下方案實現的:
死亡細胞拓撲結構
死亡細胞
該方法有眾多優點,但也有其不足。我們來看看:
優點:
+ 無縫連續,生成後的關卡渾然一體。
+ 隨機度高,演算法生成的房間即便連開發者都無法預測,每次感受煥然一新。大大提升重複可玩性。
缺點:
- 地圖邊界處理,無論是對於設計,如何處理好地圖的美術邊界,還是其連線邏輯都是非常複雜的。沒有采用很好的風格化處理,將會出現嚴重的房間感和割裂感。
- 管理複雜,對房間的製作規範嚴謹,如出口與入口,房間的分類等。
- 控制弱,不好掌控流程,房間的良好分類要求高。對敘事呈現難度高。載入解除安裝面臨的情況可能複雜不好預測。
- Debug難度高,隨著房間迭代量增加,演算法生成的地圖千變萬化,可能會出現一些無法預料之事。難以捕捉到。
實現方案參考2:房間過渡式動態生成
製作好大量的房間,玩家每次離開一個房間,按照一定的拓撲規則,黑屏載入下一個,解除安裝當前。採用這種技術實現方式的Rogulite遊戲不在少數,如《哈迪斯》,《以撒的結合》。然而這種實現方案的優缺點都比較明顯。
哈迪斯
優點:
+ 技術難度低,採用Procedural面臨的地圖無縫拼接這種複雜的美術邊界處理,及其連線技術挑戰,都不再是問題。
+ 效能要求低,每次只需要載入玩家舞臺範圍內的場景。
+ 管理簡單,房間的製作規範,出口和入口都不需要很繁瑣的管理成本。
缺點:
- 無法無縫,一個房間黑屏進入下一個房間,給人極大的割裂感。
- 過場動畫,對於播放過場動畫的房間,玩家每次進來十分繁瑣的載入和解除安裝。
隨機地圖設計1:方案探索
我們想給玩家帶來無縫流暢的遊戲體驗,因此沒有采用類似《哈迪斯》的架構。但是,我們也並沒有直接採用《死亡細胞》式的Procedural方案,原因有二:
1.《生死輪迴》是一款故事驅動的動作遊戲,遊戲內建有超過80分鐘的實時過場動畫,以及許多特殊事件。我們需要對關卡總流程有如同線性遊戲般的掌控力。
2. 在實現了Procedural式的動態生成關卡後,我們感覺到關卡的每一個區域有極強的房間感,無法滿足我們對於銜接處的美術掌控。
隨機地圖設計2:關卡拆分成區域
我們按照線性遊戲的製作思路開發了一個完整的關卡。然後對其按區域劃分。讓每一個區域有滿足整體及周邊風格的迭代。
在對區域進行多次美術迭代之後,我們不禁思考,是否也能對每一個美術區域的邏輯,進行多次迭代,以豐富關卡體驗。
隨機地圖設計3:區域由“美術”和“邏輯構成”
每一個迭代的區域由其“美術”和“邏輯”組合而成,這構成了該區域的獨特遊戲體驗。
“區域美術”意味著玩家目之所及的環境,以及環境構成的遊玩路線。對於一般的線性遊戲,關卡設計師預製了確定的遊戲流程,玩家每次的體驗一致。例如,進入這個關卡所見到的相同敵人及其佈局。
“區域邏輯”意味著玩家來到一個固定風貌場景的區域,所經歷的遊戲邏輯事件,如敵人的生成邏輯,事件指令碼,和關卡,謎題等。
關卡體驗= 關卡美術+關卡邏輯
這裡要說明的是,由於關卡美術決定了遊玩路線,因此只能在其基礎上進行邏輯迭代,而無法反過來。
那麼遊戲體驗的變化是怎樣的呢?舉一個例子:《生化危機4》中採用了一種動態難度機制,當玩家來到教堂的地方,這裡原本只站著幾個近戰寺僧。
生化危機4 的普通近戰敵人
但是如果之前的表現良好,身上的補給物充足。這裡的敵人數目就會發生變化,近戰敵人後面又補充了幾個弩兵。
生化危機4 的弩槍敵人
而採用邏輯分離的方式,我們不僅可以定製:1.敵人佈置:這裡的敵人佈置不一樣。如數量,站位和型別。2.動態難度:隨著玩家能力的提升,這裡的挑戰難度和其能力匹配。3.玩法改變:這次可能是戰鬥,下次可能是解謎。
關卡結構1:關卡區域拆分
每個主關卡,由自己,和按照區域劃分的包含在其中的大量的迭代子關卡構成。
藉助虛幻引擎4的關卡管理器,我們能方便的對一個完成的線性關卡按區域進行劃分。然後對各個區域按照建立子關卡的形式進行迭代。
例如,從上面的地圖中我們看到,A區域有3個美術關卡迭代A_1,A_2,A_3。我們看到,僅是對A_1的美術關卡,我們對其製作了3個邏輯關卡。
最終,A區域的3個美術關卡及其每3個邏輯關卡,可組合呈現9種遊戲體驗。
關卡結構2:主關卡構成
可以看到,在一個主關卡的列表中,包含了所有預設不載入和不可見的迭代子關卡。
我們利用主關卡的LevelBP來進行一些全域性事件的管理,例如控制玩家的死亡事件處理,全域性隨機下雨和霧的控制等。主關卡自己主要還包含了一些天空球,全域性的天光/方向光,後期,LightmassImportanceVolume和路徑體積等全域性物件。
那麼每個區域的隨機載入過程是怎麼實現及運作的呢?
隨機區域實現1:隨機載入管理器
我們實現了一個叫做BRoomArrow的管理器。像前面實現的近戰武器和槍支一樣,我們在C++中實現了其基礎邏輯,為其建立BP子類,方便關卡設計師對其配置。
RoomArrow C++截圖
RoomArrow BP截圖
每個管理器對應每個區域的管理,他們放置在場景中。其自身會按照一定的邏輯規則載入該區域的隨機子關卡。一個區域可以載入哪些隨機關卡由關卡設計師預製。
隨機區域實現 2:地圖的載入過程
在關卡初始化的過程中,按照區域ID,每一個RoomArrow會先載入該區域的“美術子關卡”。“美術子關卡”載入完畢之後,根據其字首名字匹配一個隨機的“邏輯子關卡”。
名字匹配的過程是這樣的,如該地圖的美術關卡為A_1_Btl_R,其將會在A_1_Btl_R_L1~A_1_Btl_R_L3中隨機尋找一個。每一個管理器載入完畢之後會設定其狀態。我們在主關卡的Level BP中檢測所有關卡是否載入完畢,以設定Loading條,和讓玩家按下任意鍵進入遊戲。這樣就完成了隨機關卡的序列化載入。
隨機區域實現 3:關卡的載入和解除安裝
我們同時讓每個管理器肩負著各個區域的載入和解除安裝功能,以平衡效能。我們在該管理器中新建了BoxComponent,其碰撞事件檢測玩家是否跑到區域內,以便設定該區域的可見或隱藏,載入或解除安裝。
隨機區域實現 4:總結
這樣,我們按照一定的設計思路,完成了滿足我們遊戲的無縫Rogulite地圖實現。概括來說,即是:
1.將關卡按區域劃分。
2.關卡美術與關卡邏輯分離。
3.建立管理器控制每個區域的隨機動態載入,關卡設計師預製每個區域的隨機內容。
4.載入解除安裝體積,平衡效能。
四、設計,技術與遊戲性
最後談一談在《生死輪迴》的開發中,圍繞設計,技術與遊戲性實現方面的幾點心得。
- 設計,技術實現和樂趣:大多時候設計產生了需求,推動我們去展開技術實現。但技術在實現的過程中,也會激發我們產生有趣的想法,擴充套件設計。技術激發出的設計,如同童年時期,搗鼓玩具時產生的純粹的,最本質的遊戲性樂趣。這種探索出的設計,往往更加獨特有趣。
- 規模,細節和品質:一個可以掌控的體量做到極致,比一味做大而空泛無聊的遊戲作品有價值的多。
- 那一刀的品質:我們實現了那麼多功能,暴露了那麼多引數給設計師。實現了那麼多粒子特效,向外包反饋了那麼多次音效修改意見。製作了精緻的角色和高品質的攻擊和受擊動畫。最後用一個龐大精美的世界呈現了遊戲舞臺。而玩家在砍下敵人的第一刀之時,便能得出這款遊戲值不值得玩的結論。
- 單個和整個:將每一個模組高品質的實現,再極好的交匯在一起構成系統,是一件極富挑戰之事。是否運轉的良好實在考驗團隊的設計,技術,耐心和運氣。
我的分享就到這裡,謝謝大家。