5-23 Throw Out Weapon Pickup Point When Enemy Died
今天要來製作敵人死亡後會掉落武器的機制,玩家或者敵人經過武器以後,都可以將武器撿起來使用。首先,掉落在地圖上的武器的設計,在以前的文章已經有介紹過了,如有人不清楚請先參考這篇文章:
4-3 Weapon Pickup Points
再來是Weapon.cs需要增加一個屬性,pickupPointPrefab,由於我的遊戲設定是敵人死後,會噴出他手中的武器,所以Weapon Pickup Points的Prefab物件便放在Weapon設定中吧。敵人藉由Weapon.cs決定他手中的武器是什麼,亦藉由Weapon.cs決定他噴出的武器是什麼。
然後,在WeaponSystem.cs增加方法ThrowOutWeaponPickupPoint,從Weapon設定中取得Prefab物件,並用Instantiate產生出來。然後,使用Rigidbody的AddForce方法,讓武器向上然後偏左一點噴出來。偏左一點,是為了避免武器的Collider碰到死掉的敵人,保險一點的話當然是加個檢查最好。
將新設定的WeaponPickupPoint拖到地圖上看看效果,會看見武器在地上旋轉,然後一直有粒子噴射。
因為我是複製之前弓箭的WeaponPickupPoint,所以圖示上還是弓箭。
記得按下Apply。
圖案就會變回矛的樣子了。
如此一來,只要有裝配Lancer Spear這個WeaponConfig的敵人,都會在死後掉落Spear這個武器。為什麼掉落的武器要跟WeaponConfig綁在一起,而非跟敵人綁在一起呢?因為敵人也有可能在中途更換武器(意即更換WeaponConfig),若敵人換了一把劍,但是卻掉落一把矛,實在不合理。所以,將PickupPoint與WeaponConfig綁在一起才能符合遊戲需求。
殺死拿著茅的敵人後。
敵人的身上噴出了一把矛。
玩家經過矛以後,就會更換武器了。大家可能會覺得目前有以下問題:
1. 拿著矛的姿勢怪怪的。
2. 玩家獲得武器以後,應該要將新武器放進Inventory System。
上述問題會再往後的文章跟大家介紹改進方法。
4-3 Weapon Pickup Points
先跟大家解釋一下本次的Weapon Pickup Points有什麼更動,原本的設計使用Trigger,而新的設計要改用普通的Collider,以及RigidBody。這是因為我希望敵人死後,武器會從身體『噴出來』,亦即需要受重力影響,且武器會停留在地面。
若仍用Trigger設計,武器受重力影響後會穿越地面一直往下墜落,所以新的Weapon Pickup Points的Rigidbody設定如下。
接著再來撰寫新的程式碼,首先修改WeaponPickupPoint.cs,增加OnCollisionEnter的方法偵測碰撞,並確認碰撞體擁有WeaponSystem的Component。因為敵人跟玩家都共用WeaponSystem,如此便能替換敵人或者玩家手中的武器了。
再來是Weapon.cs需要增加一個屬性,pickupPointPrefab,由於我的遊戲設定是敵人死後,會噴出他手中的武器,所以Weapon Pickup Points的Prefab物件便放在Weapon設定中吧。敵人藉由Weapon.cs決定他手中的武器是什麼,亦藉由Weapon.cs決定他噴出的武器是什麼。
然後,在WeaponSystem.cs增加方法ThrowOutWeaponPickupPoint,從Weapon設定中取得Prefab物件,並用Instantiate產生出來。然後,使用Rigidbody的AddForce方法,讓武器向上然後偏左一點噴出來。偏左一點,是為了避免武器的Collider碰到死掉的敵人,保險一點的話當然是加個檢查最好。
以下提供本次修改後的完整程式碼。
WeaponPickupPoint.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using RPG.Character;
using RPG.Core;
namespace RPG.Weapons{
// 在編輯模式執行程式碼
[ExecuteInEditMode]
public class WeaponPickupPoint : MonoBehaviour {
[SerializeField] Weapon weaponConfig;
[SerializeField] AudioClip pickupSFX;
[Header("Rotation")]
[SerializeField] float xRotationsPerMinute = 1f;
[SerializeField] float yRotationsPerMinute = 1f;
[SerializeField] float zRotationsPerMinute = 1f;
[Header("Box Collider")]
[SerializeField] Vector3 colliderCenter = new Vector3(0, 0.7f, 0);
[SerializeField] Vector3 colliderSize = new Vector3(1, 1, 1);
AudioSource audioSource;
BoxCollider myCollider;
void Start () {
if (Application.isPlaying) {
audioSource = gameObject.AddComponent ();
myCollider = gameObject.AddComponent ();
myCollider.center = colliderCenter;
myCollider.size = colliderSize;
}
}
void Update () {
// 判斷要在編輯模式下才執行程式
if (!Application.isPlaying) {
DestroyChildren ();
InstantiateWeapon ();
}
RotateTransform ();
}
void DestroyChildren(){
Transform simulation = transform.GetChild (0);
foreach (Transform child in simulation.transform) {
DestroyImmediate (child.gameObject);
}
}
// 產生預覽用的武器物件於場景中
void InstantiateWeapon(){
Transform simulation = transform.GetChild (0);
GameObject weapon = weaponConfig.GetWeaponPrefab();
weapon.transform.position = Vector3.zero;
Instantiate (weapon, simulation);
}
// 觸發時更換武器
void OnCollisionEnter(Collision collision){
WeaponSystem weaponSystem = collision.gameObject.GetComponent ();
if (weaponSystem) {
weaponSystem.PutWeaponInHand (weaponConfig);
audioSource.PlayOneShot (pickupSFX);
Destroy (gameObject);
}
}
void RotateTransform(){
Transform simulation = transform.GetChild (0);
//xRotationsPerMinute為1代表轉1圈,1圈有360度,將圈數乘以360度後再除以60秒,便得出1秒轉幾度
//Time.deltaTime為Update每次執行Frame經過了幾秒的基本時間單位,故將此單位乘以1秒所需旋轉的度數
//故能得出每個Frame要旋轉幾度
float xDegreesPerFrame = (xRotationsPerMinute * 360) / 60 * Time.deltaTime;
simulation.RotateAround (simulation.position, simulation.right, xDegreesPerFrame);
float yDegreesPerFrame = (yRotationsPerMinute * 360) / 60 * Time.deltaTime;
simulation.RotateAround (simulation.position, simulation.up, yDegreesPerFrame);
float zDegreesPerFrame = (zRotationsPerMinute * 360) / 60 * Time.deltaTime;
simulation.RotateAround (simulation.position, simulation.forward, zDegreesPerFrame);
}
}
}
Weapon.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace RPG.Weapons{
[CreateAssetMenu(menuName="RPG/Weapon")]
public class Weapon : ScriptableObject {
[Header("Weapon")]
[SerializeField] GameObject weaponPrefab;
[SerializeField] Transform gripTransform;
[SerializeField] AnimationClip attackAnimation;
[SerializeField] float minTimeBetweenHits = 1f;
[SerializeField] float maxAttackRange = 5f;
[SerializeField] float additionalDamage = 10f;
[SerializeField] float damageDelay = 0.25f;
[SerializeField] bool rightHand = true;
[Header("Audio")]
[SerializeField] AudioClip weaponAudio;
[Header("Projectile")]
[SerializeField] GameObject projectilePrefab;
[SerializeField] Transform projectileSocket;
[SerializeField] float projectileSpeed;
[SerializeField] Vector3 aimOffset = new Vector3(0, 1f, 0);
[Header("Weapon Pickup Point")]
[SerializeField] GameObject pickupPointPrefab;
public float GetMinTimeBetweenHits(){
return minTimeBetweenHits;
}
public float GetMaxAttackRange(){
return maxAttackRange;
}
public Transform GetGripTransform(){
return gripTransform;
}
public GameObject GetWeaponPrefab(){
return weaponPrefab;
}
public AnimationClip GetAnimClip(){
RemoveAnimationEvent ();
return attackAnimation;
}
public float GetAdditionalDamage(){
return additionalDamage;
}
public float GetDamageDelay(){
return damageDelay;
}
public bool IsRightHandWeapon(){
return rightHand;
}
public AudioClip GetWeaponAudio(){
return weaponAudio;
}
public GameObject GetProjectilePrefab(){
return projectilePrefab;
}
public Vector3 GetAimOffset(){
return aimOffset;
}
public Transform GetProjectileSocket(){
return projectileSocket;
}
public float GetProjectileSpeed(){
return projectileSpeed;
}
public GameObject GetPickupPointPrefab(){
return pickupPointPrefab;
}
// 避免Asset Pack造成我們的遊戲Crash
private void RemoveAnimationEvent(){
attackAnimation.events = new AnimationEvent[0];
}
}
}
WeaponSystem.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Assertions;
using RPG.Weapons;
namespace RPG.Character{
public class WeaponSystem : MonoBehaviour {
[SerializeField] float baseDamage = 50f;
[SerializeField] Weapon currentWeaponConfig;
[Range(0.0f, 1.0f)] [SerializeField] float criticalHitChance = 0.1f;
[SerializeField] float criticalHitMultiplier = 2.5f;
[SerializeField] ParticleSystem criticalHitParticle;
// 定義Trigger的字串常數
const string ATTACK_TRIGGER = "Attack";
const string DEFAULT_ATTACK = "DEFAULT ATTACK";
float lastHitTime;
GameObject weaponInUse;
GameObject target;
Character character;
Animator animator;
AudioSource audioSource;
void Start () {
animator = GetComponent ();
character = GetComponent ();
audioSource = GetComponent ();
PutWeaponInHand (currentWeaponConfig);
OverrideAnimatorController ();
}
private void OverrideAnimatorController(){
if (!character.GetOverrideController ()) {
Debug.Break ();
Debug.LogAssertion ("Please provide " + gameObject + "with an animator override controller.");
} else {
AnimatorOverrideController animatorOverrideController =
character.GetOverrideController ();
// 將人物的Animator取代為我們剛剛新增的Player Animator Override
animator.runtimeAnimatorController = animatorOverrideController;
// Player Animator Override中有一預設的DEFAULT ATTACK,將其取代成Weapon物件中的Animation
// 亦即人物的Animation將會依據不同的Weapon物件,而使用有不同的動畫
animatorOverrideController [DEFAULT_ATTACK] = currentWeaponConfig.GetAnimClip ();
}
}
public void PutWeaponInHand(Weapon weaponToUse){
currentWeaponConfig = weaponToUse;
// 先移除之前手持的武器
Destroy(weaponInUse);
GameObject weaponPrefab = weaponToUse.GetWeaponPrefab ();
// 生成武器時,將會放置到指定的GameObject之下,此處為WeaponSocket之下
// 故可將WeaponSocket設置成玩家的右手或左手
GameObject weaponSocket = RequestDominantHand();
weaponInUse = Instantiate (weaponPrefab, weaponSocket.transform);
// 將武器放置到正確的位置,並有正確的方向
weaponInUse.transform.localPosition = currentWeaponConfig.GetGripTransform().localPosition;
weaponInUse.transform.localRotation = currentWeaponConfig.GetGripTransform().localRotation;
}
public void AttackTarget(GameObject targetToAttack){
target = targetToAttack;
StartCoroutine (AttackTargetRepeatedly ());
}
IEnumerator AttackTargetRepeatedly(){
// 確認攻擊者跟目標都活著
while (AttackerAndTargetStillAlive()) {
AnimationClip animationClip = currentWeaponConfig.GetAnimClip ();
// 將攻擊動畫時長除以加速的倍率,取得正確的時長
float animationClipTime = animationClip.length / character.GetAnimSpeedMultiplier ();
// 間隔時間為攻擊動畫時長+武器Config設定的攻擊間隔
float timeToWait = animationClipTime +
currentWeaponConfig.GetMinTimeBetweenHits();
bool isTimeToHitAgain = Time.time - lastHitTime > timeToWait;
if (isTimeToHitAgain) {
AttackTargetOnce ();
lastHitTime = Time.time;
}
yield return new WaitForSeconds (timeToWait);
}
}
private bool AttackerAndTargetStillAlive(){
bool attackerStillAlive = GetComponent ().healthAsPercentage >= Mathf.Epsilon;
bool targetStillAlive = target.GetComponent ().healthAsPercentage >= Mathf.Epsilon;
return attackerStillAlive && targetStillAlive;
}
void AttackTargetOnce(){
// 攻擊時要面向目標
transform.LookAt(target.transform);
// 每次攻擊前都重設武器的攻擊動畫
OverrideAnimatorController();
animator.SetTrigger(ATTACK_TRIGGER);
// 判斷武器是否有發射物
GameObject projectile = currentWeaponConfig.GetProjectilePrefab();
if (projectile) {
StartCoroutine (SpawnProjectile(target));
} else {
float delay = currentWeaponConfig.GetDamageDelay();
StartCoroutine(DamageAfterDelay(delay));
}
}
IEnumerator SpawnProjectile(GameObject target){
yield return new WaitForSeconds(currentWeaponConfig.GetDamageDelay());
GameObject projectileToUse = currentWeaponConfig.GetProjectilePrefab();
Vector3 aimOffset = currentWeaponConfig.GetAimOffset ();
Vector3 projectilePosition = CalculateProjectilePosition ();
// Quaternion.identity為方向不進行任何轉向
GameObject newProjectile = Instantiate(projectileToUse, projectilePosition, Quaternion.identity);
Projectile projectileComponent = newProjectile.GetComponent ();
projectileComponent.SetDamage(CalculateDamage());
// 設定Projectile的Shooter為自己
projectileComponent.SetShooter (gameObject);
// 計算發射Projectile到target之間的單位向量
Vector3 unitVectorToPlayer = (target.transform.position + aimOffset - projectilePosition).normalized;
float projectileSpeed = currentWeaponConfig.GetProjectileSpeed();
// 將單位向量乘以發射速度,透過velocity將Projectile發射出去吧!
newProjectile.GetComponent ().velocity = unitVectorToPlayer * projectileSpeed;
}
private Vector3 CalculateProjectilePosition(){
Transform projectileSocket = currentWeaponConfig.GetProjectileSocket ();
Vector3 aimOffset = new Vector3(0, 1, 0);
return projectileSocket.position + aimOffset + transform.position;
}
IEnumerator DamageAfterDelay(float delay){
yield return new WaitForSecondsRealtime (delay);
// 呼叫攻擊Target的方法
target.GetComponent().TakeDamage(CalculateDamage());
// 播放攻擊音效
audioSource.PlayOneShot(currentWeaponConfig.GetWeaponAudio());
}
public void StopRepeatedlyAttack(){
StopAllCoroutines ();
}
public Weapon GetCurrentWeapon(){
return currentWeaponConfig;
}
public void ThrowOutWeaponPickupPoint(){
GameObject pickupPointPrefab = currentWeaponConfig.GetPickupPointPrefab();
GameObject weaponPickupPoint = Instantiate (pickupPointPrefab);
weaponPickupPoint.transform.position = transform.position + Vector3.left * 1;
weaponPickupPoint.GetComponent ()
.AddForce (Vector3.left * 30 + Vector3.up * 300);
}
private GameObject RequestDominantHand(){
// 從Children中尋找包含DominantHand的物件
Component[] dominantHand;
if (currentWeaponConfig.IsRightHandWeapon ()) {
dominantHand = GetComponentsInChildren ();
} else {
dominantHand = GetComponentsInChildren ();
}
// 計算取得有DominantHand的物件的數量
int numberOfDominantHands = dominantHand.Length;
// 確保該數量不為0
Assert.AreNotEqual (numberOfDominantHands, 0, "No DominantHand found on player, please add one");
// 確保該數量不要大於2
Assert.IsFalse (numberOfDominantHands > 2, "Multiple Dominant script on Player, please remove one");
// 傳回屬於該DominantHand的GameObject
return dominantHand [0].gameObject;
}
private float CalculateDamage(){
// 產生隨機值,比較該值是否小於致命一擊的機率
bool isCriticalHit = Random.Range (0f, 1f) <= criticalHitChance;
float damageBeforeCritical = baseDamage + currentWeaponConfig.GetAdditionalDamage ();
if (isCriticalHit) {
// 播放致命一擊的特效
criticalHitParticle.Play ();
// 回傳攻擊力乘上致命一擊的乘率
return damageBeforeCritical * criticalHitMultiplier;
} else {
return damageBeforeCritical;
}
}
}
}
接下來,我們來設定新版的WeaponPickupPoint吧,
將新設定的WeaponPickupPoint拖到地圖上看看效果,會看見武器在地上旋轉,然後一直有粒子噴射。
因為我是複製之前弓箭的WeaponPickupPoint,所以圖示上還是弓箭。
記得按下Apply。
圖案就會變回矛的樣子了。
接著,將SpearPickup拖曳到Lancer Spear的Weapon Config中。
如此一來,只要有裝配Lancer Spear這個WeaponConfig的敵人,都會在死後掉落Spear這個武器。為什麼掉落的武器要跟WeaponConfig綁在一起,而非跟敵人綁在一起呢?因為敵人也有可能在中途更換武器(意即更換WeaponConfig),若敵人換了一把劍,但是卻掉落一把矛,實在不合理。所以,將PickupPoint與WeaponConfig綁在一起才能符合遊戲需求。
殺死拿著茅的敵人後。
敵人的身上噴出了一把矛。
玩家經過矛以後,就會更換武器了。大家可能會覺得目前有以下問題:
1. 拿著矛的姿勢怪怪的。
2. 玩家獲得武器以後,應該要將新武器放進Inventory System。
上述問題會再往後的文章跟大家介紹改進方法。














留言
張貼留言