Skip to content

第05回 キャラクターアニメーション

前回: 第04回 マップデータの外部化(CSV読込)とスクロール / 次回: 第06回 サウンドとゲームイベント

今回の授業内容

プレイヤーのスプライトにアニメーションを適用した例

  • 1枚の画像へ複数のフレームを並べるスプライトシートの構造を理解する
  • 画像全体の大きさから1フレームの切り出しサイズを計算する
  • DrawRectRotaGraph3で切り出した画像を指定サイズに描画する
  • フレーム番号と待ち時間を使って画像を順番に切り替える
  • 接地状態と速度から、待機、走行、上昇、落下の表示を選ぶ
  • 移動方向に合わせて画像を左右反転する
  • 画像の切り出しサイズ、表示サイズ、当たり判定サイズを分離する

今回の配布コード

第04回のプレイヤー画像を、横8フレームのスプライトシートへ置き換えたコードです
物理挙動は変更せず、毎フレーム変化する値から表示する画像を選びます

配布ファイル一覧
ファイル内容
Source/Game.hアニメーション情報、フレーム数、表示サイズ
Source/Game.cpp表示選択、フレーム更新、拡大縮小を伴う部分描画
Source/ImageManager.h第03回から継続する画像管理
Source/ImageManager.cppスプライトシートを1枚の画像として読み込む
Assets/Images/Player.png8フレームのプレイヤー画像
Assets/Images/Tile.png足場タイル画像
Assets/Images/Goal.pngゴール画像
Assets/Maps/Stage01.csvステージマップ

スプライトシート

複数のフレームを1枚へ並べる

待機、走行、上昇、落下の計8フレームを横一列に並べたプレイヤーのスプライトシートの例プレイヤーのスプライトシートの例
待機、走行、上昇、落下の計8フレームを横一列に並べている

スプライトアニメーションは、時間経過に応じて画像を切り替えることで表現します
実装の方法は様々ですが、今回はPlayer.pngに、同じ大きさのフレーム8枚を隙間なく横一列に並べてスプライトシートとして扱います

内容待機0待機1走行0走行1走行2走行3上昇落下
フレーム番号01234567
フレーム番号内容
0~1待機
2~5走行
6ジャンプ上昇
7落下

付属画像は16x24ピクセルのフレームを使った128x24ピクセルの画像ですが、1フレームの大きさは固定しません

ImageManagerPlayer.pngを1枚の画像として読み込みます
フレームの分割は読み込んだ画像全体の大きさから計算するため、画像管理の仕組みを変更する必要はありません

フレーム番号から切り出し位置を計算する

横一列へ8枚並べた場合、1フレームの横幅は画像全体の横幅を8で割ると求められます

Game.cppcpp
const int sourceWidth = image.width / playerFrameCount;
const int sourceHeight = image.height;

切り出し開始位置はフレーム番号と切り出し幅の積になります

Game.cppcpp
const int sourceX = PlayerFrameIndex() * sourceWidth;

フレーム番号が3、切り出し幅が48なら、画像左端から144ピクセルの位置を使用します
画像の横幅はフレーム数の8で割り切れる必要があります

初期化時に横幅を確認し、配置規則に合わない画像を読み込んだ場合は処理を中止します

Game.cppcpp
const ImageResource& playerImage = images.Get(ImageId::Player);
if (playerImage.width % playerFrameCount != 0)
{
    MessageBoxW(nullptr,
        L"Player.pngの横幅を8で割り切れません。",
        L"Player Image Error", MB_OK);
    return false;
}

アニメーション情報

表示する動きを列挙する

Game.hcpp
enum class PlayerAnimation
{
    Idle,
    Run,
    Jump,
    Fall
};

Playerには現在の動き、動きの中のフレーム番号、フレームを切り替えるまでの時間、向きを追加します

Game.hcpp
PlayerAnimation animation;
int animationFrame;
int animationTimer;
bool facingRight;

入力方向からキャラクターの向きを決める

キャラクターの向きはプレイヤーが入力している方向を優先して決めます

Game.cppcpp
if (CheckHitKey(KEY_INPUT_LEFT))
{
    player.vx -= accel;
    // 左入力に合わせてキャラクターを左へ向ける
    player.facingRight = false;
}
else if (CheckHitKey(KEY_INPUT_RIGHT))
{
    player.vx += accel;
    // 右入力に合わせてキャラクターを右へ向ける
    player.facingRight = true;
}

入力がない間はfacingRightを変更せず、最後に入力した向きを維持します

物理状態から表示を選ぶ

空中では縦速度、地上では横速度を調べます

Game.cppcpp
PlayerAnimation next = PlayerAnimation::Idle;

if (!player.onGround)
{
    next = (player.vy < 0) ? PlayerAnimation::Jump : PlayerAnimation::Fall;
}
else
if (player.vx < -0.1f || player.vx > 0.1f)
{
    next = PlayerAnimation::Run;
}

vx == 0ではなく小さな範囲を停止として扱うことで、floatのわずかな速度による表示のちらつきを避けます

動きが変わったときは、別の動きの途中フレームを引き継がないように初期化します

Game.cppcpp
if (next != player.animation)
{
    player.animation = next;
    player.animationFrame = 0;
    player.animationTimer = 0;
}

スプライトのフレームを進める

毎フレーム画像を切り替えると速すぎるため、animationTimerが指定値へ達したときだけ次へ進めます

Game.cppcpp
player.animationTimer++;

if (player.animationTimer >= frameDuration)
{
    player.animationTimer = 0;
    player.animationFrame =
        (player.animationFrame + 1) % frameCount;
}

% frameCountにより、最後のフレームの次は0へ戻ります
待機は20フレーム、走行は6フレームごとに切り替える設定です
ジャンプと落下は1枚だけなので、フレーム番号を進めません

物理更新後にアニメーションを更新する

Game.cppcpp
void Game::UpdatePlaying()
{
    UpdatePlayerInput();
    UpdatePlayerPhysics();
    UpdatePlayerAnimation();
    UpdateCamera();

    // クリア、ゲームオーバー判定
}

物理更新後なら、そのフレームで着地したか、上昇から落下へ変わったかを表示へ反映できます

より高度なアニメーション管理

スプライトアニメーションの基本的な仕組みを理解するため、今回は開始フレーム、枚数、待ち時間などを条件分岐の中へ直接記述しています
ですがこれではアニメーションの種類が増える都度コードを書かなければならず、表現力にも限界があります

実際のゲームでは、より高度なアニメーション管理のシステムが必要になります
例えばアニメーションごとに次の情報をまとめ、共通の処理で再生できるようにするといった形の実装が考えられます

  • どの画像を使用するか
  • 画像のどの位置から、どの大きさで切り出すか
  • フレームの並び順
  • 各フレームを表示する時間
  • 最後まで再生した後にループするか

これらの情報をAnimationClipのような構造体にまとめれば、アニメーションの追加も調整も簡単になります
余裕があれば、ぜひ挑戦してみてください

スプライトのフレームを描画する

切り出しサイズと表示サイズを分ける

画像のサイズとゲーム画面に表示する大きさを別のものとして扱えると、画像の取り回しの自由度が上がります
例えば48x64ピクセルの画像フレームをゲーム画面では32x48ピクセルに縮小して表示できます

表示サイズは画像の大きさとは別に定義します

Game.hcpp
// Player.pngは同じ大きさのフレームを横8枚に並べる
static const int playerFrameCount = 8;

// 画像の1フレームを画面へ表示する大きさ
static const int playerDrawWidth = 32;
static const int playerDrawHeight = 48;

描画位置の計算には、この表示サイズを使います

Game.cppcpp
Rect Game::PlayerImageRect() const
{
    const int drawX =
        player.x + player.width / 2 - playerDrawWidth / 2;
    const int drawY =
        player.y + player.height - playerDrawHeight;

    return {
        drawX, drawY,
        playerDrawWidth, playerDrawHeight
    };
}

第03回で導入した足元中央基準は維持します
画像の原寸を変更しても、表示サイズや当たり判定サイズは変化しません

DrawRectRotaGraph3で拡大縮小して描画する

これまで使用してきた描画関数DrawRectGraphは切り出した画像を原寸で描画します

今回は縦横の拡大率と左右反転を同時に指定できるDrawRectRotaGraph3を使います
表示サイズを切り出しサイズで割れば拡大率を求めることができます
やや複雑な計算になってしまいますが、画像のサイズと表示のサイズを分離できれば表現の自由度が上がります

Game.cppcpp
const double scaleX =
    static_cast<double>(imageRect.width) / sourceWidth;
const double scaleY =
    static_cast<double>(imageRect.height) / sourceHeight;

DrawRectRotaGraph3(
    screenX + imageRect.width / 2,
    imageRect.y + imageRect.height / 2,
    sourceX, 0, sourceWidth, sourceHeight,
    sourceWidth / 2, sourceHeight / 2,
    scaleX, scaleY, 0.0,
    image.handle, TRUE,
    player.facingRight ? FALSE : TRUE);

最初の2つの引数は描画先の中心、sourceWidth / 2sourceHeight / 2は切り出した画像内の中心です
今回は回転しないため、角度には0.0を指定します

最後の引数がTRUEなら左右反転して描画します
左入力なら左向き、右入力なら右向きとして、入力がない間は最後の向きを維持します

デバッグ表示

F1の表示へ現在のアニメーションとフレーム番号を追加します

Game.cppcpp
DrawFormatString(
    14, 160, GetColor(255, 255, 255),
    L"animation:%d frame:%d",
    static_cast<int>(player.animation),
    PlayerFrameIndex());

スプライトシートが正常に表示されない場合は以下の点を確認しましょう

  1. animationが意図した値か
  2. frameが指定範囲内か
  3. sourceXが切り出し幅の倍数か
  4. Player.pngの横幅が8で割り切れるか
  5. 切り出しサイズと表示サイズを取り違えていないか
  6. 足元中央と当たり判定がずれていないか

実習

実習1: スプライトシートを表示する

  1. 同じ大きさのフレーム8枚を横へ並べたPlayer.pngを用意する
  2. 画像全体の大きさから切り出し幅と高さを計算する
  3. 画像とは別に表示幅と高さを定義する
  4. フレーム番号0DrawRectRotaGraph3で表示する
  5. プレイヤーの足元と当たり判定の位置を確認する

実習2: 待機と走行を切り替える

  1. PlayerAnimationを追加する
  2. 横速度からIdleRunを選ぶ
  3. animationTimerでフレームを進める
  4. 左右移動に合わせて画像を反転する

実習3: 空中表示を追加する

  1. onGroundvyからJumpFallを選ぶ
  2. ジャンプの頂点で表示が切り替わることを確認する
  3. 着地したフレームで待機または走行へ戻ることを確認する

今回の到達目標

  • スプライトシートのフレーム配置を説明できる
  • フレーム番号から切り出し位置を計算できる
  • 時間を使ってアニメーションを進められる
  • 速度と接地状態から表示を選べる
  • 切り出しサイズ、表示サイズ、当たり判定サイズを分離できる
  • アニメーションを追加しても足元中央を維持できる

まとめ

  • Player.pngを横8フレームのスプライトシートにしました
  • 画像全体の大きさから1フレームの切り出しサイズを計算しました
  • DrawRectRotaGraph3で現在のフレームを指定サイズに描画しました
  • 待機、走行、上昇、落下を物理状態から選びました
  • フレームの待ち時間によって再生速度を制御しました
  • 切り出しサイズ、表示サイズ、当たり判定サイズを分離し、足元中央を維持しました