14.武器変更を実装する
武器の変更システムに思ったよりも時間を食いましたですよ。強化のシステムはいったん延期して、最後に優先度を再検討しませふ。
クラスの構成について
武器変更はPlayerShootingクラスに実装していきます。本当は別クラスに武器の特性等を実装していくべきと思うのですが、やってみたらどの変数をどこに置いてどこから呼び出して……の切り分けに混乱しまったく実装が進まなかったので諦めました。問題意識としては持っているが能力が追い付かない、ってかんじでふ。かなC。
どんな武器を実装するか
今回は「どんな敵を実装するか」から考え、その敵への対抗手段として武器種を用意する、というアプローチをとります。例えば、「バリアを張っており一撃では倒せない敵」に対して、「バリアを貫通し一撃で倒すことのできる武器」を用意するといった具合に。
また、各武器にはデメリットを必ず用意します。ほかの武器の長所を裏返したデメリットを付与することで使えない武器(死に武器)が存在しにくくなります。
先の貫通武器であれば「ただし連射が効かない」といったように。
今回は以下の三つの武器(敵)を用意することにしました。
* サブマシンガン:連射が効き、攻撃力も並み(断続的に登場するオーソドックスな敵)
* ショットガン:広範囲の敵を一度に攻撃できるが、弾一発の攻撃力が低い(一斉に大量登場するが、HPが低い敵)
* スナイパーライフル:貫通し、攻撃力も高いが連射が効かない(盾orバリアを持っておりかつ、ダメージを食らわないでいると盾の耐久が回復する敵)
概要
武器切り替え機能の追加
武器ごとのステータスを設定:ShootメソッドとReloadメソッド
武器切り替え機能
三種類の武器を、キー1,2,3を押すことで切り替えられるようにします。また、切り替えには既定の時間がかかり即時の切り替えはできなくします。
- enumで宣言した値を、playerWeapon変数に現在の装備として代入
- Shootメソッド、Reloadメソッドを調整して、ダメージ値やリロード弾数の部分は引数を受けるように修正:引数の詳細は後ほど解説
- playerWeapon変数を見て、Shootメソッドに渡す変数を切り替える
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
enum Weapon { SubmachineGun, ShotGun, SniperRifle, Molotov } Weapon playerWeapon; void Awake() { gunHUDIcon = GameObject.Find("WeaponImg").GetComponent<Image>(); gunHUDIcon.sprite = smgHUDIcon; //残弾数UIのオブジェクトとコンポーネントを取得 ammoFullObj = GameObject.Find("AmmoFullImg"); ammoFullImg = ammoFullObj.GetComponent<Image>(); smgAmmoPrt.SetActive(true); //初期装備はSMG playerWeapon = Weapon.SubmachineGun; currentAmmo = smgMaxAmmo; timeBetweenBullets = smgTimeBetweenBullets; } void Update() { //射撃メソッド実行直後にリセット timer += Time.deltaTime; //射撃メソッドの実行判定 if (Input.GetButton("Fire1") && timer >= timeBetweenBullets && Time.timeScale != 0 && isReloading == false && isChangingWeapon == false) { //残弾数のチェック if (currentAmmo <= 0) { return; } else { //装備してる武器によってSwitchして、Shootメソッドに渡す引数を変える switch (playerWeapon) { case Weapon.SubmachineGun: Shoot(false, false, smgNumBullet, smgFireClip, smgRange, smgDamagePerShot, smgMaxNbPower, smgMaxAmmo); break; case Weapon.ShotGun: Shoot(false, true, sgNumBullet, sgFireClip, sgRange, sgDamagePerShot, sgMaxNbPower, sgMaxAmmo); break; case Weapon.SniperRifle: Shoot(true, false, srNumBullet, srFireClip, srRange, srDamagePerShot, srMaxNbPower, srMaxAmmo); break; } timer = 0f; } } if (timer >= smgTimeBetweenBullets * effectsDisplayTime) { DisableEffects(); } //リロード if (Input.GetKeyDown(KeyCode.R) && isReloading == false && isChangingWeapon == false) { isReloading = true; //リロードSEへ差し替え&再生 switch (playerWeapon) { case Weapon.SubmachineGun: gunAudio.clip = smgReloadClip; gunAudio.Play(); StartCoroutine(DelayReload(smgReloadTime)); break; case Weapon.ShotGun: gunAudio.clip = sgReloadClip; gunAudio.Play(); StartCoroutine(DelayReload(sgReloadTime)); break; case Weapon.SniperRifle: gunAudio.clip = srReloadClip; gunAudio.Play(); StartCoroutine(DelayReload(srReloadTime)); break; } } //武器チェン if (Input.GetKeyDown(KeyCode.Alpha1) && isReloading == false && isChangingWeapon == false) { isChangingWeapon = true; StartCoroutine(DelayChangeWeapon(changeWeaponTime,1)); } else if (Input.GetKeyDown(KeyCode.Alpha2) && isReloading == false && isChangingWeapon == false) { isChangingWeapon = true; StartCoroutine(DelayChangeWeapon(changeWeaponTime, 2)); } else if (Input.GetKeyDown(KeyCode.Alpha3) && isReloading == false && isChangingWeapon == false) { isChangingWeapon = true; StartCoroutine(DelayChangeWeapon(changeWeaponTime, 3)); } } |
ChangeWeaponメソッドの追加
Invokeでは引数を渡すことができないのでcoroutineを使うことにしました。
HUDのアイコンを変えたり、残弾数表示の区切りを変えたり、発射レートの値を変えたりしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
IEnumerator DelayChangeWeapon(float changeTime,int weapon) { yield return new WaitForSeconds(changeTime); ChangeWeapon(weapon); } void ChangeWeapon(int caseWeapon) { switch (caseWeapon) { case 1: playerWeapon = Weapon.SubmachineGun; gunHUDIcon.sprite = smgHUDIcon; currentAmmo = smgMaxAmmo; ammoFullImg.fillAmount = 1.0f; smgAmmoPrt.SetActive(true); sgAmmoPrt.SetActive(false); srAmmoPrt.SetActive(false); timeBetweenBullets = smgTimeBetweenBullets; break; case 2: playerWeapon = Weapon.ShotGun; gunHUDIcon.sprite = sgHUDIcon; currentAmmo = sgMaxAmmo; ammoFullImg.fillAmount = 1.0f; sgAmmoPrt.SetActive(true); smgAmmoPrt.SetActive(false); srAmmoPrt.SetActive(false); timeBetweenBullets = sgTimeBetweenBullets; break; case 3: playerWeapon = Weapon.SniperRifle; gunHUDIcon.sprite = srHUDIcon; currentAmmo = srMaxAmmo; ammoFullImg.fillAmount = 1.0f; srAmmoPrt.SetActive(true); smgAmmoPrt.SetActive(false); sgAmmoPrt.SetActive(false); timeBetweenBullets = srTimeBetweenBullets; break; } isChangingWeapon = false; } |
Shootメソッドの改修
めっちゃ長いですが、ショットガンの場合(isDiffuse == true)とスナイパーライフルの場合(isPierce == true)の場合分けを行い、それぞれ処理を追加してます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 |
void Awake() { shootableMask = LayerMask.GetMask("Shootable"); gunParticles = GetComponent<ParticleSystem>(); gunLine = GetComponentsInChildren<LineRenderer>(); (省略) } public void Shoot ( bool isPierce, bool isDiffuse, int numBullet, AudioClip fireClip, float range, int damagePerShot, int maxNbPower, int maxAmmo ) { //ワンクリックで同時発射するRayの本数を指定 numRay = numBullet; shootRay = new Ray[numRay]; //射撃SEへ差し替え&再生 gunAudio.clip = fireClip; gunAudio.Play(); //マズルフラッシュの表示 gunLight.enabled = true; //射撃エフェクト 直前に出てたものの中断と新たに表示 gunParticles.Stop(); gunParticles.Play(); //Rayの始点(マズル固定)と方向(一定範囲内でランダム)をnumRayの数だけ設定。 //弾道を表す for (int i = 0; i < numRay; i++) { shootRay[i].origin = transform.position; float x; float z; //SG用の処理 if (isDiffuse) { //弾の拡散範囲 float shootAngle = Random.Range(45.0f, 135.0f); x = Mathf.Cos(shootAngle); //z = Mathf.Sin(shootAngle); } else { x = 0f; //z = 0f; } shootRay[i].direction = transform.forward + new Vector3(x, 0, 0); //射線の表示と始点位置指定 gunLine[i].enabled = true; gunLine[i].SetPosition(0, transform.position); } for (int i = 0; i < numRay; i++) { //SRの処理 if (isPierce) { RaycastHit[] hits = Physics.RaycastAll(shootRay[i]); foreach (var obj in hits) { //Rayがぶつかった対象にEnemyHealthコンポーネントがついていたら(=エネミーだったら) EnemyHealth enemyHealth = obj.collider.GetComponent<EnemyHealth>(); if (enemyHealth != null) { enemyHealth.TakeDamage(damagePerShot, obj.point); //ノックバック角度の振れ幅 float x = Random.Range(-0.5f, 0f); float z = Random.Range(-0.5f, 0f); //ノックバック距離の振れ幅 int nbPower = Random.Range(1, maxNbPower); //角度の振れ幅を加算 //距離の振れ幅を乗算して代入 obj.collider.GetComponent<UnityEngine.AI.NavMeshAgent>().velocity = (shootRay[i].direction.normalized + new Vector3(x, 0, z)) * nbPower; } } //射線の終点を射程とイコールに gunLine[i].SetPosition(1, shootRay[i].origin + shootRay[i].direction * range); } else { if (Physics.Raycast(shootRay[i], out shootHit, range, shootableMask)) { //Rayがぶつかった対象にEnemyHealthコンポーネントがついていたら(=エネミーだったら) EnemyHealth enemyHealth = shootHit.collider.GetComponent<EnemyHealth>(); if (enemyHealth != null) { //エネミーの体力減らす&&被弾エフェクト表示処理 enemyHealth.TakeDamage(damagePerShot, shootHit.point); //ノックバックの実装 //NavmeshAgentが付与されているオブジェクトにAddForceしても効かない //https://docs.unity3d.com/Manual/nav-MixingComponents.html //Move the player agent using NavMeshAgent.velocity, so that other agents can predict the player movement to avoid the player. //ノックバック角度の振れ幅 float x = Random.Range(-0.5f, 0f); float z = Random.Range(-0.5f, 0f); //ノックバック距離の振れ幅 int nbPower = Random.Range(1, maxNbPower); //角度の振れ幅を加算 //距離の振れ幅を乗算して代入 shootHit.collider.GetComponent<UnityEngine.AI.NavMeshAgent>().velocity = (shootRay[i].direction.normalized + new Vector3(x, 0, z)) * nbPower; } //衝突地点を終点に gunLine[i].SetPosition(1, shootHit.point); } else { //rayが何にもぶつからなかった時の射線終点指定 gunLine[i].SetPosition(1, shootRay[i].origin + shootRay[i].direction * range); } } } //残弾数を減らす currentAmmo--; //残弾数UIを減らす //1でなく1.0fで書かないとちゃんと割り算の結果が小数で返らないっぽい ammoFullImg.fillAmount -= 1.0f / maxAmmo; } |
拡散能力を付与する
Rayを配列化して、ランダムな前方に複数射出するようにしています。この時、射線の数だけLineRendererを付与したオブジェクトが必要となるため、gunLine変数も同様に配列として宣言し、GunBarrelEndオブジェクトにぶら下げた射線用のオブジェクトからLineRendererを取得しています。
貫通能力を付与する
Physics.Raycastはぶつかった最初のオブジェクトの情報しか取得しないため、スナイパーライフル装備時はPhysics.RaycastAllを使って、Rayがぶつかったすべてのオブジェクトの情報を取得しています。
この時、Enemy以外にもぶつかっているので、EnemyHealthを取得できずTakeDamageを実行しようとしてもNullエラーが出ることがありました。めんどくさがらずEnemyHealthがNullだった場合の分岐を設けなければいけませんでした。
Reloadメソッドの改修
Changeメソッドと同様の理由でcoroutineを使うことにしました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
IEnumerator DelayReload(float reloadTime) { yield return new WaitForSeconds(reloadTime); Reload(playerWeapon); } //coroutineから実行 void Reload(Weapon caseWeapon) { switch (caseWeapon) { case Weapon.SubmachineGun: currentAmmo = smgMaxAmmo; break; case Weapon.ShotGun: currentAmmo = sgMaxAmmo; break; case Weapon.SniperRifle: currentAmmo = srMaxAmmo; break; } //残弾数UIを戻す ammoFullImg.fillAmount = 1.0f; isReloading = false; } |
クラス設計について
冒頭にも書きましたが勉強不足を実感してます。あまりにもメンテ性が悪すぎる……。この記事を書くためにコードを読み返すのが苦痛すぎました。雰囲気でここまでやってきましたが、体系的な勉強を始める必要がありそうです。
次回はコチラ
Unity公式チュートリアルSurvival Shooter WITH PK Chapter.15「特殊な敵を実装する」
前回はコチラ
Unity公式チュートリアルSurvival Shooter WITH PK Chapter.13「敵に射撃がヒットした時、ノックバックする」
「Unity公式チュートリアルSurvival Shooter WITH PK Chapter.14「武器変更を実装する」」への2件のフィードバック