球と球のあたり判定
概要
各モデルを包括する球を使い、あたり判定を行います。今回のサンプルでは2つの球モデルの衝突判定を行っています。
動作環境
必須環境
対応 XNA バージョン |
|
対応プラットフォーム |
|
Windows 必須頂点シェーダ バージョン | 2.0 |
Windows 必須ピクセルシェーダ バージョン | 2.0 |
動作確認環境
プラットフォーム |
|
サンプルの操作方法
動作 | キーボード | Xbox 360 コントローラー | マウス | タッチ |
---|---|---|---|---|
球1の移動 | ↑↓←→ | 左スティック | 左ボタン & ドラッグ | - |
内容
あたり判定について
シューティングゲームやアクションゲームなどではキャラクターと弾が衝突したり、キャラクター同士がぶつかるなどさまざまな衝突が発生するため、それを判断するプログラムを記述する必要があります。プログラムではこのことを一般的にあたり判定(衝突判定)と呼びます。あたり判定は数学的にも物理的にも簡単なものから複雑なものまで様々なパターンが存在します。ゲームでは一般的にあたりの正確さよりも処理の負荷を減らすように作ることが多く、あたりに極端にずれがなければ正確でなくてもあまり問題になりません。
この Tips ではあたり判定としては最も一般的且つ、負荷の少ない「球と球」のあたり判定について説明しています。球と球なので3次元空間でのあたり判定のお話になりますが、2次元空間の円と円の衝突でもほとんど同じ処理で代用することが可能です。
球と球のあたり判定のしくみ
球と球のあたり判定にはそれぞれのモデルの「位置」とモデルの大きさを示す「半径」の2つのパラメータを使用します。この2つのパラメータによって当たっているかを判定するには、下の図を見ていただくとわかりやすいかと思います。
- P:モデルの位置 L:2点間の距離(P2-P1) R:球の半径
「2つのモデルの間の距離」を「L」とし「2つのモデルの半径の和」を「R」とした場合、「L < R」の場合は衝突していることになります。逆に「L > R」の場合は衝突していないことになります。「L = R」の衝突判定はどちらにとっても構いません。
フィールド
<summary>
モデル
</summary>
private Model model = null;
<summary>
モデルの基本包括球
</summary>
private BoundingSphere baseBoundingSphere = new BoundingSphere();
<summary>
球1の位置
</summary>
private Vector3 sphere1Position = new Vector3(0.0f, 0.0f, 0.0f);
<summary>
球2の位置
</summary>
private Vector3 sphere2Position = new Vector3(1.5f, 0.0f, -3.0f);
<summary>
球1の包括球
</summary>
private BoundingSphere sphere1BoundingSphere = new BoundingSphere();
<summary>
球2の包括球
</summary>
private BoundingSphere sphere2BoundingSphere = new BoundingSphere();
<summary>
衝突フラグ
</summary>
private bool isCollision = false;
あたり判定処理は自分でプログラムを書くこともできますが、XNA Framework には球のパラメータとあたり判定を簡単に扱うことができる構造体「BoundingSphere」があるのでそれを使ってみたいと思います。
読み込んだ Model クラスの各 ModelMesh には、モデルを包括する球情報「BoundingSphere」がすでに格納されているので、取り出し先のフィールド「baseBoundingSphere」を用意しています。
他には各球の位置、2つの球のあたり判定に使用する BoundingSphere、衝突のフラグを用意しています。
読み込み
// モデルを作成
this.model = this.Content.Load<Model>("Sphere");
// 包括球取得
this.baseBoundingSphere = this.model.Meshes[0].BoundingSphere;
// 各モデル用の包括球半径設定
this.sphere1BoundingSphere.Radius = this.baseBoundingSphere.Radius;
this.sphere2BoundingSphere.Radius = this.baseBoundingSphere.Radius;
Model の各 ModelMesh には BoundingSphere プロパティがあり、ModelMesh のモデルを包括している球情報を取得することができます。今回使用しているモデルは ModelMesh がひとつなので、最初のインデックスから包括球を baseBoundingSphere にコピーしています。
球の半径は固定なので、2つの BoundingSphere の Radius プロパティに半径を事前にセットしています。
あたり判定
// 衝突判定用の球を設定
this.sphere1BoundingSphere.Center =
this.sphere1Position + this.baseBoundingSphere.Center;
this.sphere2BoundingSphere.Center =
this.sphere2Position + this.baseBoundingSphere.Center;
// 衝突判定
this.isCollision =
this.sphere1BoundingSphere.Intersects(this.sphere2BoundingSphere);
球と球の衝突判定には BoundingSphere.Intersects メソッドを使用します。2つの BoundingSphere にはすでに半径をセットしてあるので、位置情報として球の位置と元の包括球の中心座標を加算した値を BoundingSphere.Center プロパティにセットしています。元の球の中心座標を加算しているのは、包括球の中心が原点とは限らないからです。
BoundingSphere.Intersects の引数にもう片方の BoundingSphere を指定すると衝突しているかしていないか bool で返ってくるのでそれを取得しています。このフラグは当たっているかを文字で描画する際に使用しています。
あたり判定の計算方法
XNA Framework には BoundingSphere という衝突判定で便利な構造体が用意されており、球だけではなく、レイ(線)やボックスといった違った形状との衝突判定も行うことができます。
しかし、球と球の衝突判定であれば、 BoundingSphere.Intersects を使わなくても単純な計算で判定することができます。
- 結果(あたっているか) = 球2の位置と球1の位置の距離 < 球1の半径 + 球2の半径
プログラムで書くと
this.isCollision = (this.sphere2Position - this.sphere1Position).Length() <
(this.sphere1BoundingSphere.Radius + this.sphere2BoundingSphere.Radius);
となります。(上の衝突判定処理は完全な球のように包括球の中心が原点であることを想定しているため、モデルの包括球の中心を考慮していません。考慮した場合は下のようになります)
this.isCollision = ((this.sphere2Position + this.baseBoundingSphere.Center) -
(this.sphere1Position + this.baseBoundingSphere.Center)).Length() <
(this.sphere1BoundingSphere.Radius + this.sphere2BoundingSphere.Radius);
全コード
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
#if WINDOWS_PHONE
using Microsoft.Xna.Framework.Input.Touch;
#endif
namespace CollisionSphereAndSphere
{
<summary>
ゲームメインクラス
</summary>
public class GameMain : Microsoft.Xna.Framework.Game
{
<summary>
グラフィックデバイス管理クラス
</summary>
private GraphicsDeviceManager graphics = null;
<summary>
スプライトのバッチ化クラス
</summary>
private SpriteBatch spriteBatch = null;
<summary>
スプライトでテキストを描画するためのフォント
</summary>
private SpriteFont font = null;
<summary>
モデル
</summary>
private Model model = null;
<summary>
モデルの基本包括球
</summary>
private BoundingSphere baseBoundingSphere = new BoundingSphere();
<summary>
球1の位置
</summary>
private Vector3 sphere1Position = new Vector3(0.0f, 0.0f, 0.0f);
<summary>
球2の位置
</summary>
private Vector3 sphere2Position = new Vector3(1.5f, 0.0f, -3.0f);
<summary>
球1の包括球
</summary>
private BoundingSphere sphere1BoundingSphere = new BoundingSphere();
<summary>
球2の包括球
</summary>
private BoundingSphere sphere2BoundingSphere = new BoundingSphere();
<summary>
衝突フラグ
</summary>
private bool isCollision = false;
<summary>
前回のマウスの状態
</summary>
private MouseState oldMouseState;
<summary>
GameMain コンストラクタ
</summary>
public GameMain()
{
// グラフィックデバイス管理クラスの作成
this.graphics = new GraphicsDeviceManager(this);
// ゲームコンテンツのルートディレクトリを設定
this.Content.RootDirectory = "Content";
#if WINDOWS_PHONE
// Windows Phone のデフォルトのフレームレートは 30 FPS
this.TargetElapsedTime = TimeSpan.FromTicks(333333);
// バックバッファサイズの設定
this.graphics.PreferredBackBufferWidth = 480;
this.graphics.PreferredBackBufferHeight = 800;
// フルスクリーン表示
this.graphics.IsFullScreen = true;
#endif
}
<summary>
ゲームが始まる前の初期化処理を行うメソッド
グラフィック以外のデータの読み込み、コンポーネントの初期化を行う
</summary>
protected override void Initialize()
{
// TODO: ここに初期化ロジックを書いてください
// コンポーネントの初期化などを行います
base.Initialize();
}
<summary>
ゲームが始まるときに一回だけ呼ばれ
すべてのゲームコンテンツを読み込みます
</summary>
protected override void LoadContent()
{
// テクスチャーを描画するためのスプライトバッチクラスを作成します
this.spriteBatch = new SpriteBatch(this.GraphicsDevice);
// フォントをコンテントパイプラインから読み込む
this.font = this.Content.Load<SpriteFont>("Font");
// モデルを作成
this.model = this.Content.Load<Model>("Sphere");
// 包括球取得
this.baseBoundingSphere = this.model.Meshes[0].BoundingSphere;
// 各モデル用の包括球半径設定
this.sphere1BoundingSphere.Radius = this.baseBoundingSphere.Radius;
this.sphere2BoundingSphere.Radius = this.baseBoundingSphere.Radius;
// あらかじめパラメータを設定しておく
foreach (ModelMesh mesh in this.model.Meshes)
{
foreach (BasicEffect effect in mesh.Effects)
{
// デフォルトのライト適用
effect.EnableDefaultLighting();
// ビューマトリックスをあらかじめ設定
effect.View = Matrix.CreateLookAt(
new Vector3(0.0f, 10.0f, 1.0f),
Vector3.Zero,
Vector3.Up
);
// プロジェクションマトリックスをあらかじめ設定
effect.Projection = Matrix.CreatePerspectiveFieldOfView(
MathHelper.ToRadians(45.0f),
(float)this.GraphicsDevice.Viewport.Width /
(float)this.GraphicsDevice.Viewport.Height,
1.0f,
100.0f
);
}
}
}
<summary>
ゲームが終了するときに一回だけ呼ばれ
すべてのゲームコンテンツをアンロードします
</summary>
protected override void UnloadContent()
{
// TODO: ContentManager で管理されていないコンテンツを
// ここでアンロードしてください
}
<summary>
描画以外のデータ更新等の処理を行うメソッド
主に入力処理、衝突判定などの物理計算、オーディオの再生など
</summary>
<param name="gameTime">このメソッドが呼ばれたときのゲーム時間</param>
protected override void Update(GameTime gameTime)
{
KeyboardState keyboardState = Keyboard.GetState();
MouseState mouseState = Mouse.GetState();
GamePadState gamePadState = GamePad.GetState(PlayerIndex.One);
// Xbox 360 コントローラ、Windows Phone の BACK ボタンを押したときに
// ゲームを終了させます
if (gamePadState.Buttons.Back == ButtonState.Pressed)
{
this.Exit();
}
float speed = 0.1f;
// 球1の位置を移動させる
if (gamePadState.IsConnected)
{
this.sphere1Position.X += gamePadState.ThumbSticks.Left.X * speed;
this.sphere1Position.Z -= gamePadState.ThumbSticks.Left.Y * speed;
}
if (keyboardState.IsKeyDown(Keys.Left))
{
this.sphere1Position.X -= speed;
}
if (keyboardState.IsKeyDown(Keys.Right))
{
this.sphere1Position.X += speed;
}
if (keyboardState.IsKeyDown(Keys.Down))
{
this.sphere1Position.Z += speed;
}
if (keyboardState.IsKeyDown(Keys.Up))
{
this.sphere1Position.Z -= speed;
}
if (mouseState.LeftButton == ButtonState.Pressed)
{
// 直前にマウスの左ボタンが押されていない場合は差分を0にする
if (this.oldMouseState.LeftButton == ButtonState.Released)
{
this.oldMouseState = mouseState;
}
this.sphere1Position += new Vector3((mouseState.X - this.oldMouseState.X) * 0.01f,
0,
(mouseState.Y - this.oldMouseState.Y) * 0.01f);
}
// マウスの状態記憶
this.oldMouseState = mouseState;
// 衝突判定用の球を設定
this.sphere1BoundingSphere.Center =
this.sphere1Position + this.baseBoundingSphere.Center;
this.sphere2BoundingSphere.Center =
this.sphere2Position + this.baseBoundingSphere.Center;
// 衝突判定
this.isCollision =
this.sphere1BoundingSphere.Intersects(this.sphere2BoundingSphere);
// 登録された GameComponent を更新する
base.Update(gameTime);
}
<summary>
描画処理を行うメソッド
</summary>
<param name="gameTime">このメソッドが呼ばれたときのゲーム時間</param>
protected override void Draw(GameTime gameTime)
{
// 画面を指定した色でクリアします
this.GraphicsDevice.Clear(Color.CornflowerBlue);
// 深度バッファを有効にする
this.GraphicsDevice.DepthStencilState = DepthStencilState.Default;
foreach (ModelMesh mesh in this.model.Meshes)
{
// 球1を描画
foreach (BasicEffect effect in mesh.Effects)
{
// ワールドマトリックス(位置指定)
effect.World = Matrix.CreateTranslation(this.sphere1Position);
}
mesh.Draw();
// 球2を描画
foreach (BasicEffect effect in mesh.Effects)
{
// ワールドマトリックス(位置指定)
effect.World = Matrix.CreateTranslation(this.sphere2Position);
}
mesh.Draw();
}
// スプライトの描画準備
this.spriteBatch.Begin();
// 衝突判定表示
this.spriteBatch.DrawString(this.font,
"IsCollision : " + this.isCollision,
new Vector2(30.0f, 30.0f), Color.White);
// スプライトの一括描画
this.spriteBatch.End();
// 登録された DrawableGameComponent を描画する
base.Draw(gameTime);
}
}
}