Unityで弾を撃つ方法(3D)
今回はUnityの3Dゲーム制作で弾を撃つ方法を紹介していく。前回までの記事↓
Unityのゲーム制作
方法は色々あるが、今回はプレイヤーが技を出したらプレハブで作っておいた弾(炎)を撃ち出すようにしていく。
Unityで弾を撃つ方法(3D)
弾を撃つポーズを作る弾が出現する場所を作る
弾を作る
スクリプト記述
目次にもどる
弾を撃つポーズを作る
まずは弾を撃つポーズを作る。ここは今まで記事で紹介したのとほぼ同じ作業。ボタンを押したらキャラが何かしらのアニメーションをとるようにすれば何でもOKだ。やり方がわからない場合は以下を確認してほしい。
剣を振る方法
燃える剣の必殺技
今回は炎を飛ばすので、自分は右クリックすると剣に炎をまとって突き出すアニメーションが出るようにした。
メソッドを呼び出すイベントを作成し、AttackStart、Hit()、AttackEndを呼び出すようにする。AttackStartで炎を撃ち、Hit()で攻撃終了、AttackEndでは攻撃アニメーション終了処理を行うことを考慮してイベントをアニメーションのどこで発生させるか決めよう。
目次にもどる
弾が出現する場所を作る
次に弾が出現する場所を作る。Cubeなどを作り、今回は名前をHoudaiとする。Houdaiを剣や銃といった武器の子要素とする。剣の刀身や銃口など弾を出現させたい位置にHoudaiを移動させる。
.jpg)
目次にもどる
弾を作る
Houdaiの子要素としてCubeを作成し名前をHonooにしてRigidbodyのコンポーネントを装着。このHonooをトリガーにするためにBox Colliderコンポーネントのis Triggerにチェックをつける。この辺りの仕組みがわからない場合は以下の記事を確認してほしい。ダメージ判定・処理
Honooのゲームオブジェクト自体は見えなくするため、Mesh Rendererのチェックを外す。
Honooのゲームオブジェクトが当たり判定となるので、よく考えて大きさを設定する。
Honooの子要素としてパーティクルを追加。自分は以下のフリー素材の炎パーティクルを使用した(FX_Kandol_Pack/FX Fire Ⅱ/PrefabsのFX_Fire_ParticleSystem01.prefab)。
炎のフリー素材(2023年8月現在はフリー)
Honooのゲームオブジェクトの大きさにだいたい合うように炎パーティクルの大きさを変更する。
.jpg)
Honooの子としてcenterというゲームオブジェクトも作っている。後述するが着弾時吹っ飛ばし処理に関わる(これについては必ずしも実装する必要はない)。centerは弾のすぐ後ろの位置に移動させておく。
プロジェクトビューにPrefabフォルダを作成しその下にTamaフォルダを作成する(名前は自分のわかるように)。TamaフォルダにHonooをドラッグ&ドロップしてプレハブ化する。
今Houdaiの子要素となっているHonooについては削除する。
目次にもどる
スクリプト記述
炎をまとった剣を突き出すスクリプト(以前、剣振りなどで作ったのとほぼ同じスクリプト)に以下のように記述してプレイヤーのゲームオブジェクトにアタッチする(炎射撃部分のみ記述している)。public void AttackStart()
{
//攻撃開始
Attack = anim.GetInteger("Attack");
if (Attack == 20)
{
//houdaiのゲームオブジェクトの位置を代入。
houdaipos = houdai.transform.position;
//指定した炎のプレハブをhoudaiの位置に実体化。実体化したゲームオブジェクトをchildObjectに代入。
GameObject childObject = Instantiate(honotobasuprefab, houdaipos, Quaternion.identity);
//childObjectのPlayerAttackスクリプトを取得。
Playertamascript = childObject.GetComponent<PlayerAttack>();
//PlayerAttackのattackcharatuikaメソッドを呼び出し、プレハブ用処理を行う。
Playertamascript.attackcharatuika(this.gameObject);
//childObjectをプレイヤーの向きと同じにする。
childObject.transform.rotation = this.transform.rotation;
//childObjectのRigidbodyを取得。
tamazyuuryoku = childObject.GetComponent<Rigidbody>();
//プレイヤーの前方方向にAddForceのImpulseで瞬間の力をかけている。
tamazyuuryoku.AddForce(this.transform.forward * 1500f, ForceMode.Impulse);
//0.6秒後にchildObjectを削除する。
Destroy(childObject, 0.6f);
//効果音の設定。
SEscript.ObjectSE(this.gameObject);
SEscript.PlaySE(3);
SEscript.ObjectSE(this.gameObject);
SEscript.PlaySE(4);
Kiseki2.emitting = true;
anim.SetBool("Attacktime", true); //剣振り開始。
anim.SetInteger("Hcount", 1); //ヒットする回数。
anim.SetInteger("Tyakudansyurui", 1); //着弾タイプ。
anim.SetFloat("Hitinterval", 0.5f); //ヒット後、次のヒットまでの時間。
anim.SetInteger("Attacksyurui", 2); //魔法
anim.SetFloat("Tobashi", 0.1f); //吹っ飛ばし力
anim.SetBool("Atarikinsetu", false); //近接当たり発動。
return;
}
}
public void Hit()
{
//攻撃終了
Attack = anim.GetInteger("Attack");
if (Attack == 20)
{
Kiseki2.emitting = false;
anim.SetBool("FullAttacktime", false); //攻撃終了。
anim.SetBool("Attacktime", false); //剣振り終了。
anim.SetInteger("Hcount", 0); //ヒットする回数を0に初期化。
anim.SetBool("Atarikinsetu", false); //近接当たり停止。
return;
}
}
public void AttackEnd()
{
//攻撃アニメーション終了時の処理
Combo = false;
Attack = anim.GetInteger("Attack");
anim.SetInteger("Attack", 0);
anim.SetBool("FullAttacktime", false); //攻撃終了。
//ファイア飛ばし終了
if (Attack == 20)
{
Fire1.Stop();
Debug.Log("炎終了");
StartCoroutine(ComboEnd());
return;
}
}
シリアル化しているので、インスペクター欄でhonotobasuprefabに先ほど作成したHonooのプレハブを指定する。省略しているが、攻撃中でない時に右クリックを押した時にAttackパラメータに20が入るようにして、AnimatorcontrollerではAttackパラメータが20だと遷移して炎をまとった剣を突き出すアニメーションをはじめるように指定しておく(もちろん、どのキーで攻撃を始めるようにするかは自由)。
アニメーションがはじまるとAttackStartが呼び出されAttackパラメータに20が入っていると炎射撃処理が行われる。
HonooプレハブをHoudaiの位置に実体化させて、AddforceのImpulseで力をかけて前方に飛ばしている。
Honooプレハブに以下のスクリプトを記述しアタッチしておく(ほとんど以前の記事で作成したPlayerAttackと同じ)。
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.UI;
using static Cinemachine.DocumentationSortingAttribute;
public class PlayerAttack : MonoBehaviour
{
//シリアル化している。charadataを指定。
[SerializeField] charadata charadata;
[SerializeField] Lvdata Lvdata;
[SerializeField] GameObject AttackChara;
[SerializeField] GameObject centerprefab;
Animator anim;
Animator animaite;
int HHcount;
int ATK;
int INT;
int Damagetypekioku;
float Hitkankaku;
int Hit;
Rigidbody Aitezyuryoku;
int Kakutokuexp;
bool korehaprefab;
int a;
int b;
void Start()
{
anim = AttackChara.GetComponent<Animator>();
}
//プレハブの場合はここでAttackcharaを追加
public void attackcharatuika(GameObject playerObject)
{
AttackChara = playerObject;
anim = AttackChara.GetComponent<Animator>();
korehaprefab = true;
}
//攻撃範囲がゲームオブジェクトに侵入している間は呼び出し
void OnTriggerStay(Collider other)
{
if (other.tag == "Enemy")
{
IDamageable damageable = other.GetComponent<IDamageable>();
//damageableにnull値が入っていないかチェック
if (damageable != null)
{
Hitkankaku = anim.GetFloat("Hitinterval");
bool Hitkankakukakunin = damageable.HitKankaku(Hitkankaku);
if (Hitkankakukakunin)
{
return;
}
//物理は1、魔法は2、物理+魔法は3
Damagetypekioku = anim.GetInteger("Attacksyurui");
//ダメージタイプを送る。物理は1、魔法は2、物理+魔法は3
damageable.Damagetype(Damagetypekioku);
//着弾タイプを取得
int Tyakudantypekioku = anim.GetInteger("Tyakudansyurui");
//着弾タイプを送る。
damageable.tyakudantype(Tyakudantypekioku);
//damageableのダメージ処理メソッドを呼び出す。引数としてcharadataのATKを指定
ATK = charadata.ATK;
INT = charadata.INT;
switch (Damagetypekioku)
{
case 1:
// キャラのATKの値を火力として送る。
Kakutokuexp = damageable.Damage(ATK);
break;
case 2:
// キャラのINTの値を火力として送る。
Kakutokuexp = damageable.Damage(INT);
break;
case 3:
// キャラの(ATK+INT)÷2の値を火力として送る。
Kakutokuexp = damageable.Damage((ATK + INT) / 2);
break;
}
//獲得経験値があるなら経験値処理
if (Kakutokuexp > 0)
{
Debug.Log("経験値ある");
charadata.EXP = charadata.EXP + Kakutokuexp;
var a =Lvdata.playerExpTable[charadata.LV];
if (charadata.EXP >= a.exp)
{
charadata.LV += 1;
Debug.Log("レベル上がった");
Debug.Log(charadata.LV);
}
}
//吹っ飛ばし処理。
//相手側の吹っ飛ばしパラメータを1にする
animaite = other.GetComponent<Animator>();
animaite.SetInteger("Tobashityuu", 1); //吹っ飛ばし発動。
//相手側の吹っ飛ばし時間計算
float Tobashitikara = anim.GetFloat("Tobashi");
float Tobashitime = Tobashitikara + 0.1f;
Tobashitime = 0.3f;
animaite.SetFloat("Tobashizikan", Tobashitime); //吹っ飛ばし時間。
//自身の位置と攻撃した相手の位置から自身→相手への単位ベクトルを出す。
Vector3 pos = AttackChara.transform.position;
//プレハブ弾補正
if (korehaprefab)
{
pos = centerprefab.transform.localPosition;
}
Vector3 Aitepos = other.transform.position;
Vector3 tobashihoukou = (Aitepos - pos).normalized;
//Rigidbodyのフリーズを全て外した上で、方向のみ制限をつけ、吹きとばす。
Aitezyuryoku = other.GetComponent<Rigidbody>();
Aitezyuryoku.constraints = RigidbodyConstraints.None;
Aitezyuryoku.constraints = RigidbodyConstraints.FreezeRotation;
Aitezyuryoku.AddForce(transform.TransformDirection(tobashihoukou) * Tobashitikara*1000, ForceMode.Impulse);
//プレハブ弾を消す処理
if(korehaprefab)
{
Destroy(this.gameObject);
}
}
}
}
}
重要なのは以下の部分。
void Start()
{
anim = AttackChara.GetComponent<Animator>();
}
//プレハブの場合はここでAttackcharaを追加
public void attackcharatuika(GameObject playerObject)
{
AttackChara = playerObject;
anim = AttackChara.GetComponent<Animator>();
korehaprefab = true;
}
プレハブではヒエラルキー欄のゲームオブジェクトをSerializeFieldとして指定することができない。しかしプレイヤーのゲームオブジェクトは取得したい。よって、プレイヤーにアタッチした炎をまとった剣を突き出すスクリプトのAttackStartで以上のattackcharatuika(GameObject playerObject)のメソッドを呼び出すようにしている。
送られたきたプレイヤーのゲームオブジェクトを引数から取得してAttackCharaに代入して保存。また、プレハブでの攻撃処理だと区別するためにkorehaprefabをtrueにしている。
そして、プレハブでの攻撃処理の場合、着弾したら消えるようにするので、最後に以下のように記述している。
//プレハブ弾を消す処理
if(korehaprefab)
{
Destroy(this.gameObject);
}
今回は詳しく紹介しないが、吹っ飛ばし処理についても記述している。
//吹っ飛ばし処理。
//相手側の吹っ飛ばしパラメータを1にする
animaite = other.GetComponent<Animator>();
animaite.SetInteger("Tobashityuu", 1); //吹っ飛ばし発動。
//相手側の吹っ飛ばし時間計算
float Tobashitikara = anim.GetFloat("Tobashi");
float Tobashitime = Tobashitikara + 0.1f;
Tobashitime = 0.3f;
animaite.SetFloat("Tobashizikan", Tobashitime); //吹っ飛ばし時間。
//自身の位置と攻撃した相手の位置から自身→相手への単位ベクトルを出す。
Vector3 pos = AttackChara.transform.position;
//プレハブ弾補正
if (korehaprefab)
{
pos = centerprefab.transform.localPosition;
}
Vector3 Aitepos = other.transform.position;
Vector3 tobashihoukou = (Aitepos - pos).normalized;
//Rigidbodyのフリーズを全て外した上で、方向のみ制限をつけ、吹きとばす。
Aitezyuryoku = other.GetComponent<Rigidbody>();
Aitezyuryoku.constraints = RigidbodyConstraints.None;
Aitezyuryoku.constraints = RigidbodyConstraints.FreezeRotation;
Aitezyuryoku.AddForce(transform.TransformDirection(tobashihoukou) * Tobashitikara*1000, ForceMode.Impulse);
プレイヤーの攻撃位置と敵の位置から単位ベクトルを出し着弾した敵が吹っ飛ばす方向を決めている。プレイヤーが格闘攻撃をする場合にはこのままで問題ないが、プレハブでの飛び道具で攻撃する場合には飛び道具の位置と敵の位置で計算するようにしている(centerprefabには先述した子ゲームオブジェクトcenterを指定している。 centerを弾の後ろの位置にしておけば着弾した際も、弾が飛んできた方向とは逆に吹っ飛ばしを発生させられる)。
ダメージ処理については敵に以下のスクリプトをアタッチしておく。
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
//UIを使用するので記述。
using UnityEngine.UI;
public class CharaDamage : MonoBehaviour, IDamageable
{
//シリアル化している。charadataを指定。
[SerializeField] private charadata charadata;
//シリアル化。SliderのHPゲージ指定
[SerializeField] Slider Slider;
[SerializeField] GameObject UkeruChara;
Tyakudan tyakudanscript;
[SerializeField] GameObject tyakudanobj;
int Damagetypekioku;
int tyakudantypekioku;
float Hitkankaku;
bool Hitkaishi;
int ototyakudan;
SEkanri SEscript;
Animator anim;
int HP;
int MAXHP;
int ATK;
int DEF;
int INT;
int RES;
void Start()
{
//charadataがnullでないことを確認
if (charadata != null)
{
//valueのHPゲージのスライダーを最大の1に
Slider.value = 1;
HP = charadata.MAXHP;
MAXHP = charadata.MAXHP;
ATK = charadata.ATK;
DEF = charadata.DEF;
INT = charadata.INT;
RES = charadata.RES;
}
GameObject a = GameObject.FindWithTag("Tyakudan");
tyakudanscript = a.GetComponent<Tyakudan>();
GameObject b = GameObject.FindWithTag("SEkanri");
SEscript = b.GetComponent<SEkanri>();
}
public bool HitKankaku(float value)
{
Hitkankaku = value;
return Hitkaishi;
}
public void Damagetype(int value)
{
Damagetypekioku = value;
}
public void tyakudantype(int value)
{
tyakudantypekioku = value;
}
// ダメージ処理のメソッド valueにはPlayer1のATKやINTの値が入ってる
public int Damage(int value)
{
ATK = charadata.ATK;
DEF = charadata.DEF;
INT = charadata.INT;
RES = charadata.RES;
switch (Damagetypekioku)
{
case 1:
// キャラのATKからMazokusoldierのDEFを引いた値をHPから引く
HP -= value - DEF;
Debug.Log("ダメージ処理開始1");
break;
case 2:
// キャラのINTからMazokusoldierのRESを引いた値をHPから引く
HP -= value - RES;
Debug.Log("ダメージ処理開始2");
break;
case 3:
// キャラの(ATK+INT)÷2の値からMazokusoldierの(DEF+RES)÷2を引いた値をHPから引く
HP -= value - ((DEF+RES)/2);
Debug.Log("ダメージ処理開始3");
break;
}
// HPゲージに反映
Slider.value = (float)HP / (float)MAXHP;
Hitkaishi = true;
StartCoroutine(Hittime());
// 着弾エフェクト
tyakudanscript.tyakudankiokusyori(tyakudanobj);
ototyakudan = tyakudanscript.tyakudansyori(tyakudantypekioku);
SEscript.ObjectSE(this.gameObject);
SEscript.PlaySE(ototyakudan);
// HPが0以下ならDeath()メソッドを呼び出す。
if (HP <= 0)
{
Death();
return charadata.GETEXP;
}
else
{
return 0;
}
}
// 死亡処理のメソッド
public void Death()
{
// ゲームオブジェクトを非アクティブに
this.gameObject.SetActive(false);
}
IEnumerator Hittime()
{
yield return new WaitForSeconds(Hitkankaku);
Hitkaishi = false;
}
}
以上のように記述すれば炎を飛ばして敵に当てダメージを与える処理が可能(着弾時に敵に炎が出る処理については紹介していない。今後機会があれば紹介する)。
続きの記事は以下。
足音をつける