DXライブラリとC言語でゲーム制作。今回は3Dモデルを3D空間で動かす機能を実装してみる。マップとの当たり判定は後日。
【2025/09/06追記】プレイヤーの行動状態について、const static int PLEYR_IDLE = 0…と1つずつ定義してましたが enum で列挙する方法に変更しました。
【2025/09/14追記】ゲームパッドでのスティックでの移動が8方向以上になるように変更しました。
3Dモデルの動かし方の基礎
3Dモデルを3D空間で自由に動かす場合、キーボードまたはゲームパッドの入力から移動後の座標を求めるのは変わらない。が、2Dゲームとは異なりカメラの方向を考慮する必要がある。
DXライブラリ置き場の3Dアクション基本のページに3Dモデルの動かし方が載っているが移動方向が8方向となっている。ゲームパッドのスティックで動かす場合8方向のみでは物足りないのでもっと自由に動かせるようにする。
おおざっぱな流れとしてはこんな感じ。
- ゲームパッドの左スティックの入力またはキーボードの入力(ここではAWSDキー)から移動ベクトルを作成
- カメラの水平方向の角度を取得し、その角度だけ回転させる行列を作成
- 1で作成したベクトルを2で作成した行列で回転させる。これがカメラから見ての移動方向になる
- 移動ベクトルをもとに3Dモデルの座標および向きを更新
カメラから見た向きに合わせて移動ベクトルを回転させるのが大事。この回転のおかげでカメラが水平方向のどの向きでもスティックを前に倒せば前進する。
ジャンプの実装やマップとの当たり判定については別記事にて実装。
実装
では実装。今回は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();
次にゲームパッドの左スティックの情報が必要になるのでInput.cpp/hに左スティックの情報を格納する構造体を追加し、Input_UpdateGamepad()にて中身を更新する。そしてその構造体を他ファイルから参照できる関数 Input_GetVector() を追加する。
Input.cpp
//ゲームパッドの左スティックの値を格納する構造体 VECTOR vector; //ゲームパッドの入力状態を更新する void Input_UpdateGanepad() { //中略 for (i = 0; i < GAMEPAD_MAX; i++) { if (i < 16) { //ボタン //中略 } else if (i == XINPUT_THUNMBL_UP) { //左スティック if (input.ThumbLY > STICK_DEADZONE) { //左スティックが上に一定以上倒されていたら Input_Gamepad[i]++; //加算 vector.z += (float)input.ThumbLY; } else { if (Input_Gamepad[i] > 0) { //前フレームで対応するキーが押されていれば Input_Gamepad[i] = -1; //-1にする。-1はキーが離された時用の値 } else { Input_Gamepad[i] = 0; //0にする } } } else if (i == XINPUT_THUNMBL_DOWN) { if (input.ThumbLY < -STICK_DEADZONE) { //左スティックが下に一定以上倒されていたら Input_Gamepad[i]++; //加算 vector.z += (float)input.ThumbLY; } else { if (Input_Gamepad[i] > 0) { //前フレームで対応するキーが押されていれば Input_Gamepad[i] = -1; //-1にする。-1はキーが離された時用の値 } else { Input_Gamepad[i] = 0; //0にする } } } else if (i == XINPUT_THUNMBL_LEFT) { if (input.ThumbLX < -STICK_DEADZONE) { //左スティックが左に一定以上倒されていたら Input_Gamepad[i]++; //加算 vector.x += (float)input.ThumbLX; } else { if (Input_Gamepad[i] > 0) { //前フレームで対応するキーが押されていれば Input_Gamepad[i] = -1; //-1にする。-1はキーが離された時用の値 } else { Input_Gamepad[i] = 0; //0にする } } } else if (i == XINPUT_THUNMBL_RIGHT) { if (input.ThumbLX > STICK_DEADZONE) { //左スティックが右に一定以上倒されていたら Input_Gamepad[i]++; //加算 vector.x += (float)input.ThumbLX; } else { if (Input_Gamepad[i] > 0) { //前フレームで対応するキーが押されていれば Input_Gamepad[i] = -1; //-1にする。-1はキーが離された時用の値 } else { Input_Gamepad[i] = 0; //0にする } } } //中略 } //中略 //左スティックの入力ベクトルを渡す(他のファイル用) VECTOR Input_GetVector() { return vector; } //中略
Input.h
#ifndef DEF_INPUT_H #define DEF_INPUT_H //中略 VECTOR Input_GetVector(); //これをヘッダーファイルに追加
次に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; //プレイヤーの角度変化速度 //プレイヤーの行動状態の列挙体 typedef enum { PLAYER_IDLE, //待機(何の操作もない場合) PLAYER_WALK, //移動 }Player_Action;
今回の計算に三角関数関連のものを使う必要があるので <math.h> をインクルードしている。
TargetMoveDirection および Angle はプレイヤーの向き用の変数。TargetMoveDirection があるのはプレイヤーの向きを滑らかに変えるため。
Action および PrevAction はアニメーション用の変数。これにプレイヤーの行動状態(待機、移動など)を格納する。
プレイヤーの行動状態については enum を使って列挙する。特に指定がない場合は上から順に0,1,2,…と番号が割り当てられる。追加・削除しても自動で割り当ててくれるので便利。
そしてプレイヤーの移動処理を追加する。
//以下の処理を追加 //初期化 void Player_Initialize() { //中略 //アクションの初期化 player.Action = 0; //中略 } //更新 void Player_Update() { 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); } // このフレームでの移動ベクトルを初期化 MoveVec = VGet(0.0f, 0.0f, 0.0f); // 移動したかどうかのフラグを初期状態では「移動していない」を表す0にする MoveFlag = false; //プレイヤーの行動の保存 player.PrevAction = player.Action; //まずはゲームパッド左スティックの入力の確認 MoveVec = Input_GetVector(); //ゲームパッドの左スティックの入力がなかった場合 if (MoveVec.x == 0 && MoveVec.z == 0) { //キーボードの入力を確認する //左 if (Input_GetKeyboard(KEY_INPUT_A)) { MoveVec.x -= 1.0f; //移動フラグを立てる MoveFlag = true; } //右 if (Input_GetKeyboard(KEY_INPUT_D)) { MoveVec.x += 1.0f; //移動フラグを立てる MoveFlag = true; } //上 if (Input_GetKeyboard(KEY_INPUT_W)) { MoveVec.z += 1.0f; //移動フラグを立てる MoveFlag = true; } //下 if (Input_GetKeyboard(KEY_INPUT_S)) { MoveVec.z -= 1.0f; //移動フラグを立てる MoveFlag = true; } } else { //ゲームパッドの左スティックの入力があった場合 //移動フラグを立てる MoveFlag = true; } if (MoveFlag) { { MATRIX RotY; //移動ベクトルの回転に使う行列 float angleH = 0.0f; //カメラの水平方向の角度 //カメラの水平方向の角度を取得 angleH = Camera_GetAngleH(); //水平方向の回転を求める(Y軸回転) RotY = MGetRotY(angleH); //移動ベクトルをカメラの水平方向の角度だけ回転させる MoveVec = VTransform(MoveVec, RotY); } // 移動ベクトルをプレイヤーが向くべき方向として保存 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軸方向の移動パラメータを無効にする」は(おそらく)移動の際に変な挙動をするのを防ぐ目的。
次に移動ベクトルや移動フラグを一旦0にし、ゲームパッドの左スティックまたはキーボードの入力から移動ベクトルの土台を作成する。
移動フラグが true (移動した)ならカメラの水平方向の角度を取得し、その角度分だけ移動ベクトルを回転させて正規化、最後に PLAYER_MOVE_SPEED 倍させる。あとプレイヤーの行動状態も決定する。
最後にプレイヤーの向きを更新し、座標やアニメーションを更新する。
プレイヤーの向きを計算する関数 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); }