Related Posts Plugin for WordPress, Blogger...

5-25 Using Weapon Grip Position Based On Individual

本章要來修改武器設定中的Grip Tranform功能,大家還記得在以前的章節中,我們是將Grip Tranform綁定在Weapon的Scriptable Object上。但因為目前遊戲的武器系統要朝向讓每個敵人都可以撿起不同的武器,同理玩家也可以使用所有敵人手中的武器。此時,便產生原本給龐大敵人使用的武器,拿到玩家手中也變得非常龐大。

握住武器的方向,位置也都與玩家角色有偏差。這是因為該Grip Tranform是以龐大敵人角色來設計的,而非以玩家角色來設計。本章就要將Grip Tranform改到每個角色的Weapon System上,如此不同的武器給不同人使用時,便能套用正確的Grip Tranform。

如有人對Grip Position的設計不了解,可先參考以前的文章唷!
2-25 Introducing Scriptable Objects
https://rpgcorecombat.blogspot.tw/2017/12/2-25-introducing-scriptable-objects.html

首先, 如下圖為原本的Weapon Config中,其Grip Tranform設定是給Berserker使用的,玩家套用上去當然會產生上述的滑稽現象。

第一步,我們必須確保武器套用角色的Grip Transform時,武器的方向跟大小都必須正確,所以3D模型必須要統一規格。如下圖這兩把劍的Rotation雖設為0但方向卻不一樣,由於我有很多素材都是從Asset Store下載的,這種問題特別多。

所以我想到一個快速的解決方法,不需要使用3D建模工具調整模型。首先我們在場景中建立一個Empty GameObject。

然後把原本的物件Longsword放進去,並將原本的Empty GameObject也取名為Long Sword。

將Long Sword的Position,Rotation都設為0,Scale設為1。

然後大家會發現劍的位置並不對,這時就要請大家調整Longsword,也就是劍的主體。

將Longsword調整到可以與另外一把劍重合,藉此來統一規格。

接著,原本的Longsword的Prefab物件就將他刪掉吧。

大家會發現原本場景中的Longsword變成紅色字,這是因為Prefab被刪掉了,不用擔心。

請將Long Sword拉進Project視窗建立新的Prefab吧。

因為舊的Prefab被刪掉了,所以在Weapon Config設定中會出現Missing的提示。

將新的Long Sword拉進去吧。

然後我們修改一下WeaponSystem的程式碼吧,新增Grip Transform屬性,並修改PutWeaponInHand方法中指定Grip Transform的部分。



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;
  [SerializeField] Transform gripTransform;
  [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 = gripTransform.localPosition;
   weaponInUse.transform.localRotation = gripTransform.localRotation;
   weaponInUse.transform.localScale = gripTransform.localScale;
  }

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

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] int ID;
  [SerializeField] GameObject weaponPrefab;
  [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 int GetID(){
   return ID;
  }

  public float GetMinTimeBetweenHits(){
   return minTimeBetweenHits;
  }

  public float GetMaxAttackRange(){
   return maxAttackRange;   
  }

  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];
  }
 }
}


然後在新的Weapon System上指定Grip Transform唷。

製作Grip Transform時,記得要以外層的物件為主唷。

在遊戲模式中調整好角度後,使用Copy Component複製資訊。

然後在PlayerGripPosition中使用Paste Component Values。

 執行遊戲看看吧!嗯,劍的位置很好,去撿其他的武器看看。

另外一把劍的位置也很OK,這代表以後玩家撿到任何一把劍都可以放到正確的位置上了。同理,敵人的設定也跟玩家一樣唷!

在撿起一把槍,嗯,槍的位置好像怪怪的。

 由於槍的長度比較長,使用者握住槍的位置應是在中間,而非底端。調整的時候,記得自己想像劍的握把跟握槍的位置的差別進行調整。

調整完成!


留言