Unity-DOTSで作る2D弾幕シューティングの例の三回目です。
敵/弾/銃のデータ構造について
という感じで弾を撃つわけですが、弾を撃つにあたって必要なIComponentDataの定義をしていきます。
基本的なルールとして定めているのは下記のようなものです。
- 敵は銃を持つ
- 敵は複数の銃を持つ場合がある
- 銃は敵を親とし、親に追従して動く
- 銃は座標のオフセット値を持つ(敵の絵に合わせて射出点を変えたい)
- 銃は弾を撃つ
これを下記のようなIComponentDataで表現しています。
public struct HasParent : IComponentData {
public Entity entity;
public float xOffset;
public float yOffset;
}
public struct Gunner : IComponentData
{
public float angle;
public bool isTriggered;
public bool isNoInterval;
}
public struct Enemy : IComponentData {
public int explosionMaterialId;
public bool triggered;
public bool damaged;
public int verseId;
public bool isAppeared;
}
public struct Gun : IComponentData
{
public int bulletId;
public int damage;
public float bulletSpeedDefault;
public float bulletSpeedRandomize;
public float bulletAcceralation;
public int shotWays;
public int shotBlazeCount;
public int shotSimultaneousCount;
public int shotTimeInterval;
public int shotBlazeInterval;
public float shotAngle;
public float shotAngularSpeed;
public float shotAngularRandomize;
public int shotAngularEaseType;
public bool shotAngluarReverse;
public float shotBulletAngleInterval;
public float shotBulletPlaceInterval;
public float shotWaysAngleInterval;
public float shotOffsetRadius;
public int colliderKind;
public int owner;
public float direction;
public float bulletSpeed;
public int counter;
public int magazine;
public bool isEaseReversing;
public bool isTriggered;
public bool isNoInterval;
}
Gun
のパラメータが死ぬほど多いですね。
調整できる箇所は多いに越したことはないからしょうがないですね。
例えば二つの銃を持つ敵を作成する、という場合、下記のような手順を踏みます。
- 「敵である」かつ「射手である」というEntityを作成します。
- 「親を持つ」かつ「銃である」というEntityを二つ作成し、1. のEntityの参照を持たせておきます
- 「銃である」Entityは、それぞれ連射間隔や親が射撃可能な状態であるかということを処理しつつ、「弾である」Entityを作成していきます。
敵のEntityを作成する箇所のコードを抜粋して記載します。
EnemyStatusStruct enemyStatus = FetchEnemyStatus(qSpawn);
int[] componentIds = enemyStatus.componentIds;
if (componentIds.Length == 0)
return;
List<ComponentType> typeList = new List<ComponentType>();
for (int j = 0; j < componentIds.Length; j++)
{
typeList.Add(SettingManager.ComponentDataTypeDic[componentIds[j]]);
}
typeList.Add(typeof(Enemy));
typeList.Add(typeof(Gunner));
typeList.Add(typeof(RenderSprite));
typeList.Add(typeof(Drifting));
typeList.Add(typeof(Durability));
typeList.Add(typeof(Scorable));
typeList.Add(typeof(Destructive));
typeList.Add(typeof(NeedMaterialSetting));
typeList.Add(typeof(MotionData));
typeList.Add(typeof(LinearMotion));
typeList.Add(typeof(CircularMotion));
typeList.Add(typeof(SpringMotion));
typeList.Add(typeof(AttractedMotion));
typeList.Add(typeof(Rotation));
typeList.Add(typeof(HasParent));
ComponentType[] types = typeList.ToArray();
EntityArchetype entityArchetype = entityManager.CreateArchetype(types);
Entity enemyEntity = entityManager.CreateEntity(entityArchetype);
// コンポーネントの初期値設定をする
for (int j = 0; j < types.Length; j++)
{
var type = types[j];
if (type == typeof(Translation))
{
entityManager.SetComponentData(enemyEntity, new Translation
{
Value = new Vector3(qSpawn.x, qSpawn.y, 0),
});
}
if (type == typeof(RenderSprite))
{
entityManager.SetComponentData(enemyEntity, new RenderSprite
{
materialId = enemyStatus.moveAnimationId,
speed = 10,
scaleCurrent = new float2(1, 1),
sortingOrder = SpriteRenderSystem.SORTING_ENEMY,
});
}
if (type == typeof(MotionData))
{
entityManager.SetComponentData(enemyEntity, new MotionData
{
Acceleration = new float3(2, 2, 0),
friction = new float3(1, 1, 0),
MaxSpeed = enemyStatus.maxSpeed,
});
}
if (type == typeof(Enemy))
{
entityManager.SetComponentData(enemyEntity, new Enemy
{
explosionMaterialId = enemyStatus.explosionAnimationId,
triggered = true,
verseId = qSpawn.verseId,
});
}
if (type == typeof(Durability)) entityManager.SetComponentData(enemyEntity, new Durability { value = enemyStatus.durability, });
if (type == typeof(Rotation)) entityManager.SetComponentData(enemyEntity, new Rotation { radian = 0.0f, });
if (type == typeof(Gunner)) entityManager.SetComponentData(enemyEntity, new Gunner { isTriggered = true, });
if (type == typeof(Scorable)) entityManager.SetComponentData(enemyEntity, new Scorable { value = enemyStatus.score, });
if (type == typeof(Destructive)) entityManager.SetComponentData(enemyEntity, new Destructive { Is = false, materialId = enemyStatus.explosionAnimationId, });
if (type == typeof(NeedMaterialSetting)) entityManager.SetComponentData(enemyEntity, new NeedMaterialSetting { Is = true, });
}
private EnemyStatusStruct[] mEnemyStatus;
private EnemyStatusStruct FetchEnemyStatus(QSpawn data)
{
int hit = -1;
for (int i = 0; i < mEnemyStatus.Length; i++)
{
if (data.enemyId == mEnemyStatus[i].enemyId)
{
hit = i;
break;
}
}
if (hit == -1) return new EnemyStatusStruct { };
else return mEnemyStatus[hit];
}
ここでやっているのは下記のようなことです。
- 敵Entityの
ComponentType
を選定して、アーキタイプを作る - そのアーキタイプでEntityを作る
- それぞれのコンポーネントに初期設定をする
ComponentTypeの設定について、ベタ書きしてあるのはすべての敵に共通するComponentTypeになります。
敵ごとに違ったアーキタイプを持ちたいという場合もあるかと思うので、①で作った外部DBに「敵が個別に持っているコンポーネント」の一覧テーブルを作成しておき、そのデータを起動時に読み込んでアーキタイプに追加する、というようなことをやっています。
ステージキューの箇所で、敵のスポーンと武装が別のイベントになっていたのは、一つの敵に対して複数の銃を設定したかったためです。
何も銃を持っていない敵を出現させてから、それぞれの銃を個別に敵に載せていくというようなイメージになります。
「親を持つ」という表現は銃に限らず「敵を親に持つ敵」も可能なので、多関節のようなものもこの形式で表現できるかと思います。
プレイヤーに関しては、「プレイヤーである」かつ「射手である」という形で表現できるので、同じくGunner
のIComponentDataが使えます。