[C#]デリゲートを利用したゲームキャラの行動パターンの制御
デリゲートの概要
デリゲートとは日本語で、「委譲」を意味し、C#においてはメソッドを参照する事で、参照したメソッドと同じ働きをするオブジェクトを生成することができる。
これにより、メソッドの呼び分けを便利にすることができる。
またこれを用いたゲームのキャラクタの制御を行ったので報告する。
目次
- デリゲートの宣言
- 複数メソッドの指定
- ゲームでの応用
デリゲートの宣言と利用
コードと実行結果
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Delegate
{
internal class Program
{
delegate void test(int i);//デリゲートの宣言、この場合戻り値は無し、引数はint型
static void Main(string[] args)
{
test test=new test(test1);
test(5);
test = test2;
test(6);
}
static void test1(int i)
{
Console.WriteLine(System.Reflection.MethodBase.GetCurrentMethod().Name +"::i="+i.ToString() );
}
static void test2(int i)
{
Console.WriteLine(System.Reflection.MethodBase.GetCurrentMethod().Name+"::i="+i.ToString());
}
}
}
概要に記載した通り、デリゲートもオブジェクトの1種であるため、宣言及び生成が必要になる。
宣言時に必要な情報は、オブジェクト名、参照するメソッドの戻り値、引数であり下のようになる。
また デリゲートで呼ばれるメソッドの引数、戻り値はそれぞれ統一する必要がある。
delegate void test(int i);//この場合、戻り値は無し、引数はint型
オブジェクトの生成は通常のオブジェクトの生成と同様にnew演算子を用いオブジェクトを生成する。この時コンストラクタの引数にメソッド名を与えることで参照するメソッドを指定する。
test test=new test(test1);//①今回はtest1というメソッドを参照している
このようにして参照したメソッドは通常のメソッドと同様に"オブジェクト名()"の形で利用できる。
test(5);//①で参照したtest1というメソッドを、testオブジェクトから利用している
複数メソッドの指定
コードと実行結果
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Delegate
{
internal class Program
{
delegate void test(int i);//この場合、戻り値は無し、引数はint型
static void Main(string[] args)
{
test test=new test(test1);
test += test2;//上で参照したtest1に加え、test2も参照する。
test(2);
}
static void test1(int i)
{
Console.WriteLine(System.Reflection.MethodBase.GetCurrentMethod().Name +"::i="+i.ToString() );
}
static void test2(int i)
{
Console.WriteLine(System.Reflection.MethodBase.GetCurrentMethod().Name+"::i="+i.ToString());
}
}
}
デリゲートでは、+=演算子を用いる事で複数のメソッドを指定する事もできる。(下記)
またメソッドの実行順はメソッドを登録した順番である。
test += test2;
ゲームでの応用(Unity)
コード全体
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Stage1Boss : MonoBehaviour
{
const int Hp = 1500;
int hp;
GameObject cam;
GameObject Mysip;
HpBar HpBar;
Vector3 iniPos;//初期位置
Rigidbody rb;
public GameObject shotPref;
public GameObject WarpPref;
delegate void move();
int loopCnt;
bool angleIsFixed=true;
enum E_MoveSection
{
Section1=500,
Section2=1000,
Section3=1500
}
// Start is called before the first frame update
void Start()
{
rb = GetComponent();
loopCnt = 0;
hp= Hp;
Mysip = GameObject.FindGameObjectWithTag("MyShip");
cam = GameObject.FindGameObjectWithTag("MainCamera");
HpBar = GameObject.FindGameObjectWithTag("HpBar").GetComponent();
transform.SetParent(cam.transform);
HpBar.IsDisplayed = true;
iniPos=transform.localPosition;
}
// Update is called once per frame
void Update()
{
HpBar.DisplayHpBar(hp, Hp);
if (hp <= 0)
{
HpBar.IsDisplayed=false;
Destroy(gameObject);
}
Vector3 v=this.GetComponent().velocity;
float angle ;
if (angleIsFixed)
{
angle = 90;
}
else{
angle = -Mathf.Atan2(v.y, v.x)*Mathf.Rad2Deg;
}
transform.rotation = Quaternion.Euler(angle, 90, -90);
}
private void FixedUpdate()
{
BossMove()();
loopCnt++;
}
move BossMove()
{
move move;
switch (loopCnt % 1500)
{
case int n when n < (int)E_MoveSection.Section1:
move = new move(BossSection1);
move += radious;
break;
case int n when n < (int)E_MoveSection.Section2:
move = new move(BossSection2);
move += warp;
break;
default://(int)E_MoveSection.Section3
move = new move(BossSection3);
move += aim;
break;
}
return move;
}
private void OnTriggerEnter(Collider other)
{
if (other.gameObject.tag == "NormalBullet")
{
hp--;
}
else if (other.gameObject.tag == "ChargeShot")
{
ChargeShot cs = other.GetComponent();
if (cs != null)
{
hp -= cs.damege;
Debug.Log("damage=" + cs.damege);
}
}
//Debug.Log("BOSS_HP="+hp);
}
//以下行動ターン
void BossSection1()
{ //初期位置にゆっくり戻る
angleIsFixed = true;
Debug.Log("Move");
Vector3 pos=transform.localPosition;
Vector3 v=iniPos-pos;
rb.velocity = v;
}
void BossSection2()
{
angleIsFixed = true;
Vector3 v= new Vector3(0, -1, 0) * 5;
rb.velocity = v;
}
void BossSection3()
{
angleIsFixed = false;
Vector3 v= new Vector3(0, 1, 0) * 10;
rb.velocity = v;
Debug.Log("BossBack");
}
//以下攻撃パターン
void radious()
{
if (loopCnt % 10 == 0)
{
for (int i = 0; i < 360; i += 30)
{
int rnd = Random.Range(-5, 5);
float rad = (i + rnd) * Mathf.Deg2Rad;
GameObject shot = Instantiate(shotPref, transform.position, Quaternion.identity);
shot.transform.SetParent(cam.transform);
shot.GetComponent().velocity = new Vector3(Mathf.Cos(rad), Mathf.Sin(rad), 0) * 2;
}
}
}
void warp()
{
//取り巻きのザコを出現させる
if (loopCnt % 30 == 0)
{
Instantiate(WarpPref, transform.position, Quaternion.identity);
}
}
void aim()
{
//自機狙い
if (loopCnt % 10 == 0)
{
GameObject shot = Instantiate(shotPref, transform.position, Quaternion.identity);
shot.transform.SetParent(cam.transform);
shot.GetComponent().velocity = (Mysip.transform.position - shot.transform.position).normalized * 10;
}
}
}
今回は、シューティングゲームのボス敵の行動パターンの作成にデリゲートを用いた。
移動パターンと、射出パターンをそれぞれ独立して作り、委譲することで行動パターンのバリエーションを増す事が可能。例えば、射出パターンと移動パターンをそれぞれ3種類作成すれば、単純計算で全部で9通りの行動パターンが作成できる。いくつかは、ゲームに適さない物もあるがそれでも数十、数百パターンを、少ないメソッドで作成できる。
また今回はデリゲートに用いるオブジェクトの型はmoveとした。(下記)
delegate void move();
今回作成するボスは時経過で行動パターンが変化するようにした。行動パターンは以下の3つの繰り返しとした。
- 画面中央付近に移動しつつ円形に弾を射出
- 画面外に移動し、雑魚的がワープしてくる
- 画面内に戻りそのまま画面上部に移動そのまま再度画面外へ。同時に自機狙いで弾を射出
呼び出すメソッドの切り替え用の関数の戻り値はmove型とした。これにより関数の呼び出し元でメソッドを実行するようにしている(下コード)。
move BossMove()
{
move move;
switch (loopCnt % 1500)
{
case int n when n < (int)E_MoveSection.Section1:
move = new move(BossSection1);
move += radious;
break;
case int n when n < (int)E_MoveSection.Section2:
move = new move(BossSection2);
move += warp;
break;
default://(int)E_MoveSection.Section3
move = new move(BossSection3);
move += aim;
break;
}
return move;
}
デリゲートのサンプルダウンロード