Related Posts Plugin for WordPress, Blogger...

4-15 Finishing The Weapon System

本章節要在Weapon System上實作遠距離攻擊的武器,當裝備的武器為『弓』的時候,必須可以射箭,當裝備的武器為『劍』的時候,則必須為近距離攻擊。實作概念為於Weapon Config中設置Projectile,如有Projectile存在時會將其發射,如不存在則會直接令目標受傷。由於WeaponConfig中有AttackRange參數,所以不管武器是近戰武器或是遠距離武器,都可以共用AttackRange的設定。

實作前,需要先解決『撿拾武器』的Bug,如下圖於OnTriggerEnter時呼叫了FindObjectOfType來尋找WeaponSystem,這段程式碼於重構WeaponSystem前撰寫的,所以當時並不會出問題。但重構後的程式碼,讓Enemy也可以共用WeaponSystem,所以更換武器時有可能更換Enemy的武器。

請如下圖進行修改,將FindObjectOfType的對象改成PlayerMovement,並再使用GetComponent取得WeaponSystem,即可解決這個Bug。

如對撿拾武器的機制不熟悉,可先閱讀之前的文章:
4-3 Weapon Pickup Points
https://rpgcorecombat.blogspot.tw/2018/01/4-3-weapon-pickup-points.html

接著要修正攻擊的Delay時間,如下圖於AttackTargetOnce方法中,我們直接指定delay時間為0.25秒,正確做法應該要從WeaponConfig中取得。

請如下圖在Weapon中定義damageDelay參數,然後再請大家自行於AttackTargetOnce方法中,從Weapon取得damageDelay。如不會修改,後面會再提供完整程式碼給大家參考。

接著要進入正題啦!首先請大家準備好弓跟弓箭的模型。如果沒有的話也沒關係,用把劍,然後用之前製作Projectile時的球球也可以,就當作揮劍的時候會發射劍氣吧!如不清楚Projectile的部分,可以先參考以下文章:
2-15 Enemy Attack Spheres And Spawning Enemy Projectile
https://rpgcorecombat.blogspot.tw/2017/12/2-15-enemy-attack-spheres-and-spawning.html

2-18 Using The Trail Renderer Component


接著,請在Project視窗點右鍵,選擇Create/RPG/Weapon建立一個新的WeaponConfig唷!

取名為Simple Bow,然後新增一個Empty GameObject製作成Prefab,取名為SimpleBowGripPosition,這個Prefab要用來定位角色握住弓的位置。如果製作到這個步驟不清楚的話,請先閱讀以前有關武器系統的文章:
2-25 Introducing Scriptable Objects

2-26 Problems With Prefabs 

2-27 Import Mechanim Animation Pack 

3-2 The Animator Override Controller 


接著來撰寫程式碼!這次更改到的程式碼有Weapon、WeaponSystem、Projectile、PlayerMovement。
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;

  [Header("Projectile")]
  [SerializeField] GameObject projectilePrefab;
  [SerializeField] Transform projectileSocket;
  [SerializeField] float projectileSpeed;
  [SerializeField] Vector3 aimOffset = new Vector3(0, 1f, 0);

  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 GameObject GetProjectilePrefab(){
   return projectilePrefab;
  }

  public Vector3 GetAimOffset(){
   return aimOffset;
  }

  public Transform GetProjectileSocket(){
   return projectileSocket;
  }

  public float GetProjectileSpeed(){
   return projectileSpeed;
  }

  // 避免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(.1f, 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;

  void Start () {
   animator = GetComponent ();
   character = 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()) {
    float weaponHitPeriod = currentWeaponConfig.GetMinTimeBetweenHits ();
    // 將動畫加速的倍率算進等待時間中
    float timeToWait = weaponHitPeriod * character.GetAnimSpeedMultiplier();

    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());
  }

  public void StopRepeatedlyAttack(){
   StopAllCoroutines ();
  }

  public Weapon GetCurrentWeapon(){
   return currentWeaponConfig;
  }

  private GameObject RequestDominantHand(){
   // 從Children中尋找包含DominantHand的物件
   DominantHand[] dominantHand = GetComponentsInChildren ();
   // 計算取得有DominantHand的物件的數量
   int numberOfDominantHands = dominantHand.Length;
   // 確保該數量不為0
   Assert.AreNotEqual (numberOfDominantHands, 0, "No DominantHand found on player, please add one");
   // 確保該數量不要大於1
   Assert.IsFalse (numberOfDominantHands > 1, "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;
   }
  }
 }
}

Projectile.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using RPG.Core;
using RPG.Character;

namespace RPG.Weapons{
 public class Projectile : MonoBehaviour {

  GameObject shooter;
  float damageCaused;

  public void SetShooter(GameObject shooter){
   this.shooter = shooter;
  }

  public void SetDamage(float damage){
   damageCaused = damage;
  }

  void OnCollisionEnter(Collision collision){
   int layerCollidedWith = collision.gameObject.layer;
   // 若被碰撞的物體所處的Layer跟shooter的Layer不同,才會觸發攻擊機制
   // 如敵人就不會攻擊到敵人
   if (shooter && layerCollidedWith != shooter.layer) {
    DamageIfDamageable (collision);
   }
  }

  private void DamageIfDamageable(Collision collision){
   // 取得HealthSystem
   HealthSystem healthSystem = collision.gameObject.GetComponent();
   if (healthSystem) {
    healthSystem.TakeDamage (damageCaused);
   }
   Destroy(gameObject);
  }
 }
}

PlayerMovement.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using RPG.CameraUI;
using RPG.Core;
using RPG.Weapons;

namespace RPG.Character{
 public class PlayerMovement : MonoBehaviour {

  [SerializeField] float walkMoveStopRadius = 0.2f;

  EnemyAI currentEnemy;
  Character character;
  SpecialAbilities abilities;
  WeaponSystem weaponSystem;

  void Start(){
   abilities = GetComponent ();
   character = GetComponent ();
   weaponSystem = GetComponent ();

   RegisterForMouseClcik ();
  }

  void Update(){
   float healthAsPercentage = GetComponent ().healthAsPercentage;
   if (healthAsPercentage > Mathf.Epsilon) {
    ScanForAbilityKeyDown ();
   }
  }

  private void ScanForAbilityKeyDown(){
   for (int keyIndex = 1; keyIndex <= abilities.GetNumberOfAbilities(); keyIndex++) {
    if (Input.GetKeyDown (keyIndex.ToString())) {
     if (currentEnemy) {
      abilities.AttemptSpecialAbility (keyIndex, currentEnemy.gameObject);
     } else {
      abilities.AttemptSpecialAbility (keyIndex);
     }
    }
   }
  }

  private void RegisterForMouseClcik(){
   CameraRaycaster cameraRaycaster = FindObjectOfType ();
   cameraRaycaster.onMouseOverEnemy += OnMouseOverEnemy;
   cameraRaycaster.onMouseOverWalkable += OnMouseOverWalkable;
  }

  void OnMouseOverEnemy(EnemyAI enemy){
   currentEnemy = enemy;

   if (Input.GetMouseButton (0)) {
    // 普通攻擊
    StartCoroutine (MoveAndAttack (enemy));
   } else if (Input.GetMouseButtonDown (1)) {
    // 技能-強力攻擊
    StartCoroutine (MoveAndPowerAttack (enemy));
   } 
  }

  IEnumerator MoveToTarget(GameObject target){
   // 設置地點為Enemy的位置
   character.SetDestination(target.transform.position);
   character.SetStopDistance(weaponSystem.GetCurrentWeapon().GetMaxAttackRange());
   while (!IsTargetInRange (target)) {
    yield return new WaitForEndOfFrame ();
   }
   yield return new WaitForEndOfFrame ();
  }

  IEnumerator MoveAndAttack(EnemyAI target){
   // 先移動到目標位置
   yield return StartCoroutine (MoveToTarget (target.gameObject));
   // 鎖定Enemy後會自動攻擊 
   weaponSystem.AttackTarget (target.gameObject);
  }

  IEnumerator MoveAndPowerAttack(EnemyAI target){
   // 先移動到目標位置
   yield return StartCoroutine (MoveToTarget (target.gameObject));
   // 0為使用第一個技能 
   abilities.AttemptSpecialAbility(0, currentEnemy.gameObject);
  }

  void OnMouseOverWalkable(Vector3 destination){
   if (Input.GetMouseButton (0)) {
    // 要求停止自動攻擊
    weaponSystem.StopRepeatedlyAttack();
    // 設置地點為滑鼠點擊的位置
    character.SetDestination(destination);
    character.SetStopDistance (walkMoveStopRadius);
   }
  }

  // 確認敵人是否在範圍技的攻擊範圍內
  private bool IsTargetInRange(GameObject target){
   float distanceToTarget = (target.transform.position - transform.position).magnitude;
   return distanceToTarget <= weaponSystem.GetCurrentWeapon().GetMaxAttackRange();
  }

  void OnDrawGizmos(){
   if (Application.isPlaying) {
    //繪製攻擊範圍
    Gizmos.color = new Color (255f, 0f, 0f, 0.5f);
    Gizmos.DrawWireSphere (transform.position, weaponSystem.GetCurrentWeapon ().GetMaxAttackRange ());
   }
  }
 }
}


撰寫好程式碼後,請在要發射的3D物件,比方我的範例用的Simple Arrow中,加入RigidBody。

在Collision Detection中選擇Continuous Dynamic。這一步不清楚的人請先看先前的文章:
2-20 Preventing Projectile Pass-Through
https://rpgcorecombat.blogspot.tw/2017/12/2-20-preventing-projectile-pass-through.html


Simple Arrow再新增Box Collider,以及Projectile.cs。


接著在Player底下新增一個Empty GameObject,名為ProjectileSocket,這是用來定位Projectile發射位置的座標。


請如下圖自己在場景中調整發射位置。

 調整完以後拉入Project視窗變成Prefab,並從場景中刪除。

上述步驟做完以後,請大家回到Simple Bow的WeaponConfig中,如下圖依序填寫好所有參數,此時應該可以正確填入所有參數。

此時執行遊戲看看,如果有人發現弓的位置錯了,如下圖。

請將遊戲暫停,調整好弓的位置。
 
然後使用Copy Component的方式將資訊記錄下來。

回到SimpleBowGripPosition使用Paste Component Values即可。

接著再執行遊戲試看看,玩家射箭。

箭慢慢地飛向敵人。

啊!射中了!敵人倒下。


留言