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
接著來撰寫程式碼!這次更改到的程式碼有Weapon、WeaponSystem、Projectile、PlayerMovement。
Weapon.cs
WeaponSystem.cs
Projectile.cs
PlayerMovement.cs
撰寫好程式碼後,請在要發射的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。

調整完以後拉入Project視窗變成Prefab,並從場景中刪除。
上述步驟做完以後,請大家回到Simple Bow的WeaponConfig中,如下圖依序填寫好所有參數,此時應該可以正確填入所有參數。
此時執行遊戲看看,如果有人發現弓的位置錯了,如下圖。
請將遊戲暫停,調整好弓的位置。
然後使用Copy Component的方式將資訊記錄下來。
接著再執行遊戲試看看,玩家射箭。
箭慢慢地飛向敵人。
啊!射中了!敵人倒下。
實作前,需要先解決『撿拾武器』的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 ());
}
}
}
}
在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即可。
接著再執行遊戲試看看,玩家射箭。
箭慢慢地飛向敵人。
啊!射中了!敵人倒下。




















留言
張貼留言