10 - 衝突判定
Collision System
QuantumはECSベースのため、衝突イベントはエンティティ毎ではなく、システムが購読できるグローバルなシグナル毎になります。パフォーマンスと利便性を考慮すると、あらゆる衝突を検知するシステムを1つ用意して、型によって衝突をフィルタリングし、対応するシグナルを発生させます。
衝突システムを実装する前に、新しいAsteroidsAsteroid.qtn
を作成し、以下のコードを追加してください。
C#
component AsteroidsAsteroid
{
}
この空タグコンポーネントは、AsteroidsLarge
プレハブに追加します。
次に、新しいC#スクリプトを作成し、AsteroidsCollisionSystem
と名付けます。
そして、以下のコードを追加してください。
C#
using UnityEngine.Scripting;
namespace Quantum.Asteroids
{
[Preserve]
public unsafe class AsteroidsCollisionsSystem : SystemSignalsOnly, ISignalOnCollisionEnter2D
{
public void OnCollisionEnter2D(Frame f, CollisionInfo2D info)
{
// Projectile is colliding with something
if (f.Unsafe.TryGetPointer<AsteroidsProjectile>(info.Entity, out var projectile))
{
if (f.Unsafe.TryGetPointer<AsteroidsShip>(info.Other, out var ship))
{
// Projectile Hit Ship
}
else if (f.Unsafe.TryGetPointer<AsteroidsAsteroid>(info.Other, out var asteroid))
{
// projectile Hit Asteroid
}
}
// Ship is colliding with something
else if (f.Unsafe.TryGetPointer<AsteroidsShip>(info.Entity, out var ship))
{
if (f.Unsafe.TryGetPointer<AsteroidsAsteroid>(info.Other, out var asteroid))
{
// Asteroid Hit Ship
}
}
}
}
}
このコードは、グローバルな衝突シグナルを購読して、エンティティの型によってフィルタリングします。
次に、AsteroidsCollisionSignals.qtn
を作成して、以下のシグナルを追加してください。
C#
signal OnCollisionProjectileHitShip(CollisionInfo2D info, AsteroidsProjectile* projectile, AsteroidsShip* ship);
signal OnCollisionProjectileHitAsteroid(CollisionInfo2D info, AsteroidsProjectile* projectile, AsteroidsAsteroid* asteroid);
signal OnCollisionAsteroidHitShip(CollisionInfo2D info, AsteroidsShip* ship, AsteroidsAsteroid* asteroid);
弾と船との衝突
弾に関するインタラクションはAsteroidsProjectileSystem
に実装します。システムを開き、以下のコードを追加してください。
C#
public void OnCollisionProjectileHitShip(Frame f, CollisionInfo2D info, AsteroidsProjectile* projectile, AsteroidsShip* ship)
{
if (projectile->Owner == info.Other)
{
info.IgnoreCollision = true;
return;
}
f.Destroy(info.Entity);
}
コードの最初の部分で、自分自身の船が発射した弾との衝突を無視しています。そして2つ目の部分で、それ以外の弾を削除します。ここから、弾に当たった敵の船を破壊するロジックを追加することもできます。
弾とアステロイドとの衝突
弾がアステロイドに当たった場合は、アステロイドを複数の小さなアステロイドに分裂させます。
AsteroidsWaveSpawnerSystem
には既にアステロイドをスポーンする関数があるため、シグナルによって調整すれば、子アステロイドのスポーンにも再利用できます。
AsteroidsAsteroid.qtn
に以下のシグナルを追加して、コンポーネントにはChildAsteroid
を追加します。
C#
component AsteroidsAsteroid
{
asset_ref<EntityPrototype> ChildAsteroid;
}
signal SpawnAsteroid(AssetRef<EntityPrototype> childPrototype, EntityRef parent);
ChildAsteroid
フィールドは実行時に変更しないため、技術的には設定ファイルを使用するのに適しています。しかし、フィールドを1つだけ持つコンポーネントのために設定ファイルを作成して紐づけるのはやりすぎで、パフォーマンスが落ちたりコードが複雑になってしまうだけです。
AsteroidsWaveSpawnerSystem
を開き、ISignalSpawnAsteroid
インターフェースを実装してください。
SpawnAsteroid
関数にEntityRef parent
パラメーターを追加して、正しくシグナルを実装できるようにします。SpawnAsteroidWave
関数のSpawnAsteroid()
を呼び出している部分では、親エンティティとしてEntityRef.None
を渡します。
最後にこのコードを、
C#
asteroidTransform->Position = GetRandomEdgePointOnCircle(f, config.AsteroidSpawnDistanceToCenter);
以下のコードに置き換えてください。
C#
if (parent == EntityRef.None)
{
asteroidTransform->Position = GetRandomEdgePointOnCircle(f, config.AsteroidSpawnDistanceToCenter);
}
else
{
asteroidTransform->Position = f.Get<Transform2D>(parent).Position;
}
これで親エンティティが与えられた場合は、ランダムな位置ではなく親の位置からアステロイドがスポーンされるようになりました。
AsteroidsProjectileSystem
に戻り、以下のコードを追加してください。
C#
public void OnCollisionProjectileHitAsteroid(Frame f, CollisionInfo2D info, AsteroidsProjectile* projectile, AsteroidsAsteroid* asteroid)
{
if (asteroid->ChildAsteroid != null)
{
f.Signals.SpawnAsteroid(asteroid->ChildAsteroid, info.Other);
f.Signals.SpawnAsteroid(asteroid->ChildAsteroid, info.Other);
}
f.Destroy(info.Entity);
f.Destroy(info.Other);
}
そして、AsteroidsProjectileSystem
で2つのシグナルを受け取れるように、ISignalOnCollisionProjectileHitShip
とISignalOnCollisionProjectileHitAsteroid
インターフェースを実装してください。
C#
public unsafe class AsteroidsProjectileSystem : SystemMainThreadFilter<AsteroidsProjectileSystem.Filter>, ISignalAsteroidsShipShoot, ISignalOnCollisionProjectileHitShip, ISignalOnCollisionProjectileHitAsteroid
船とアステロイドとの衝突
AsteroidsShipSystem
を開き、ISignalOnCollisionAsteroidHitShip
インターフェースを実装します。
また、以下のコードを該当する関数に追加します。
C#
public void OnCollisionAsteroidHitShip(Frame f, CollisionInfo2D info, AsteroidsShip* ship, AsteroidsAsteroid* asteroid)
{
f.Destroy(info.Entity);
}
これで船がアステロイドに衝突すると、単に船が破壊されるようになります。完全なゲームループでは、船をリスポーンするロジックを追加したり、すべての船が破壊されたらスコアを出してゲームを終了するような調整も入るでしょう。
AsteroidsCollisionsSystem
に戻り、以下のようにシグナルを発生させましょう。
C#
// Projectile Hit Ship
を
C#
f.Signals.OnCollisionProjectileHitShip(info, projectile, ship);
C#
// Projectile Hit Asteroid
を
C#
f.Signals.OnCollisionProjectileHitAsteroid(info, projectile, asteroid);
C#
// Asteroid Hit Ship
を
C#
f.Signals.OnCollisionAsteroidHitShip(info, ship, asteroid);
Unityに戻り、AsteroidsSystemConfig
にAsteroidsCollisionSystem
を追加します。
子アステロイドのプレハブ
シーンにAsteroidLarge
プレハブをドラッグして、EntityPrototype
のCircle Radius
を0.5
に調整します。子要素のスケールは(0.6, 0.6, 0.6)
にします。プレハブの名前をAsteroidSmall
に変更して、Resources
フォルダーにドラッグして戻し、ポップアップではPrefab Variant
を選択して、小さなアステロイドのプレハブを作成しましょう。その後、シーンからプレハブを削除します。
そして、AsteroidLarge
プレハブのEntityPrototype
のChild Asteroid
フィールドに、AsteroidSmall VariantEntityPrototype
をドラッグします。
衝突コールバックの有効化
コード側の衝突判定の設定は完了しました。しかしQuantumのデフォルトではパフォーマンス上の理由から、エンティティ間の衝突コールバックは呼び出されません。EntityPrototype
のPhysicsCollider2D
のCallback Flags
を変更して、これらを有効にすることができます。
AsteroidLarge
・AsteroidsProjectile
・AsteroidsShip
プレハブのOnDynamicCollisionEnter
を有効にしてください(AsteroidSmall
はAsteroidLarge
のPrefab Variantなので、手動で調整する必要はありません)。
ゲームを再生して、以下のような衝突が発生するか確認してみましょう。
- 弾がアステロイドに当たると、アステロイドは破壊され、2つのアステロイドに分裂する。弾は削除される。
- 船がアステロイドに当たると、船は破壊される。
- 他の船を撃つと、その船は破壊される。
ウェーブの修正
現在は、最初のウェーブでのみアステロイドがスポーンします。AsteroidWaveSpawnerSystem
に以下の関数を追加して、これを修正します。
C#
public void OnRemoved(Frame f, EntityRef entity, AsteroidsAsteroid* component)
{
if (f.ComponentCount<AsteroidsAsteroid>() <= 1)
{
SpawnAsteroidWave(f);
}
}
さらにISignalOnComponentRemoved<AsteroidsAsteroid>
インターフェースを実装します。このシグナルは、コンポーネントがエンティティから削除された時に呼び出されます(エンティティが削除されると、すべてのコンポーネントも同様に削除されます)。
これでアステロイドゲームのゲームプレイメカニクスの核が完成しました。
