DXライブラリとC言語でゲーム制作。今回は3Dモデルを3D空間で動かす機能を実装してみる。マップとの当たり判定は後日。
3Dモデルの動かし方の基礎
3Dモデルを3D空間で自由に動かす場合、キーボードまたはゲームパッドの入力から移動後の座標を求めるのは変わらない。が、2Dゲームとは異なりカメラの方向を考慮する必要がある。
DXライブラリ置き場の3Dアクション基本のページを見ると3Dモデルの動かし方はおおざっぱに以下のようになる。
- カメラの座標と注視点からカメラ真正面方向およびカメラから真左方向のベクトルを作成、正規化する
- キーボードおよびゲームパッドの入力に沿って1で作成したベクトルを加算
- 2でできたベクトルに補正をかけて3Dモデルの座標を更新
- 3Dモデルの向きも合わせて更新
1のベクトルの作成が難しそうだが、カメラの座標から注視点までのベクトルからY座標の値を0にすればそのままカメラ真正面方向になるし、そのベクトルを半時計周りに90度回転させればカメラから真左方向のベクトルになる(上のDXライブラリ置き場では真左方向のベクトルは外積で求めている)。
なおジャンプの実装やマップとの当たり判定については後日実装する。まとめてやるとソースコードがかなりの量になってページがパンクしてしまうので…。
実装
では実装。今回は3Dモデルを動かすのが目的だがついでにモデルの向きやアニメーションも変化させる。けっこうな量になってしまったのでジャンプについては後日。
実装の前にBlenderなどでテキトーに下のような平面を作成してfbx出力し、mv1化しておく。これがないと実際に動いているかどうかがわからないので絶対に作ること。

今回以降カメラの座標や注視点を他のファイルで使う必要があるので Camera.cpp/h にカメラの座標と注視点を渡す関数を追加する。
Camera.cpp
//以下の関数を追加 //中略 //カメラの座標をゲットする(他のファイルから用) VECTOR Camera_GetEye() { return camera.Eye; } //カメラの注視点をゲットする(他のファイルから用) VECTOR Camera_GetTarget() { return camera.Target; }
Camera.h
//以下の関数を追加 VECTOR Camera_GetEye(); VECTOR Camera_GetTarget();
次にPlayer.cppに必要なコードを書いていく。まずプレイヤーの移動速度などの変数の定義とPlayerの構造体に以下の変数を追加する。
Player.cpp
//以下の定義・変数を追加 #include <math.h> typedef struct { //中略 VECTOR TargetMoveDirection; //モデルが向くべき方向 float Angle; //モデルが向いている方向 int Action; //現在のプレイヤーの行動 int PrevAction; //1F前のプレイヤーの行動 //中略 }player_t; const static float PLAYER_MOVE_SPEED = 0.2f; //プレイヤーの移動速度 const static float PLAYER_ANGLE_SPEED = 0.2f; //プレイヤーの角度変化速度 const static int PLAYER_IDLE = 0; //待機状態 const static int PLAYER_WALK = 1; //移動状態
今回の計算に三角関数関連のものを使う必要があるので <math.h> をインクルードしている。
TargetMoveDirection および Angle はプレイヤーの向き用の変数。TargetMoveDirection があるのはプレイヤーの向きを滑らかに変えるため。
Action および PrevAction はアニメーション用の変数。これにプレイヤーの行動状態(待機、移動など)を格納する。
そしてプレイヤーの移動処理を追加する。
//以下の処理を追加 //初期化 void Player_Initialize() { //中略 //アクションの初期化 player.Action = 0; //中略 } //更新 void Player_Update() { VECTOR UpMoveVec; // 方向ボタン「↑」を入力をしたときのプレイヤーの移動方向ベクトル VECTOR LeftMoveVec; // 方向ボタン「←」を入力をしたときのプレイヤーの移動方向ベクトル VECTOR MoveVec; // このフレームの移動ベクトル bool MoveFlag; // 移動したかどうかのフラグ( 1:移動した 0:移動していない ) VECTOR NowPos; //移動後の座標 // ルートフレームのZ軸方向の移動パラメータを無効にする { MATRIX LocalMatrix; // ユーザー行列を解除する MV1ResetFrameUserLocalMatrix(player.ModelHandle, 2); // 現在のルートフレームの行列を取得する LocalMatrix = MV1GetFrameLocalMatrix(player.ModelHandle, 2); // Z軸方向の平行移動成分を無効にする LocalMatrix.m[3][2] = 0.0f; // ユーザー行列として平行移動成分を無効にした行列をルートフレームにセットする MV1SetFrameUserLocalMatrix(player.ModelHandle, 2, LocalMatrix); } // プレイヤーの移動方向のベクトルを算出 { // 方向ボタン「↑」を押したときのプレイヤーの移動ベクトルはカメラの視線方向からY成分を抜いたもの UpMoveVec = VSub(Camera_GetTarget(),Camera_GetEye()); UpMoveVec.y = 0.0f; // 方向ボタン「←」を押したときのプレイヤーの移動ベクトルは上を押したときの方向ベクトルとY軸のプラス方向のベクトルに垂直な方向 LeftMoveVec = VCross(UpMoveVec, VGet(0.0f, 1.0f, 0.0f)); // 二つのベクトルを正規化( ベクトルの長さを1.0にすること ) UpMoveVec = VNorm(UpMoveVec); LeftMoveVec = VNorm(LeftMoveVec); } // このフレームでの移動ベクトルを初期化 MoveVec = VGet(0.0f, 0.0f, 0.0f); // 移動したかどうかのフラグを初期状態では「移動していない」を表す0にする MoveFlag = false; //プレイヤーの行動の保存 player.PrevAction = player.Action; //キーボードorゲームパッドの入力に応じてプレイヤーの移動ベクトルを計算 //左 if (Input_GetKeyboard(KEY_INPUT_A) || Input_GetGamepad(XINPUT_THUNMBL_LEFT)) { // 移動ベクトルに「←」が入力された時の移動ベクトルを加算する MoveVec = VAdd(MoveVec, LeftMoveVec); // 移動したかどうかのフラグを「移動した」にする MoveFlag = true; } //右 if (Input_GetKeyboard(KEY_INPUT_D) || Input_GetGamepad(XINPUT_THUNMBL_RIGHT)) { // 移動ベクトルに「←」が入力された時の移動ベクトルを反転したものを加算する MoveVec = VAdd(MoveVec, VScale(LeftMoveVec, -1.0f)); // 移動したかどうかのフラグを「移動した」にする MoveFlag = true; } //上 if (Input_GetKeyboard(KEY_INPUT_W) || Input_GetGamepad(XINPUT_THUNMBL_UP)) { // 移動ベクトルに「↑」が入力された時の移動ベクトルを加算する MoveVec = VAdd(MoveVec, UpMoveVec); // 移動したかどうかのフラグを「移動した」にする MoveFlag = true; } //下 if (Input_GetKeyboard(KEY_INPUT_S) || Input_GetGamepad(XINPUT_THUNMBL_DOWN)) { // 移動ベクトルに「↑」が入力された時の移動ベクトルを反転したものを加算する MoveVec = VAdd(MoveVec, VScale(UpMoveVec, -1.0f)); // 移動したかどうかのフラグを「移動した」にする MoveFlag = true; } if (MoveFlag) { // 移動ベクトルを正規化したものをプレイヤーが向くべき方向として保存 player.TargetMoveDirection = VNorm(MoveVec); // プレイヤーが向くべき方向ベクトルをプレイヤーのスピード倍したものを移動ベクトルとする MoveVec = VScale(player.TargetMoveDirection, PLAYER_MOVE_SPEED); //プレイヤーの行動を移動にする player.Action = PLAYER_WALK; } else { //プレイヤーの行動を待機にする player.Action = PLAYER_IDLE; } //プレイヤーの移動方向にモデルの方向を近づける Player_AngleUpdate(); //座標の更新 NowPos = VAdd(player.Position, MoveVec); player.Position = NowPos; MV1SetPosition(player.ModelHandle, player.Position); //アニメーション処理 Player_PlayAnimation(); }
最初の「ルートフレームのZ軸方向の移動パラメータを無効にする」は(おそらく)移動の際に変な挙動をするのを防ぐ目的。
次にカメラの座標と注視点を得てカメラから見て真正面方向の移動ベクトルとカメラから見て左方向の移動ベクトルを作成する。2つのベクトルを作成した後は正規化をすること。別にしなくても実装できなくはないが、後で移動距離などの調整をするときに非常に面倒なことになる。
先ほど正規化した2つの移動ベクトルをキーボードまたはゲームパッドの入力に合わせて加算。ついでに移動したかどうかのフラグ MoveFlag も処理する。
MoveFlag が True(移動した)ならプレイヤーが向く方向および移動ベクトルを最終決定し、プレイヤーの行動状態を「移動」にする。False(移動していない)ならプレイヤーの行動状態を「待機」にする
移動ベクトル、プレイヤーの向き、行動状態(アニメーション)が決まったらそれぞれ情報を更新する。
プレイヤーの向きを計算する関数 Player_AngleUpdate() はこちら。
//プレイヤーの向きを更新 void Player_AngleUpdate() { float TargetAngle; // 目標角度 float SaAngle; // 目標角度と現在の角度との差 // 目標の方向ベクトルから角度値を算出する TargetAngle = (float)atan2(player.TargetMoveDirection.x, player.TargetMoveDirection.z); // 目標の角度と現在の角度との差を割り出す { // 最初は単純に引き算 SaAngle = TargetAngle - player.Angle; // ある方向からある方向の差が180度以上になることは無いので // 差の値が180度以上になっていたら修正する if (SaAngle < -DX_PI_F) { SaAngle += DX_TWO_PI_F; } else if (SaAngle > DX_PI_F) { SaAngle -= DX_TWO_PI_F; } } // 角度の差が0に近づける if (SaAngle > 0.0f) { // 差がプラスの場合は引く SaAngle -= PLAYER_ANGLE_SPEED; if (SaAngle < 0.0f) { SaAngle = 0.0f; } } else { // 差がマイナスの場合は足す SaAngle += PLAYER_ANGLE_SPEED; if (SaAngle > 0.0f) { SaAngle = 0.0f; } } // モデルの角度を更新 player.Angle = TargetAngle - SaAngle; MV1SetRotationXYZ(player.ModelHandle, VGet(0.0f, player.Angle + DX_PI_F, 0.0f)); }
上にも書いたが三角関数関連の計算(ここではatan2())を使うため、<math.h> を include しておかないと計算できないので注意。
プレイヤーのアニメーション処理 Player_PlayAnimation() はこちら。今回は1F前の行動状態と現在の行動状態を比較し、その2つが異なれば行動状態が切り替わったとして現在の行動状態の方アニメーションに切り替える。今回再生するアニメーションは待機(IDLE)と移動(WALK)のみ。
//プレイヤーのアニメーション処理 void Player_PlayAnimation() { //再生時間を進める player.PlayTime += 1.0f; //再生時間がアニメーションの総再生時間に達したら再生時間を0に戻す if (player.PlayTime >= player.TotalTime) player.PlayTime = 0.0f; //1F前のプレイヤーの行動と現在のプレイヤーの行動が違っていれば再生するアニメーションを変更 if (player.PrevAction != player.Action) { //今までアタッチしていたアニメーションのデタッチ MV1DetachAnim(player.ModelHandle, player.AttachIndex); if (player.Action == PLAYER_IDLE) { //Idle という名前のアニメーションの番号を取得する player.AnimIndex = MV1GetAnimIndex(player.ModelHandle, "Armature|Idle"); //取得したアニメーション番号のアニメーションをアタッチする player.AttachIndex = MV1AttachAnim(player.ModelHandle, player.AnimIndex, -1, FALSE); //アタッチしたアニメーションの総再生時間を取得する player.TotalTime = MV1GetAttachAnimTotalTime(player.ModelHandle, player.AttachIndex); } else if (player.Action == PLAYER_WALK) { //Walk という名前のアニメーションの番号を取得する player.AnimIndex = MV1GetAnimIndex(player.ModelHandle, "Armature|Walk"); //取得したアニメーション番号のアニメーションをアタッチする player.AttachIndex = MV1AttachAnim(player.ModelHandle, player.AnimIndex, -1, FALSE); //アタッチしたアニメーションの総再生時間を取得する player.TotalTime = MV1GetAttachAnimTotalTime(player.ModelHandle, player.AttachIndex); } //再生時間の初期化 player.PlayTime = 0.0f; } //再生時間をセットする MV1SetAttachAnimTime(player.ModelHandle, player.AttachIndex, player.PlayTime); }
コメント