ロックオンの機能を追加します。
まず、新しい攻撃用のボタンの登録を行うとともに、List や TargetMarker ゲームオブジェクトのプレファブを登録するための変数を追加します。
処理の内容としては、新しい攻撃用のボタンを押し続けている間は、ロックオンの機能が有効になる処理を追加します。
この対象をロックオンする際の処理は、現在の攻撃の処理と同様に Raycast メソッドを活用します。
現在の攻撃では、Raycast メソッドで判定後、すぐに攻撃を行う流れになっていますが、この部分を List に保持しておくように変更しています。
同じく、ロックオンした位置についても List に保持しておきます。
ロックオンの機能は、現在の弾数の値とロックオンした対象の数とを比べておくことで、現在の弾数を超えてはロックオンが機能しないように制御します。
最後にロックオン用の攻撃用のボタンを離した際に、List にロックオンされた対象が登録されている場合には、
最初にロックオンされている対象から順番に攻撃の処理を1回ずつ行っていきます。これにより、3つの対象がロックオンされている場合には、
3回分の攻撃が行われて、着弾エフェクトの表示や、弾数の減少といった、通常の攻撃の処理と共通する制御を行います。
この説明を読んだうえで、まずは、処理のイメージを作り、どのようなロジックになればこれらの一連の機能を実装することができるのか設計を考えてみてください。
その後、実装に挑戦してから、最後にこちらのスクリプトを確認するようにしてみてください。
RayController.cs
<= クリックすると開きます。
using System.Collections;
using UnityEngine;
using UnityEngine.EventSystems;
/// <summary>
/// Ray による弾の発射処理の制御クラス
/// </summary>
public class RayController : MonoBehaviour {
[Header("発射口用のエフェクトのサイズ調整")]
public Vector3 muzzleFlashScale;
private bool isShooting;
private GameObject muzzleFlashObj; // 生成したエフェクトの代入用
private GameObject hitEffectObj;
private GameObject target; // Ray で補足した対象の代入用
[SerializeField, Header("Ray 用のレイヤー設定")]
private int[] layerMasks;
[SerializeField] //Debug用。確認できたら、SerializeField 属性を削除して private にしておく
private string[] layerMasksStr;
[SerializeField]
private PlayerController playerController;
private EnemyController enemy;
////* 新しい変数の宣言を4つ追加します *////
private List<EnemyController> targetList = new List<EnemyController>();
private List<TargetMarker> markerList = new List<TargetMarker>();
private List<RaycastHit> hitList = new List<RaycastHit>();
[SerializeField]
private TargetMarker targetMarkerPrefab;
////* ここまで *////
void Start() {
// Layer の情報を文字列に変換し、Raycast メソッドで利用しやすい情報を変数として作成しておく
layerMasksStr = new string[layerMasks.Length];
for (int i = 0; i < layerMasks.Length; i++) {
layerMasksStr[i] = LayerMask.LayerToName(layerMasks[i]);
}
}
void Update() {
// TODO ゲーム状態がプレイ中でない場合には処理を行わない制御をする
#if UNITY_EDITOR
// UI がタップされたときは処理しない(UI のボタンを押したらそちらのみを反応させる)
if (EventSystem.current.IsPointerOverGameObject()) {
return;
}
#else // スマホ用
if (EventSystem.current.IsPointerOverGameObject(Input.GetTouch(0).fingerId)) {
return;
}
#endif
// 発射許可がない場合
if (!playerController.IsShootPerimission) {
// 処理しない
return;
}
// リロード判定(弾数 0 でリロード機能ありの場合)
if (playerController.BulletCount == 0 && playerController.isReloadModeOn && Input.GetMouseButtonDown(0)) {
StartCoroutine(playerController.ReloadBullet());
}
////* ここから処理を修正・追加します *////
// 発射判定(弾数が残っており、リロード実行中でない場合)
if (playerController.BulletCount > 0 && !playerController.isReloading) { // <= ☆ 条件式を変更します
// 押しっぱなしで対象をロックオン
if (Input.GetButton("Fire2")) {
LockOnTargets();
//Debug.Log("Lock on");
}
// 押しっぱなしで弾を発射
if (Input.GetMouseButton(0)) {
// 発射時間の計測
StartCoroutine(ShootTimer());
}
}
// ロックオン対象がいる場合には各ロックオン対象に自動発射
if (Input.GetButtonUp("Fire2") && targetList.Count > 0) {
StartCoroutine(ShootLockOnTargets());
Debug.Log("Lock on Fire");
}
////* ここまで *////
}
/// <summary>
/// 継続的な弾の発射処理
/// </summary>
/// <returns></returns>
private IEnumerator ShootTimer() {
if (!isShooting) {
isShooting = true;
// 発射エフェクトの表示。初回のみ生成し、2回目はオンオフで切り替える
if (muzzleFlashObj == null) {
// 発射口の位置に RayController ゲームオブジェクトを配置する
muzzleFlashObj = Instantiate(EffectManager.instance.muzzleFlashPrefab, transform.position, transform.rotation);
muzzleFlashObj.transform.SetParent(gameObject.transform);
muzzleFlashObj.transform.localScale = muzzleFlashScale;
} else {
muzzleFlashObj.SetActive(true);
}
// 発射
Shoot();
yield return new WaitForSeconds(playerController.shootInterval);
muzzleFlashObj.SetActive(false);
if (hitEffectObj != null) {
hitEffectObj.SetActive(false);
}
isShooting = false;
} else {
yield return null;
}
}
/// <summary>
/// 弾の発射
/// </summary>
private void Shoot() {
// カメラの位置からクリック(タップ)した地点に Ray を投射
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
Debug.DrawRay(ray.origin, ray.direction, Color.red, 3.0f);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, playerController.shootRange, LayerMask.GetMask(layerMasksStr))) {
Debug.Log(hit.collider.gameObject.name);
// 同じ対象を攻撃しているか確認。対象がいなかったか、同じ対象でない場合
if (target == null || target != hit.collider.gameObject) {
target = hit.collider.gameObject;
Debug.log(target.name);
// TryGetComponent の処理で敵の情報を取得できるか判定をする
if (target.TryGetComponent(out enemy)) {
// 敵の情報を取得できた場合、ダメージの情報を敵のクラスに渡す
enemy.TriggerEvent(playerController.bulletPower);
// 演出
PlayHitEffect(hit.point, hit.normal);
}
// 同じ対象の場合
} else if (target == hit.collider.gameObject) {
// ダメージの情報を敵のクラスに渡す
enemy.TriggerEvent(playerController.bulletPower);
// 演出
PlayHitEffect(hit.point, hit.normal);
}
}
// 弾数を減らす
playerController.CalcBulletCount(-1);
}
/// <summary>
/// ヒット演出
/// </summary>
/// <param name="effectPos"></param>
/// <param name="surfacePos"></param>
private void PlayHitEffect(Vector3 effectPos, Vector3 surfacePos) {
// ヒット用のエフェクトが生成されているかを確認する
if (hitEffectObj == null) {
// まだ生成されていない場合には、ヒット用のエフェクトを生成
hitEffectObj = Instantiate(EffectManager.instance.hitEffectPrefab, effectPos, Quaternion.identity);
} else {
// 生成されている場合には、エフェクトを表示する位置と回転情報を更新
hitEffectObj.transform.position = effectPos;
hitEffectObj.transform.rotation = Quaternion.FromToRotation(Vector3.forward, surfacePos);
// エフェクトを表示
hitEffectObj.SetActive(true);
}
}
////* 新しいメソッドを2つ追加 *////
/// <summary>
/// ロックオン
/// </summary>
private void LockOnTargets() {
// 弾数以上はロックオンできない
if (playerController.BulletCount <= targetList.Count) {
return;
}
// カメラの位置から正面に向かって Ray を投射
Ray ray = new Ray(Camera.main.transform.position, Camera.main.transform.forward);
// クリックした位置用
ray = Camera.main.ScreenPointToRay(Input.mousePosition);
Debug.DrawRay(ray.origin, ray.direction, Color.green, 3.0f);
if (Physics.Raycast(ray, out RaycastHit hit, playerController.shootRange, LayerMask.GetMask(layerMasksStr))) {
Debug.Log(hit.collider.gameObject.name);
// TODO EnemyController から EventBase に変更する
// Ray の Hit した対象が Event であり、かつ、ターゲットのリストに登録されていない場合
if (hit.collider.TryGetComponent(out EnemyController eventBase) && !targetList.Contains(eventBase)) {
// クラスを継承して使うようにして、TryGetComponent の処理を Base を取得して統一する
targetList.Add(eventBase);
// 対象に照準マーカーを付与
TargetMarker targetMarker = Instantiate(targetMarkerPrefab, hit.collider.gameObject.transform);
targetMarker.SetUpTargetMarker(playerController.transform);
markerList.Add(targetMarker);
// ヒットした地点の登録
hitList.Add(hit);
}
}
}
/// <summary>
/// ロックオンした対象に順番に発射
/// </summary>
/// <returns></returns>
private IEnumerator ShootLockOnTargets() {
for (int i = 0; i < targetList.Count; i++) {
// 発射エフェクトの表示。初回のみ生成し、2回目はオンオフで切り替える
if (muzzleFlashObj == null) {
// 発射口の位置に RayController ゲームオブジェクトを配置する
muzzleFlashObj = Instantiate(EffectManager.instance.muzzleFlashPrefab, transform.position, transform.rotation);
muzzleFlashObj.transform.SetParent(gameObject.transform);
muzzleFlashObj.transform.localScale = muzzleFlashScale;
} else {
muzzleFlashObj.SetActive(true);
}
// ダメージ処理(デバッグ用。教材用)
targetList[i].TriggerEvent(playerController.bulletPower);
// 演出
PlayHitEffect(hitList[i].point, hitList[i].normal);
// マーカー削除
Destroy(markerList[i].gameObject);
// SE
SoundManager.instance.PlaySE(SoundManager.SE_Type.Gun_1);
playerController.CalcBulletCount(-1);
yield return new WaitForSeconds(playerController.shootInterval);
muzzleFlashObj.SetActive(false);
if (hitEffectObj != null) {
hitEffectObj.SetActive(false);
}
}
targetList.Clear();
markerList.Clear();
hitList.Clear();
}
////* ここまで *////
}
スクリプトを修正したらセーブします。