回顧
學習UE4已有近2周的時間,跟着數天學院“UE4游戲開發”課程的學習,已經完成了UE4藍圖方面比較基礎性的學習。通過UE4藍圖的開發,我實現了類似CS的單人版射擊游戲,效果如下視頻:
不得不說UE4藍圖功能的強大,無需寫一句代碼,就能實現一個基本的游戲玩法。並且使用門檻極低,只要熟悉藍圖的API,通過“拖拖,連連”就能完成游戲玩法的開發,對游戲策划(設計師)及其友好,與C++相比,生產效率極高。
多武器系統
目前的游戲設定是開場后,角色身上就自動裝備了一把武器,為了實現類似於CS雪地地圖一樣的設定:開場沒有武器,地圖中擺放着多把武器,需要從地上拾取后才能使用,就需要將當前的武器系統進行擴展,創建多個藍圖來實現不同武器的模型、特效、屬性等等。
武器玩法邏輯的處理
之前只有單個武器時,武器藍圖包含有玩法邏輯例如彈道計算、粒子顯示、開槍動畫、傷害控制等等,這些玩法(GamePlay)的邏輯是寫在武器藍圖里面的。如果按照以前的設計,創建多個武器藍圖時,這些武器中的玩法邏輯也需要拷貝多份。
拷貝
然而,“拷貝”這種開發方式在程序編碼中是非常不推薦的。所謂拷貝,意味着需要維護多份同樣邏輯實現的代碼,如果后續需要對該部分玩法進行調整 優化(策划又要改需求),那么拷貝了多少次,就需要修改多少次。例如,項目后期有50把武器(參考穿越火線),如果在前期為了省事將武器的玩法邏輯拷貝了49次,那么如果某天有該邏輯的修改需求時,就需要將該修改操作重復50次,這是讓人崩潰的一件事。因此,在軟件開發中,不要輕易使用Ctrl-C + Ctrl-V。
引用
那么該如何解決該問題呢?軟件開發中常用的方法是將拷貝轉變為引用。所謂引用,意思就是將公共的部分剝離出來,形成函數(方法),在需要該邏輯的地方引用該函數即可。如果后期有修改需求,只需要修改該函數一個地方,就可以實現多處邏輯被同時修改。同樣,在UE4的藍圖中也提供了函數(Functions)這樣的功能,通過創建函數可以將藍圖中公共的邏輯封裝 ,從而實現多處引用。但是,UE4的藍圖是面向對象的,不同的武器(對象)之間是不能共用函數的,因此,將公共邏輯改為引用的方式是行不通的。
繼承
既然藍圖是面向對象的,那么可以使用面向對象的編程特點:繼承。所謂繼承,就是將函數、變量封裝到父類,從該父類集成出來的子類可以使用父類暴露出來的方法與屬性。利用繼承的特性,於是我們就可以從藍圖的Actor類中繼承出Weapon類,而我們游戲中的各種武器,例如手槍、沖鋒槍、狙擊槍、火箭炮等等可以使用武器中封裝的一些邏輯,比如開槍,換彈匣,等等,繼承圖如下:
可以看到,各種武器繼承了Weapon之后,就擁有的子彈的變量,也擁有了開槍的函數,從而實現了武器邏輯的復用。
武器屬性的設置
不同的武器除了模型(Mesh)不同以外,還有子彈數量、開槍動畫、裝彈動畫、子彈撞擊粒子、子彈傷害等等不同的屬性,不同的武器這些屬性都不同,而這些屬性都需要在父類Weapon中進行處理。那么如何才能為不同的武器配置這些屬性呢?這就涉及到C++變量如何暴露給藍圖使用。
根據官方文檔:虛幻反射系統,C++中的變量可以被UPROPERTY()
宏修飾,就可以暴露給藍圖使用,還可以根據需要設定訪問權限。
C++代碼如下:
// 擊中目標粒子
UPROPERTY(EditDefaultsOnly)
UParticleSystem* TargetFX;
// 開槍動畫
UPROPERTY(EditDefaultsOnly)
UAnimationAsset* ShootAnim;
// 開槍間隔
UPROPERTY(EditDefaultsOnly)
float ShootInterval;
// 換彈匣動畫
UPROPERTY(EditDefaultsOnly)
UAnimationAsset* ReloadAnim;
// 換彈匣時間
UPROPERTY(EditDefaultsOnly)
float ReloadTime;
// 每顆子彈傷害值
UPROPERTY(EditDefaultsOnly)
float Damage;
// 最大子彈數
UPROPERTY(EditDefaultsOnly)
int8 MaxBullet;
// 是否正在換彈匣
UPROPERTY(BlueprintReadOnly)
bool Reloading = false;
// 是否正在射擊
UPROPERTY(BlueprintReadOnly)
bool Shooting = false;
// 當前子彈數(因為要暴露給藍圖獲取,所以類型擴充到int32)
UPROPERTY(BlueprintReadOnly)
int32 CurrentBullet;
自動生成的藍圖設置如下:
可以看到,如果變量是粒子指針和動畫的指針,藍圖中則直接生成了對應的可視化選擇框,太方便了有木有。這不就是策划所需要的配置表嗎,還是可視化的,再也不用擔心把文件名配錯了。
武器邏輯
除了變量,C++函數有類似的處理方法,宏UFUNCTION()
可以將函數暴露給藍圖,供藍圖調用。因此,上文中提到的換彈匣的邏輯,就可以移植到C++中,從而給予所有武器具有開槍與換彈匣能力。
開槍的核心代碼如下(已精簡):
// 獲取坐標與朝向
World->GetFirstPlayerController()->GetPlayerViewPoint(Location, Rotation);
// 播放開槍動畫
Mesh->PlayAnimation(ShootAnim, false);
// 計算終點坐標 = 起點坐標 + 方向 * 距離
FVector EndLocation = Location + Rotation.Vector() * 10000;
// 發射射線
World->LineTraceSingleByChannel(Result, Location, EndLocation, ECC_WorldStatic, ccq);
// 已擊中
if (Result.Actor.IsValid())
{
// 播放粒子
FRotator EmitterRotation = FRotator(0, 0, 0);
AActor* Actor = Result.Actor.Get();
UGameplayStatics::SpawnEmitterAtLocation(Actor, TargetFX, Result.Location, EmitterRotation);
// 中彈的是Character
if (dynamic_cast<ACharacter*>(Actor) != NULL)
{
// 受傷害
ACharacter* Shooter = dynamic_cast<ACharacter*>(WeaponOwner);
UGameplayStatics::ApplyDamage(Actor, Damage, Shooter->GetController(), this, NULL);
}
}
接下來是換彈匣:
Reloading = true;
// 播放動畫
Mesh->PlayAnimation(ReloadAnim, false);
// 設置延時回調
GetWorldTimerManager().SetTimer(ReloadTimer, this, &AWeapon::ReloadFinish, ReloadTime);
void AWeapon::ReloadFinish()
{
CurrentBullet = MaxBullet;
Reloading = false;
}
因為換彈匣並不是瞬間換好(按了R鍵需要等一定時間后子彈才會恢復),因此使用了定時器來實現。
遇到的問題
- 藍圖中綁定的Mesh無法傳遞到C++
我按照之前的方法,在C++中定義好骨骼Mesh指針:
// 武器mesh
UPROPERTY(EditDefaultsOnly)
USkeletalMeshComponent* MeshComponent;
編譯之后,興沖沖的跑到藍圖中准備選擇Mesh,然而卻發現藍圖中卻沒有出現MeshComponent這個字段,以為是C++代碼沒有編譯到,於是就反復試了幾次,結果還是沒有。怎么辦呢?懷疑是UE4的這個反射系統不支持USkeletalMeshComponent*
這種變量類型,把變量類型改為int32
后,果然,這個字段出現了。
SkeletalMeshComponent不行,那么父類MeshComponent呢?
// 武器mesh
UPROPERTY(EditDefaultsOnly)
UMeshComponent* MeshComponent;
然而,編譯之后藍圖中還是沒有......看來的確是不支持USkeletalMeshComponent*
或者UMeshComponent*
這種類型。怎么辦呢?我突然想到,既然不支持在藍圖中直接選擇默認值,那么在藍圖中調用set方法來設置該變量吧!
於是,我將C++代碼修改為:
// 武器mesh
UPROPERTY(BlueprintReadWrite)
USkeletalMeshComponent* MeshComponent;
藍圖如下:
這樣雖然實現了需求,但是需要在每一把武器藍圖中做一樣的調用,如果有50把武器呢?100把武器呢?這樣做明顯與我的期望不一致,因此這種方法雖然可行,但是不可取。
還有其他辦法嗎?通過查詢UE4 C++的API,我找到了AActor::GetComponentByClass
這個函數,官方文檔的描述是:
Searches components array and returns first encountered component of the specified class
即查找並獲取本Actor的指定類型的組件的第一個。這不正是我需要的嗎?如果我指定查找類型為USkeletalMeshComponent,而每個武器只有一個USkeletalMeshComponent,那這樣不就從藍圖中獲取到了綁定的Mesh嗎?實現代碼如下:
void AWeapon::BeginPlay()
{
Super::BeginPlay();
// 獲得Mesh
MeshComponent = dynamic_cast<USkeletalMeshComponent*>(GetComponentByClass(USkeletalMeshComponent::StaticClass()));
}
通過這種方式,實現了武器的全部邏輯從藍圖中去除。當新加一件武器時,只需要在武器藍圖屬性中配置動畫、粒子、子彈容量等屬性后,就可以直接在游戲中使用。
藍圖與C++選擇的思考
既然UE4同時支持藍圖與C++,那么我們在開發時應該如何選擇呢?官方文檔有如下的解釋:
程序員利用C++即可添加基礎Gameplay系統,然后設計師可基於這些系統進行構建或利用這些系統為某個特定關卡或游戲本身創建自定義Gameplay。
也就是說,程序員用C++開發一些基礎的系統,例如本文當中的武器系統(Weapon),設計師(策划)即可利用該武器系統在藍圖上進行武器的擴充,設計出不同的武器;設計師(策划)也可以利用C++開發的基礎系統,將這些系統在藍圖上進行組裝,以構建更豐富的玩法系統。除此之外,
- 藍圖的可重構性非常非常非常低,如果某個模塊的邏輯比較復雜,就會出現各種線條亂飛,非常的凌亂,過段時間再過來看,可能作者自己都看不明白了。因此我建議,對於比較復雜的邏輯,最好使用C++來實現,如果一定要用藍圖,請將該部分邏輯拆分為幾段小邏輯來實現(充分利用藍圖的函數與宏)。
- 藍圖本身是作為二進制文件來保存(.uasset),在版本管理工具中(Git)無法進行差異性對比,如果對於某段頻繁修改(升級)的邏輯,又想看到每次修改的變化,最好也使用C++來實現,可以對比每個版本的修改內容。
- 對於復雜的數學運算或循環次數較多的邏輯,也最好采用C++來實現,以保證運行效率。