アマゾンバナーリンク

ディスプレイ広告

スポンサーリンク

Unityで作る弾幕シューティングゲーム その6 敵の制御と弾幕生成の設計

2020年7月15日

こんにちは!ジェイです。前回は音楽を鳴らすフレームワークを作ったので今回は敵の制御と弾幕生成の仕様について考えてみます。

記事内広告

敵生成と弾幕生成のクラスを分ける

敵の行動の種類と弾幕のクラスを一緒に作成してしまうと、例えば、3種類の敵の行動と3種類の弾幕を作ってしまうと3×3で9種類のクラスが必要になります。そうならないように、敵のクラスと弾幕生成用のクラスを分けて作ります。

弾幕生成クラス

まずは弾幕を生成するクラスをゲーム全体の管理をするCGameManagerのCBulletFactoryとして追加します。

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CGameManager : MonoBehaviour
{
public class CBulletFactory
{
public GameObject Bullet;
public float Radius;
public string[] SpriteName;
public Sprite[] BulletSprite;
public bool ColliderType;
float SizeX, SizeY;
public CBulletFactory(string[] sprite_name, bool collider_type, float radius=0.5f,float size_x=0.5f, float size_y = 0.5f)
{
Radius = radius;
SpriteName = sprite_name;
BulletSprite = new Sprite[SpriteName.Length];
ColliderType = collider_type;
SizeX = size_x;
SizeY = size_y;
}
public void Load()
{
for (int i = 0; i < SpriteName.Length-1; ++i)
{
BulletSprite[i] = CUtility.GetSprite(SpriteName[0], SpriteName[i + 1]);
}
}
public void CreateBullet(Vector3 pos, int color, int script_type, float speed, float angle) 
{
GameObject newParent = new GameObject("Empty");
Bullet = Instantiate(newParent, pos, Quaternion.identity);
Bullet.tag = "Bullet";
SpriteRenderer sr = Bullet.AddComponent<SpriteRenderer>();
sr.sprite = BulletSprite[color];
sr.sortingLayerName = "Bullet";
Bullet.transform.localScale = new Vector3(0.7f, 0.7f, 0.7f);
switch(script_type)
{
case 0:
Bullet.AddComponent<CBullet>().SetParam(speed, angle);
break;
} 
if (ColliderType)
{
CircleCollider2D cc = Bullet.AddComponent<CircleCollider2D>();//.radius = SizeX;
cc.radius = Radius;
cc.isTrigger = true;
}
else
{
BoxCollider2D bc = Bullet.AddComponent<BoxCollider2D>();//.size = new Vector2(SizeX, SizeY);
bc.size = new Vector2(SizeX, SizeY);
bc.isTrigger = true;
}
Destroy(newParent);
}
}
public CBulletFactory[] BulletFactory = new CBulletFactory[]
{
new CBulletFactory(new string[]{ "img/bullet/b0","b0_0","b0_1","b0_2","b0_3","b0_4" }, true),
new CBulletFactory(new string[]{ "img/bullet/b1","b1_0","b1_1","b1_2","b1_3","b1_4","b1_5" },true),
new CBulletFactory(new string[]{ "img/bullet/b2","b2_0","b2_1" ,"b2_2" ,"b2_3" ,"b2_4" ,"b2_5" ,"b2_6" ,"b2_7" ,"b2_8" ,"b2_9"},false),
new CBulletFactory(new string[]{ "img/bullet/b3","b3_0","b3_1" ,"b3_2" ,"b3_3" ,"b3_4" },false),
new CBulletFactory(new string[]{ "img/bullet/b4","b4_0","b4_1" ,"b4_2" ,"b4_3" ,"b4_4" ,"b4_5" ,"b4_6" ,"b4_7" ,"b4_8" ,"b4_9"},true),
new CBulletFactory(new string[]{ "img/bullet/b5","b5_0","b5_1" ,"b5_2" },false),
new CBulletFactory(new string[]{ "img/bullet/b6","b6_0","b6_1" ,"b6_2"},false),
new CBulletFactory(new string[]{ "img/bullet/b7","b7_0","b7_1" ,"b7_2" ,"b7_3" ,"b7_4" ,"b7_5" ,"b7_6" ,"b7_7" ,"b7_8" ,"b7_9" },true),
new CBulletFactory(new string[]{ "img/bullet/b8","b8_0","b8_1" ,"b8_2" ,"b8_3" ,"b8_4" ,"b8_5" ,"b8_6" ,"b8_7" ,"b8_8" },false),
new CBulletFactory(new string[]{ "img/bullet/b9","b9_0","b9_1" ,"b9_2" },false),
new CBulletFactory(new string[]{ "img/bullet/b10","b10_0","b10_1" ,"b10_2" ,"b10_3" ,"b10_4" ,"b10_5" ,"b10_6" ,"10_7" },true),
new CBulletFactory(new string[]{ "img/bullet/b11","b11_0","b11_1" ,"b11_2" ,"b11_3" ,"b11_4" ,"b11_5" ,"b11_6" ,"11_7" },true),
new CBulletFactory(new string[]{ "img/bullet/b12","b12_0","b12_1" ,"b12_2" ,"b12_3" ,"b12_4" ,"b12_5" ,"b12_6" ,"b12_7" ,"b12_8" ,"b12_9" },true),
new CBulletFactory(new string[]{ "img/bullet/b13","b13_0","b13_1" ,"b13_2" ,"b13_3" ,"b13_4" ,"b13_5" ,"b13_6" ,"b13_7" ,"b13_8" ,"b13_9" },true),
new CBulletFactory(new string[]{ "img/bullet/b14","b14_0","b14_1" ,"b14_2" ,"b14_3" },true),
new CBulletFactory(new string[]{ "img/bullet/b15","b15_0","b15_1" ,"b15_2" },true),
new CBulletFactory(new string[]{ "img/bullet/_b6","_b6_0","_b6_1","_b6_2","_b6_3","_b6_4","_b6_5","_b6_6","_b6_7","_b6_8","_b6_9" },false),
new CBulletFactory(new string[]{ "img/bullet/l0", "l0_0", "l0_1" },false),
new CBulletFactory(new string[]{ "img/bullet/l0_moto", "l0_moto_0", "l0_moto_1" },true),
};
void Start()
{
for (int i = 0; i < BulletFactory.Length; ++i)
{
BulletFactory[i].Load();
}
}
}

まず初めに弾のスプライトの数だけ情報を配列を用意します。コンストラクタで初期化する情報は以下です。

CBulletFactory(string[] sprite_name, bool collider_type, float radius=0.5f,float size_x=0.5f, float size_y = 0.5f)

  • string[] sprite_name 1番目の要素に弾画像ファイルのパス、それ以降に分解した弾のスプライトの名前を入れる
  • bool collider_type trueならCircleCollider2D、falseならBoxCollider2Dを追加
  • radius CircleCollider2Dの当たり判定の円の大きさを決める
  • float size_x
  • float size_y

そして、次にBulletFactory[i].Loadで弾の種類の数だけスプライトをGetSprite関数でロードします。

 //【機能】マルチプルスプライトからスライスしたスプライトを取得する
//【第1引数】スプライトファイル名(正確にはResources フォルダからのスプライトファイルまでのパス)
//【第2引数】取得したいスライスされたスプライト名
//【戻り値】取得したスプライト
public static Sprite GetSprite(string fileName, string spriteName)
{
Sprite[] sprites = Resources.LoadAll<Sprite>(fileName);
return System.Array.Find<Sprite>(sprites, (sprite) => sprite.name.Equals(spriteName));
}

実際に弾を生成するCreateBulletは以下の様になります。

public void CreateBullet(Vector3 pos, int color, int script_type, float speed, float angle) 
{
GameObject newParent = new GameObject("Empty");
Bullet = Instantiate(newParent, pos, Quaternion.identity);
Bullet.tag = "Bullet";
SpriteRenderer sr = Bullet.AddComponent<SpriteRenderer>();
sr.sprite = BulletSprite[color];
sr.sortingLayerName = "Bullet";
Bullet.transform.localScale = new Vector3(0.7f, 0.7f, 0.7f);
switch(script_type)
{
case 0:
Bullet.AddComponent<CBullet>().SetParam(speed, angle);
break;
} 
if (ColliderType)
{
CircleCollider2D cc = Bullet.AddComponent<CircleCollider2D>();//.radius = SizeX;
cc.radius = Radius;
cc.isTrigger = true;
}
else
{
BoxCollider2D bc = Bullet.AddComponent<BoxCollider2D>();//.size = new Vector2(SizeX, SizeY);
bc.size = new Vector2(SizeX, SizeY);
bc.isTrigger = true;
}
Destroy(newParent);
}

一度Instantiateで親オブジェクトを生成し、最後に削除しているのがポイントです。

スクリプトから敵を生成する

ゲーム内には、色々なイベントが発生します。
例えば、

  • アイテムの発生
  • 敵の制御
  • 待ち時間
  • BGMの再生
  • 会話などのシーンを発生

これらをスクリプトで読み込んで、制御するのにコマンドパターンを使用します。

下の様にRun関数を実装した、インターフェースを用意します。
インターフェイスとは、仮想関数のみ定義できるクラスのことです。
抽象クラスでは、メンバ変数を持たせることができましたが、インターフェイスでは、できないので継承とオーバーライドを使うことを前提とします。

public interface ICommand
{
void Run();
};

次に派生クラスを用意します。

public class CWaitCommand : ICommand
{
private CCommandManager CommandManager;
private float WaitTime;
public CWaitCommand(CCommandManager game_control, float wait_time)
{
CommandManager = game_control;
WaitTime = wait_time;
}
public void Run()
{
CommandManager.WaitTime = WaitTime;
}
}

これは待ち時間を設定するコマンドのクラスです。敵を呼び出すコマンドのクラスなど、ICommandを派生してやれば、好きなイベントを作ることができます。

public class CCommandManager
{
 private List<ICommand> Command = new List<ICommand>();
public void Run()
{
if (WaitTime > 0) WaitTime--;
while (CommandIndex < Command.Count && WaitTime == 0)
{
Command[CommandIndex].Run();
CommandIndex++;
}
}
}

そして、その定義した派生クラスのコマンドをCCommandManagerクラスのList<ICommand>型のCommandに追加します。CGameManagerクラスのUpdate関数でRunを実行すれば、WaitTimeが0の時に、異なった派生クラス型のCommand.Runが実行されて、イベントが起きる仕組みです。

1つのインターフェースに対して複数の異なる振る舞いをする実装を作成できる機構であるポリモーフィズム(多様性)を使い異なった目的のコマンドを同じRun関数で実行していくのが、今回のコマンドパターンの特徴です。

ちなみにこのRun関数の呼び出しは、CGameManager.csのUpdate関数で呼び出します。

敵生成コマンド

コマンドパターンを使うには引数を単純にする必要があるので、構造体に必要なパラメーターをまとめてCEnemy.csに追加します。

public struct TagEnemyStatus
{
public GameObject EnemyObj;
public float X, Y;
public int Life, GraphType, WaitTime, BulletPattern;
public int BulletInterval, BulletType, BulletColor, BulletScriptType;
public TagEnemyStatus(GameObject enemy_obj, float x = 0.0f, float y = 0.0f,
int life = 1, int graph_type = 0, int wait_time = 180, int bullet_pattern = 0,
int bullet_interval = 60, int bullet_type = 0, int bullet_color = 0, int bullet_script_type=0)
{
EnemyObj = enemy_obj;
X = x;
Y = y;
Life = life;
GraphType = graph_type;
WaitTime = wait_time;
BulletPattern = bullet_pattern;
BulletInterval = bullet_interval;
BulletType = bullet_type;
BulletColor = bullet_color;
BulletScriptType = bullet_script_type;
}
}

さらに敵の共通する部分を抜き出すために敵基底クラスのCEnemyクラスを定義します。OnTriggerEnter2Dを基底クラスに追加しておけば、違う種類の2DColliderを後からつけても判定できるので便利です。

public abstract class CEnemy : MonoBehaviour
{
protected TagEnemyStatus EnemyStatus;
protected CBulletController BulletController;
protected Animator _Animator;
protected bool BulletDischarge = false; // 弾幕発射フラグ
protected int Cnt = 0;
public int Life = 60;
public int Wait = 100;
protected float VX = 0.0f, VY = 0.0f;
void Start()
{
_Animator = GetComponent<Animator>();
switch(EnemyStatus.BulletPattern)
{
case 0:
BulletController = new CAimBulletController(
EnemyStatus.BulletType, EnemyStatus.BulletColor, EnemyStatus.BulletScriptType);
break;
} 
}
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.gameObject.tag == "Shot")
{
Life -= collision.GetComponent<CShot>().ShotPower;
if (Life <= 0)
{
Destroy(gameObject);
}
Destroy(collision.gameObject);
}
}
}

そして、いよいよ実際に敵を生成する方法です。ポイントはpublic static void Newというスタティック関数で自分自身のクラスを生成するところです。これをコマンドパターンの敵出現コマンドを定義し呼び出すような形になります。

//移動パターン0 
//下がってきて停滞して上がっていく
public class CEnemy0 : CEnemy
{
void Update()
{
if (Cnt == 0)
{
VY = -3.0f;
}
if (Cnt == 100)
{
VY = 0.0f;
BulletDischarge = true;
}
if (Cnt == 100 + Wait)
{
VY = 3.0f;
BulletDischarge = false;
}
transform.position += new Vector3(VX, VY, 0.0f) * Time.deltaTime;
_Animator.SetFloat("H", VX);
if (BulletController != null && BulletDischarge)
{
BulletController.Move(transform.position);
}
Cnt++;
if (CUtility.IsOut(transform.position))
{
Destroy(gameObject);
}
}
public static void New(TagEnemyStatus enemy_status)
{
GameObject enemy_obj = Instantiate(enemy_status.EnemyObj, new Vector3(enemy_status.X, enemy_status.Y, 0), Quaternion.identity);
enemy_obj.AddComponent<CEnemy0>().EnemyStatus = enemy_status;
}
}

実際にコマンドパターンを使う流れ

  • CCommandManagerのLoadScriptでスクリプトの読み込み
  • 読み込んだスクリプトからコマンドリストにコマンドを追加していく
  • CGameManager.csのUpdate関数でRun関数を呼び出しコマンドリストに格納されたコマンドを実行していく。

CWaitCommandやCPlayCommandはそのままRun関数の中身を実行しているだけですが、敵を生成するコマンドだけは特殊なので、詳しく説明します。

先ほどEnemyクラスで定義したNew関数をデリゲートを使って、EnemyFunc関数の配列に入れてゲーム初期化時にEnemyFunc[i]の様にCEnemyCreateCommandに登録して、Run()関数のCreateEnemyFuncを呼び出すことで、指定したCEnemyの派生クラスのNew関数を呼んで、好きなタイプの敵キャラを生成することができます。

デリゲート

C++でいうところの関数ポインタです。デリゲートを使うことによって関数を入れておける変数のようなもので、間接的に呼び出すことができるようになります。
delegate 戻り値の型 メソッド型名(引数の型, …); で定義できます。

詳しくは以下の説明がわかりやすいと思います。

public delegate void CREATE_ENEMY_FUNC(TagEnemyStatus enemy_status);
public class CCommandManager
{
private CREATE_ENEMY_FUNC[] EnemyFunc =
{
CEnemy0.New,
CEnemy1.New,
CEnemy2.New,
CEnemy3.New,
CEnemy4.New,
CEnemy5.New
};
}
public class CEnemyCreateCommand : ICommand
{
private TagEnemyStatus EnemyStatus;
CREATE_ENEMY_FUNC CreateEnemyFunc;
public CEnemyCreateCommand(CREATE_ENEMY_FUNC func, TagEnemyStatus enemy_status)
{
CreateEnemyFunc = func;
EnemyStatus = enemy_status;
}
public void Run()
{
CreateEnemyFunc(EnemyStatus);
}
};

弾幕を生成するクラス

Enemyを生成する方法までは説明しました。実はこのEnemy生成時にEnemyStatusの中に弾幕を生成するためのパラメーターが入ってます。その情報を見て弾幕の生成パターンや弾の種類、色などを判別します。

具体的にはCEnemyクラスの中にCBulletControllerクラスにMove関数を定義してやって、弾幕の生成パターンの情報を見て、クラスを入れ替えることによって、同じMove関数でも違う実行結果が得られるようになります。これを関数のオーバーライド(上書き)といいます。上書きされる基底クラスにはvirtualのキーワードを派生クラスの方には、overrideのキーワードを使います。

そしてCEnemyで定義されているBulletDischargeというbool型変数がtrueの時はCBulletControllerのMove関数を呼び出し、flaseの時は呼び出さない仕様にしています。

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public abstract class CBulletController
{
protected int Cnt, BulletType, ScriptType, BulletColor;
protected float Speed, Angle;
protected CGameManager GameManager;
public CBulletController(int bullet_type, int bullet_color, int script_type, float speed, float angle)
{
Cnt = 0;
BulletType = bullet_type; 
BulletColor = bullet_color;
ScriptType = script_type;
Speed = speed;
Angle = angle;
GameManager = GameObject.Find("GameManager").GetComponent<CGameManager>();
}
public virtual void Move(Vector3 pos)
{
}
}
public class CAimBulletController : CBulletController
{
public CAimBulletController(int bullet_type, int bullet_color, int script_type) : base(bullet_type, bullet_color, script_type, 3.0f, 0.0f)
{
}
public override void Move(Vector3 pos)
{
if (Cnt % 24 == 0)
{
GameObject PlayerObj = GameObject.FindGameObjectWithTag("Player");
if (PlayerObj != null)
{
Vector3 player_pos = PlayerObj.transform.position;
Angle = Mathf.Atan2(player_pos.y - pos.y, player_pos.x - pos.x);
}
else // プレイヤーが見つからなければ下に発射
{
Angle = Mathf.PI+(Mathf.PI/2);
}
CSoundPlayer.PlaySound("enemy_shot", true);
GameManager.BulletFactory[BulletType].CreateBullet(pos, BulletColor, ScriptType, Speed, Angle);
}
Cnt++;
}
}

そして、コマンドパターンで1番重要なCCommandManager.csのすべてを載せておきます。

using JetBrains.Annotations;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using UnityEngine;
public delegate void CREATE_ENEMY_FUNC(TagEnemyStatus enemy_status);
public interface ICommand
{
void Run();
};
public class CCommandManager
{
private List<ICommand> Command = new List<ICommand>();
private CREATE_ENEMY_FUNC[] EnemyFunc =
{
CEnemy0.New,
CEnemy1.New,
CEnemy2.New,
CEnemy3.New,
CEnemy4.New,
CEnemy5.New,
CEnemy6.New,
CEnemy7.New,
CEnemy8.New,
CEnemy9.New,
CEnemy10.New,
};
string[] EnemyName =
{
"enemy0",
"enemy1",
"enemy2",
"enemy3",
"enemy4",
"enemy5",
"enemy6",
"enemy7",
"enemy8",
"enemy9",
"enemy10",
};
private int CommandIndex { set; get; }
public float WaitTime { set; get; }
public void Initalize()
{
Command.Clear();
CommandIndex = 0;
WaitTime = 0.0f;
}
public void Run()
{
if (WaitTime > 0)
{
WaitTime -= Time.deltaTime;
}
else
{
WaitTime = 0;
}
while (CommandIndex < Command.Count && WaitTime == 0)
{
Command[CommandIndex].Run();
CommandIndex++;
}
}
public bool LoadScript(string file_name)
{
CommandIndex = 0;
WaitTime = 0;
bool comment = false;
int cnt = 0; // 行数カウント
foreach (string line in File.ReadLines(file_name))
{
if (string.IsNullOrWhiteSpace(line)) continue;
if (line.Substring(0, 2) == "//") continue;
System.StringSplitOptions option = System.StringSplitOptions.RemoveEmptyEntries;
string[] param = line.Split(new char[] { ',', ' ' }, option);
if (param.Length <= 0) continue;
if (param[0] == "*/") comment = false;
else if (param[0] == "/*") comment = true;
if (comment) continue;
List<string> error_string = new List<string>();
if (param[0] == "enemy")
{
if (param.Length < 2)
{
error_string.Add($"{file_name}{cnt}行目のenemyコマンドのパラメーターが足りません");
continue;
}
for (int j = 0; j < EnemyName.Length; j++)
{
if (param[1] == EnemyName[j])
{
TagEnemyStatus enemy_status = new TagEnemyStatus();
for (int k = 2; k < param.Length - 1; k += 2)
{
if(param[k] == "obj")
{
enemy_status.EnemyObj = CGameManager.ResourcesLoader.GetObjectHandle(param[k + 1]);
}
else if (param[k] == "ex")
{
// 初期値から最終値の倍率を変更する
// num 現在の数値(sa~eaの値を入れる)
// sa 変化前の初期値
// ea 変化前の最終値
// sb 変化後の初期値
// eb 変化後の最終値
// 使用例 10~100までの変化を0~1までの変化に倍率を変更する
// cout << ChangeRate( 10, 10, 100, 0, 1 ) << endl;
float sax = 0.0f;
float eax = 100.0f;
float sbx = -6.5f;
float ebx = 2.5f;
//print(CUtility.ChangeRate(100, sax, eax, sbx, ebx));
enemy_status.X = CUtility.ChangeRate(float.Parse(param[k + 1]), sax, eax, sbx, ebx);
}
else if (param[k] == "ey")
{
float say = 0.0f;
float eay = 100.0f;
float sby = 6.0f;
float eby = -5.0f;
//print(CUtility.ChangeRate(100, say, eay, sby, eby));
enemy_status.Y = CUtility.ChangeRate(float.Parse(param[k + 1]),say, eay, sby, eby); 
}
else if (param[k] == "life")
{
enemy_status.Life = int.Parse(param[k + 1]);
}
else if (param[k] == "w_time")
{
enemy_status.WaitTime = int.Parse(param[k + 1]);
}
else if (param[k] == "bl_pat")
{
enemy_status.BulletPattern = int.Parse(param[k + 1]);
}
else if (param[k] == "bl_type")
{
enemy_status.BulletType = int.Parse(param[k + 1]);
}
else if (param[k] == "bl_col")
{
enemy_status.BulletColor = int.Parse(param[k + 1]);
}
else if (param[k] == "bl_int")
{
enemy_status.BulletInterval = int.Parse(param[k + 1]);
}
else if (param[k] == "gr_type")
{
enemy_status.GraphType = int.Parse(param[k + 1]);
}
}
Command.Add(new CEnemyCreateCommand(EnemyFunc[j], enemy_status));
}
}
}
else if (param[0] == "wait")
{
if (param.Length < 2)
{
error_string.Add($"{file_name}{cnt}行目のwaitコマンドのパラメーターが足りません");
continue;
}
Command.Add(new CWaitCommand(this, float.Parse(param[1])));
}
else if (param[0] == "play")
{
if (param.Length < 2)
{
error_string.Add($"{file_name}{cnt}行目のwaitコマンドのパラメーターが足りません");
continue;
}
Command.Add(new CPlayCommand(param[1]));
}
else if (param[0] == "fadeout")
{
if (param.Length < 4)
{
error_string.Add($"{file_name}{cnt}行目のwaitコマンドのパラメーターが足りません");
continue;
}
float start_val = float.Parse(param[1]);
float end_val = float.Parse(param[2]);
float end_time = float.Parse(param[3]);
Command.Add(new CFadeOutCommand(start_val, end_val, end_time));
}
}
return true;
}
}
public class CEnemyCreateCommand : ICommand
{
private TagEnemyStatus EnemyStatus;
CREATE_ENEMY_FUNC CreateEnemyFunc;
public CEnemyCreateCommand(CREATE_ENEMY_FUNC func, TagEnemyStatus enemy_status)
{
CreateEnemyFunc = func;
EnemyStatus = enemy_status;
}
public void Run()
{
CreateEnemyFunc(EnemyStatus);
}
};
public class CWaitCommand : ICommand
{
private CCommandManager CommandManager;
private float WaitTime;
public CWaitCommand(CCommandManager game_control, float wait_time)
{
CommandManager = game_control;
WaitTime = wait_time;
}
public void Run()
{
CommandManager.WaitTime = WaitTime;
}
};
public class CPlayCommand : ICommand
{
string BGMName;
public CPlayCommand(string bgm_name)
{
BGMName = bgm_name;
}
public void Run()
{
CSoundPlayer.PlaySound(BGMName, false);
}
};
public class CFadeOutCommand : MonoBehaviour, ICommand
{
float StartVal, EndVal, EndTime;
public CFadeOutCommand(float start_val, float end_val, float end_time)
{
StartVal = start_val;
EndVal = end_val;
EndTime = end_time;
}
public void Run()
{
//fadeout 1 0 5
CSoundPlayer.CallSetFadeTimer(StartVal, EndVal, EndTime);
}
}

以下実際に読むテキストの内容です。

play 0_0 // 0_0を再生
wait 1 // 1秒待つ
enemy enemy1 obj Enemy ex 50 ey 0 hit_s 12 life 5 gr_type 0 w_time 60 bl_pat 0 bl_type 1 bl_col 0

先頭にコマンド名 値という内容になってます。
コマンド名はwait、値は1

  • enemyはコマンド名
  • enemy1は敵の種類
  • objはInstancelateで生成するEnemyの種類
  • exは敵を生成するX座標
  • eyは敵を生成するY座標
  • hit_sは敵の当たり判定
  • lifeは敵の生命力
  • gr_typeは弾のグラフィックのタイプ
  • w_timeは停止する敵の場合、停止する時間
  • bl_patは弾幕生成の種類(CBulletControllerの種類を決定)
  • bl_typeは弾の画像のタイプ
  • bl_colは弾の色

ゲーム全体の処理をするCGameManager

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CGameManager : MonoBehaviour
{
public static CResourcesLoader<GameObject> ResourcesLoader = new CResourcesLoader<GameObject>();
public class CBulletFactory
{
public GameObject Bullet;
public float Radius;
public string[] SpriteName;
public Sprite[] BulletSprite;
public bool ColliderType;
float SizeX, SizeY;
public CBulletFactory(string[] sprite_name, bool collider_type, float radius=0.5f,float size_x=0.5f, float size_y = 0.5f)
{
Radius = radius;
SpriteName = sprite_name;
BulletSprite = new Sprite[SpriteName.Length];
ColliderType = collider_type;
SizeX = size_x;
SizeY = size_y;
}
public void Load()
{
for (int i = 0; i < SpriteName.Length-1; ++i)
{
BulletSprite[i] = CUtility.GetSprite(SpriteName[0], SpriteName[i + 1]);
}
}
public void CreateBullet(Vector3 pos, int color, int script_type, float speed, float angle) 
{
GameObject newParent = new GameObject("Empty");
Bullet = Instantiate(newParent, pos, Quaternion.identity);
Bullet.tag = "Bullet";
SpriteRenderer sr = Bullet.AddComponent<SpriteRenderer>();
sr.sprite = BulletSprite[color];
sr.sortingLayerName = "Bullet";
Bullet.transform.localScale = new Vector3(0.7f, 0.7f, 0.7f);
switch(script_type)
{
case 0:
Bullet.AddComponent<CBullet>().SetParam(speed, angle);
break;
} 
if (ColliderType)
{
CircleCollider2D cc = Bullet.AddComponent<CircleCollider2D>();//.radius = SizeX;
cc.radius = Radius;
cc.isTrigger = true;
}
else
{
BoxCollider2D bc = Bullet.AddComponent<BoxCollider2D>();//.size = new Vector2(SizeX, SizeY);
bc.size = new Vector2(SizeX, SizeY);
bc.isTrigger = true;
}
Destroy(newParent);
}
}
public CBulletFactory[] BulletFactory = new CBulletFactory[]
{
new CBulletFactory(new string[]{ "img/bullet/b0","b0_0","b0_1","b0_2","b0_3","b0_4" }, true),
new CBulletFactory(new string[]{ "img/bullet/b1","b1_0","b1_1","b1_2","b1_3","b1_4","b1_5" },true),
new CBulletFactory(new string[]{ "img/bullet/b2","b2_0","b2_1" ,"b2_2" ,"b2_3" ,"b2_4" ,"b2_5" ,"b2_6" ,"b2_7" ,"b2_8" ,"b2_9"},false),
new CBulletFactory(new string[]{ "img/bullet/b3","b3_0","b3_1" ,"b3_2" ,"b3_3" ,"b3_4" },false),
new CBulletFactory(new string[]{ "img/bullet/b4","b4_0","b4_1" ,"b4_2" ,"b4_3" ,"b4_4" ,"b4_5" ,"b4_6" ,"b4_7" ,"b4_8" ,"b4_9"},true),
new CBulletFactory(new string[]{ "img/bullet/b5","b5_0","b5_1" ,"b5_2" },false),
new CBulletFactory(new string[]{ "img/bullet/b6","b6_0","b6_1" ,"b6_2"},false),
new CBulletFactory(new string[]{ "img/bullet/b7","b7_0","b7_1" ,"b7_2" ,"b7_3" ,"b7_4" ,"b7_5" ,"b7_6" ,"b7_7" ,"b7_8" ,"b7_9" },true),
new CBulletFactory(new string[]{ "img/bullet/b8","b8_0","b8_1" ,"b8_2" ,"b8_3" ,"b8_4" ,"b8_5" ,"b8_6" ,"b8_7" ,"b8_8" },false),
new CBulletFactory(new string[]{ "img/bullet/b9","b9_0","b9_1" ,"b9_2" },false),
new CBulletFactory(new string[]{ "img/bullet/b10","b10_0","b10_1" ,"b10_2" ,"b10_3" ,"b10_4" ,"b10_5" ,"b10_6" ,"10_7" },true),
new CBulletFactory(new string[]{ "img/bullet/b11","b11_0","b11_1" ,"b11_2" ,"b11_3" ,"b11_4" ,"b11_5" ,"b11_6" ,"11_7" },true),
new CBulletFactory(new string[]{ "img/bullet/b12","b12_0","b12_1" ,"b12_2" ,"b12_3" ,"b12_4" ,"b12_5" ,"b12_6" ,"b12_7" ,"b12_8" ,"b12_9" },true),
new CBulletFactory(new string[]{ "img/bullet/b13","b13_0","b13_1" ,"b13_2" ,"b13_3" ,"b13_4" ,"b13_5" ,"b13_6" ,"b13_7" ,"b13_8" ,"b13_9" },true),
new CBulletFactory(new string[]{ "img/bullet/b14","b14_0","b14_1" ,"b14_2" ,"b14_3" },true),
new CBulletFactory(new string[]{ "img/bullet/b15","b15_0","b15_1" ,"b15_2" },true),
new CBulletFactory(new string[]{ "img/bullet/_b6","_b6_0","_b6_1","_b6_2","_b6_3","_b6_4","_b6_5","_b6_6","_b6_7","_b6_8","_b6_9" },false),
new CBulletFactory(new string[]{ "img/bullet/l0", "l0_0", "l0_1" },false),
new CBulletFactory(new string[]{ "img/bullet/l0_moto", "l0_moto_0", "l0_moto_1" },true),
};
CCommandManager CommandManager = new CCommandManager();
void Start()
{
for (int i = 0; i < BulletFactory.Length; ++i)
{
BulletFactory[i].Load();
}
// Prefabsフォルダから、すべてのGameObjectを読み込む
if (!ResourcesLoader.LoadAllObjects("Prefabs"))
{
print("Prefabsファイル読み込みに失敗しました");
}
// Soundフォルダからすべての音声ファイルを読み込む
if (!CSoundPlayer.LoadAllSounds("se"))
{
print("seファイル読み込みに失敗しました");
}
if (!CSoundPlayer.LoadAllSounds("music"))
{
print("seファイル読み込みに失敗しました");
}
string file_name = Application.dataPath + @"\Scenes\Stage\stage1.txt";
CommandManager.Initalize();
CommandManager.LoadScript(file_name);
}
void Update()
{
CommandManager.Run();
}
}

敵やその他のコマンドについては以上です。

CUtility.csに追加した関数

ある数をa~bまで変化するのをc~dの数に変換する関数です。これは敵の位置をX軸(-6.5f~2.5f)までY軸(6.0f~-5.0f)までというのがわかりずらいので、スクリプトに書くときに0~100までの間の数字で、指定できるようにするために追加しました。

// 初期値から最終値の倍率を変更する
// num 現在の数値(sa~eaの値を入れる)
// sa 変化前の初期値
// ea 変化前の最終値
// sb 変化後の初期値
// eb 変化後の最終値
// 使用例 10~100までの変化を0~1までの変化に倍率を変更する
// cout << ChangeRate( 10, 10, 100, 0, 1 ) << endl;
static public float ChangeRate(float num, float sa, float ea, float sb, float eb)
{
num = Mathf.Clamp(num, sa, ea);
float a = (sb - eb) / (sa - ea);
float b = sb - sa * a;
return a * num + b;
}

フェードアウトについての注意点

今回BGMのフェードアウトを行う関数CallSetFadeTimerを実装するに当たって、普通にStartCoroutine(SetFadeTimer(start_val, end_val, end_time)と書くだけではNullPointerExceptionになってしまうので、以下のような対策をしました。

 public static void CallSetFadeTimer(float start_val, float end_val, float end_time)
{
//AddComponentでオブジェクトを生成
CSoundPlayer sound_player = (new GameObject("sound_player_obj")).AddComponent<CSoundPlayer>();
//コルーチン呼び出し(この場合は正常に実行できる)
sound_player.StartCoroutine(SetFadeTimer(start_val, end_val, end_time));
}

プロジェクトのダウンロードはこちらから

実行結果