【DXライブラリ】3Dモデルを動かす

DXライブラリで3Dモデルを動かす方法について説明しています。キーボード・ゲームパッドの入力とカメラの方向から移動ベクトルを求めてプレイヤーの座標に足していきます。

3Dモデルの動かし方の基本

3Dモデルを3D空間で自由に動かす場合、キーボードまたはゲームパッドの入力から移動後の座標を求めるのは変わらない。が、2Dゲームとは異なりカメラの方向を考慮する必要がある。

おおざっぱな流れとしてはこんな感じ。

  1. ゲームパッドの左スティックの入力またはキーボードの入力(大抵はAWSDキー)から移動ベクトルを作成
  2. カメラの水平方向の角度を取得し、その角度だけ回転させる行列を作成
  3. 1.で作成したベクトルを2.で作成した行列で回転させる。これがカメラから見ての移動方向になる
  4. 移動ベクトルをもとに3Dモデルの座標および向きを更新
  5. アニメーション設定がある場合はそれも更新する

カメラから見た向きに合わせて移動ベクトルを回転させるのが大事。この回転のおかげでカメラが水平方向のどの向きでもスティックを前に倒せば前進する。

ジャンプの実装やマップとの当たり判定についてはここで載せるとあまりにも長くなるので別記事にて紹介予定。

サンプルコード

では実装。今回は3Dモデルを動かすのが目的だがそれにあたりモデルの向きを変える処理や前回実装したアニメーションも組み込んでいく。そのためかなりの量になってしまっているが喰らいついてほしい。

実装の前にBlenderなどでテキトーに下のようなマップを作成してfbx→mv1化して読み込んで表示させる。これがないと実際に動いているかどうかがわからないので作っておくこと。

カメラの処理

カメラの向きの合わせて移動ベクトルを求めるため、カメラの座標・注視点およびカメラの水平方向の角度が必要になる。これらの変数は他のファイルで使う必要があるので Camera.cpp/h にカメラの座標と注視点を渡す関数を追加する。

Camera.cpp

//以下の関数を追加
VECTOR Camera_GetEye();
VECTOR Camera_GetTarget();
float Camera_GetAngleH();

Camera.h

//以下の関数を追加
//カメラの座標をゲットする(他のファイルから取得用)
VECTOR Camera_GetEye() {
    return camera.Eye;
}

//カメラの注視点をゲットする(他のファイルから取得用)
VECTOR Camera_GetTarget() {
    return camera.Target;
}

//カメラの水平方向の角度をゲットする(他のファイルから取得用)
float Camera_GetAngleH() {
    return camera.AngleH;
}

ゲームパッドの処理

次にゲームパッドの左スティックの情報が必要になるのでInput.cpp/hに左スティックの情報を格納する構造体を追加し、Input_UpdateGamepad()にて中身を更新する。そしてその構造体を他ファイルから参照できる関数 Input_GetVector() を追加する。

Input.h

//以下の関数を追加
VECTOR Input_GetVector();

Input.cpp

//以下の変数・関数を追加
//ゲームパッドの左スティックの値を格納する構造体
VECTOR vector;

//左スティックの入力ベクトルを渡す(他のファイルから取得用)
VECTOR Input_GetVector() {
    return vector;
}

//以下の関数にコードを追加・変更
//ゲームパッドの入力状態を更新する
void Input_UpdateGamepad() {
    //略
    
    // 左スティックの入力値保管用変数の初期化
    vector = VGet(0.0f, 0.0f, 0.0f);

    for (i = 0; i < XINPUT_ALL; i++) {
        if (i < XINPUT_LEFT_TRIGGER) {
            //略
        }
        else if (i == XINPUT_THUMBL_UP) {
            if (input.ThumbLY > STICK_DEADZONE) {
                Input_Gamepad[i]++;  
                vector.z = (float)input.ThumbLY; //追加部分
            }
            else {
                if (Input_Gamepad[i] > 0) {
                    Input_Gamepad[i] = -1;
                }
                else {
                    Input_Gamepad[i] = 0;
                }
            }
        }
        else if (i == XINPUT_THUMBL_DOWN) {
            if (input.ThumbLY < -STICK_DEADZONE) {
                Input_Gamepad[i]++;   
                vector.z = (float)input.ThumbLY; //追加部分
            }
            else {
                if (Input_Gamepad[i] > 0) {
                    Input_Gamepad[i] = -1;
                }
                else {
                    Input_Gamepad[i] = 0;
                }
            }
        }
        else if (i == XINPUT_THUMBL_LEFT) {
            if (input.ThumbLX < -STICK_DEADZONE) {
                Input_Gamepad[i]++;    
                vector.x = (float)input.ThumbLX; //追加部分
            }
            else {
                if (Input_Gamepad[i] > 0) {
                    Input_Gamepad[i] = -1;
                }
                else {
                    Input_Gamepad[i] = 0;
                }
            }
        }
        else if (i == XINPUT_THUMBL_RIGHT) {
            if (input.ThumbLX > STICK_DEADZONE) {
                Input_Gamepad[i]++;    
                vector.x = (float)input.ThumbLX; //追加部分
            }
            else {
                if (Input_Gamepad[i] > 0) {
                    Input_Gamepad[i] = -1;
                }
                else {
                    Input_Gamepad[i] = 0;
                }
            }
        }
        else if (i == XINPUT_THUMBR_UP) { //右スティック
           //略
        }
    }

    //略
}

プレイヤーの処理

本命のプレイヤーの処理を Player.cpp に追加していく。

まず今回の計算に三角関数関連のものを使う必要があるので <math.h> をインクルードし、プレイヤーの向きに関する変数 TargetMoveDirection および Angle 、アニメーションの遷移用の変数 Action および PrevAction を追加する。
また移動速度や角度変化速度、プレイヤーの行動状態の定義もしておく。

//Player.cpp
//math.hをインクルードする
#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_TPOSE //T-POSE
}Player_Action;

次にプレイヤーの向きを変える関数を追加する。

//以下の関数を追加
//プレイヤーの向きを更新
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));
}

モデルの向きを更新するには MV1SetRotationXYZ() を使う。

宣言 int MV1SetRotationXYZ( int MHandle, VECTOR Rotate )
概要 3Dモデルの回転値を設定する
引数 int MHandle 3Dモデルのハンドル
VECTOR Rotate 回転値(単位はラジアン)
戻り値 0 成功
-1 エラー発生

続けてプレイヤーの移動処理を行う関数 Player_MoveUpdate() を追加する。

//プレイヤーの移動処理
void Player_MoveUpdate() {
    VECTOR MoveVec; // このフレームの移動ベクトル
    bool MoveFlag; // 移動したかどうかのフラグ( true:移動した  false:移動していない )
    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;
    }

    //座標の更新
    NowPos = VAdd(player.Position, MoveVec);
    player.Position = NowPos;
    MV1SetPosition(player.ModelHandle, player.Position);
}

最初の「ルートフレームのZ軸方向の移動パラメータを無効にする」は(おそらく)移動の際に変な挙動をするのを防ぐ目的。

次に移動ベクトルや移動フラグを一旦0にし、ゲームパッドの左スティックまたはキーボードの入力から移動ベクトルの土台を作成する。

移動フラグが true (移動した)ならカメラの水平方向の角度を取得し、その角度分だけ移動ベクトルを回転させて正規化、最後に PLAYER_MOVE_SPEED 倍させる。あとプレイヤーの行動状態も決定する。

最後にプレイヤーの座標を MV1SetPosition() で更新する。

宣言 int MV1SetPosition( int MHandle, VECTOR Position )
概要 3Dモデルの座標を設定する
引数 int MHandle 3Dモデルのハンドル
VECTOR Position 3Dモデルに設定する座標
戻り値 0 成功
-1 エラー発生

上にも書いたが三角関数関連の計算(ここではatan2())を使うため、<math.h> を include しておかないと計算できないので注意。

そしてプレイヤーのアニメーション処理を以下の様に変更する。

//プレイヤーのアニメーション処理
void Player_PlayAnimationUpdate() {
    //再生時間を進める
    player.PlayTime += 1.0f;

    //再生時間がアニメーションの総再生時間に達したら再生時間を戻す
    if (player.PlayTime >= player.TotalTime) {
        player.PlayTime = 0.0f; 
    }

    //1F前のプレイヤーの行動と現在のプレイヤーの行動が違っていれば再生するアニメーションを変更
    if (player.PrevAction != player.Action) {
        //今までアタッチしていたアニメーションのデタッチ
        MV1DetachAnim(player.ModelHandle, player.AttachIndex);

        //再生するアニメーションの番号を取得(if文からswitch文に変更)
        switch (player.Action) {
        case PLAYER_IDLE:
            player.AnimIndex = MV1GetAnimIndex(player.ModelHandle, "Armature|Idle");
            break;
        case PLAYER_WALK:
            player.AnimIndex = MV1GetAnimIndex(player.ModelHandle, "Armature|Walk");
            break;
        default:
            player.AnimIndex = MV1GetAnimIndex(player.ModelHandle, "Armature|T-Pose");
            break;
        }

        //取得したアニメーション番号のアニメーションをアタッチする
        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);
}

アニメーションの遷移についてここでは1フレーム前の行動状態と現在の行動状態を比較し、その2つが異なれば行動状態が切り替わったとして現在の行動状態の方アニメーションに切り替える。

最後に Player_Update() を以下のように変更する。

//プレイヤーの更新関数を以下のように変更
void Player_Update() {
    //移動処理
    Player_MoveUpdate();

    //プレイヤーの移動方向にモデルの方向を近づける
    Player_AngleUpdate();

	//アニメーション処理
	Player_PlayAnimationUpdate();
}

あとは Player.h に今回追加した関数を追加する。

//以下の関数をPlayer.hに追加
void Player_AngleUpdate();
void Player_MoveUpdate();

コメント

タイトルとURLをコピーしました