4-3 Weapon Pickup Points
今天要來介紹撿拾地面的武器的機制~玩家經過放置在地上的武器後,會自動更換成玩家手中的武器。首先,在場景中製作一個空白的GameObject。
取名為Weapon Pickup。
拉進Project視窗中變成Prefab。
新增一個Script名為Weapon Pickup Point,並拉進Weapon Pickup的Prefab中。
接著先向大家介紹WeaponPickPoint中比較特別的程式碼,ExecuteInEditMode,在Class名稱之前加上這段程式碼後,Unity便會在編輯模式中自動執行Update內部的程式碼,執行的時機為編輯中有一些變動時,比如用滑鼠點擊不同的物件就會觸發。
再新增一個Audio Source,用來播放撿拾武器的音效。
再來,我們要修改Player.cs內更換武器的機制,原先於Player.cs中已撰寫了一個方法PutWeaponInHand,只要稍加修改後便能拿來利用。
好的,以下提供Player.cs完整程式碼:
完成!讓我們來看看實際效果如何,於編輯模式中如下圖,會顯示該武器的模型在場景中。
執行遊戲看看,玩家手中拿的還是原本的劍。
經過地面上的鋤頭後,玩家手中的武器成功更換為鋤頭了!
取名為Weapon Pickup。
拉進Project視窗中變成Prefab。
新增一個Script名為Weapon Pickup Point,並拉進Weapon Pickup的Prefab中。
接著先向大家介紹WeaponPickPoint中比較特別的程式碼,ExecuteInEditMode,在Class名稱之前加上這段程式碼後,Unity便會在編輯模式中自動執行Update內部的程式碼,執行的時機為編輯中有一些變動時,比如用滑鼠點擊不同的物件就會觸發。
如下圖,我們撰寫產生武器在場景中的程式。
好的!以下提供大家WeaponPickUp.cs的完整程式碼:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using RPG.Character;
namespace RPG.Weapons{
// 在編輯模式執行程式碼
[ExecuteInEditMode]
public class WeaponPickupPoint : MonoBehaviour {
[SerializeField] Weapon weaponConfig;
[SerializeField] AudioClip pickupSFX;
AudioSource audioSource;
void Start () {
audioSource = GetComponent ();
}
void Update () {
// 判斷要在編輯模式下才執行程式
if (!Application.isPlaying) {
DestroyChildren ();
InstantiateWeapon ();
}
}
void DestroyChildren(){
foreach (Transform child in transform) {
DestroyImmediate (child.gameObject);
}
}
// 產生預覽用的武器物件於場景中
void InstantiateWeapon(){
GameObject weapon = weaponConfig.GetWeaponPrefab();
weapon.transform.position = Vector3.zero;
Instantiate (weapon, gameObject.transform);
}
// 觸發Trigger時更換玩家的武器
void OnTriggerEnter(){
FindObjectOfType ().PutWeaponInHand(weaponConfig);
audioSource.PlayOneShot (pickupSFX);
}
}
}
撰寫好後,請將拉入素材進相關參數,Weapon Config要放進一個武器,Pickup SFX要放進撿拾武器的音效。接著,再請大家新增一個Box Collider,勾選Is Trigger,然後請大家調整Box Collider的位置與武器對應,當玩家觸發到Trigger時便會更換玩家手中的武器。
再新增一個Audio Source,用來播放撿拾武器的音效。
再來,我們要修改Player.cs內更換武器的機制,原先於Player.cs中已撰寫了一個方法PutWeaponInHand,只要稍加修改後便能拿來利用。
好的,以下提供Player.cs完整程式碼:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.SceneManagement;
using RPG.CameraUI; // TODO consider re-wiring
using RPG.Core;
using RPG.Weapons;
namespace RPG.Character{
public class Player : MonoBehaviour, IDamageable {
[SerializeField] float maxHealthPoints = 100f;
[SerializeField] float baseDamage = 50f;
[SerializeField] AnimatorOverrideController animatorOverrideController;
[SerializeField] Weapon currentWeaponConfig;
[SerializeField] AbilityConfig[] abilities;
[SerializeField] AudioClip[] damageSounds;
[SerializeField] AudioClip[] deathSounds;
[Range(.1f, 1.0f)] [SerializeField] float criticalHitChance = 0.1f;
[SerializeField] float criticalHitMultiplier = 2.5f;
[SerializeField] ParticleSystem criticalHitParticle;
// 定義Trigger的字串常數
const string DEATH_TRIGGER = "Death";
const string ATTACK_TRIGGER = "Attack";
const string DEFAULT_ATTACK = "DEFAULT ATTACK";
float lastHitTime;
float currentHealthPoint;
Enemy currentEnemy;
CameraRaycaster cameraRaycaster;
AbilityBehaviour[] abilityBehaviour;
AudioSource audioSource;
Animator animator;
GameObject weaponInUse;
public float healthAsPercentage{
get{ return currentHealthPoint / maxHealthPoints; }
}
void Start(){
audioSource = GetComponent ();
RegisterForMouseClcik ();
SetCurrentMaxHealth ();
PutWeaponInHand (currentWeaponConfig);
OverrideAnimatorController ();
AttachSpecialAbility();
}
void Update(){
if (healthAsPercentage > Mathf.Epsilon) {
ScanForAbilityKeyDown ();
}
}
private void ScanForAbilityKeyDown(){
for (int keyIndex = 1; keyIndex <= abilities.Length; keyIndex++) {
if (Input.GetKeyDown (keyIndex.ToString())) {
AttemptSpecialAbility (keyIndex);
}
}
}
private void AttachSpecialAbility(){
abilityBehaviour = new AbilityBehaviour[abilities.Length];
for (int i = 0; i < abilities.Length; i++) {
// 儲存ISpecialAbility物件
abilityBehaviour[i] = abilities [i].AttachAbilityTo (gameObject);
}
}
private void OverrideAnimatorController(){
// 取得Animator物件
animator = GetComponent ();
// 將人物的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.gripTransform.localPosition;
weaponInUse.transform.localRotation = currentWeaponConfig.gripTransform.localRotation;
}
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 void SetCurrentMaxHealth(){
currentHealthPoint = maxHealthPoints;
}
private void RegisterForMouseClcik(){
cameraRaycaster = FindObjectOfType ();
// 註冊滑鼠碰到敵人的事件
cameraRaycaster.onMouseOverEnemy += OnMouseOverEnemy;
}
void OnMouseOverEnemy(Enemy enemy){
currentEnemy = enemy;
if (Input.GetMouseButtonDown (0) && IsTargetInRange (enemy.gameObject)) {
AttackTarget ();
}else if (Input.GetMouseButtonDown (1)) {
// 0為使用第一個技能
AttemptSpecialAbility(0);
}
}
private void AttemptSpecialAbility(int abilityIndex){
Energy energyComponent = GetComponent ();
// 取得技能需要的能量消耗量
float energyCost = abilities [abilityIndex].GetEnergyCost ();
if (energyComponent.IsEnergyAvailable (energyCost)) {
energyComponent.ConsumeEnergy (energyCost);
// 發動技能,並傳入Player的baseDamage
AbilityParams abilityParams = new AbilityParams (currentEnemy, baseDamage);
abilityBehaviour [abilityIndex].Use(abilityParams);
}
}
private void AttackTarget(){
// 確認本次攻擊時間離上一次攻擊時間須大於minTimeBetweenHits,相當於技能冷卻時間
if ( (Time.time - lastHitTime > currentWeaponConfig.GetMinTimeBetweenHits())) {
// 每次攻擊前都重設武器的攻擊動畫
OverrideAnimatorController();
// 呼叫Trigger啟動攻擊動畫
animator.SetTrigger(ATTACK_TRIGGER);
// 呼叫攻擊Target的方法
currentEnemy.TakeDamage(CalculateDamage());
// 紀錄目前攻擊的時間
lastHitTime = Time.time;
}
}
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;
}
}
// 確認敵人是否在範圍技的攻擊範圍內
private bool IsTargetInRange(GameObject target){
float distanceToTarget = (target.transform.position - transform.position).magnitude;
return distanceToTarget <= currentWeaponConfig.GetMaxAttackRange();
}
public void TakeDamage(float damage){
// Math.Clamp可以確保數值運算後不會低於最小值或者高於最大值
currentHealthPoint = Mathf.Clamp (currentHealthPoint - damage, 0f, maxHealthPoints);
// 隨機播放受傷的音效
audioSource.clip = damageSounds [Random.Range (0, damageSounds.Length)];
audioSource.Play ();
bool playerDies = currentHealthPoint <= 0;
if (playerDies) {
StartCoroutine (KillPlayer ());
}
}
public void Heal(float points){
currentHealthPoint = Mathf.Clamp (currentHealthPoint + points, 0f, maxHealthPoints);
}
IEnumerator KillPlayer(){
// 播放死亡音效
audioSource.clip = deathSounds [Random.Range (0, deathSounds.Length)];
audioSource.Play ();
// 播放死亡動畫
animator.SetTrigger(DEATH_TRIGGER);
// 等待一段時間(依音效長度而定)
yield return new WaitForSecondsRealtime(audioSource.clip.length);
// 重載關卡
SceneManager.LoadScene(0);
}
}
}
完成!讓我們來看看實際效果如何,於編輯模式中如下圖,會顯示該武器的模型在場景中。
執行遊戲看看,玩家手中拿的還是原本的劍。
經過地面上的鋤頭後,玩家手中的武器成功更換為鋤頭了!












留言
張貼留言