Skip to content

第02回 世界観・画像とアニメーション ​

自分のゲームの外見を作る ​

完成版の主人公、敵、攻撃、経験値アイテムを、自分の作品に合わせて変えていきます。主人公が何者で、何に向かって攻撃し、何を集めるゲームなのかを考えると、画像や動きの役割を決めやすくなります。

例えば、主人公が清掃ロボットなら、敵を汚れ、攻撃を洗浄、経験値を回収資源として表現できます。魔法使いなら、攻撃を魔法、経験値を魔力として見せることもできます。見立てに合わせて外見を作り、後の回で攻撃や成長のルールも変えられます。

この回では、画像の配置、表示サイズ、アニメーションの設定を扱います。待機・移動だけでなく、被弾・やられの表示も同じ定義表で指定できます。自作画像、既存素材の加工、利用可能な素材など、作品に合う方法で用意してください。

画像は配布版の見本です。手元のプロジェクトで変更した外見やコードは、その後の制作にも引き継ぎます。

画像を変える場所と処理のつながり ​

第01回で用意したCompleteを引き続き使います。ビルドや環境設定は配布ファイルを参照してください。

場所決めること
Assets/Images/使用するPNG画像
Source/AutoShooter/Resources/SpriteImages.h画像を区別するID
Source/AutoShooter/Resources/SpriteImages.cppファイル名、コマ分割、表示サイズ、基準点
Source/AutoShooter/Resources/CharacterAnimation.h動作ごとに使う画像と再生方法
Source/AutoShooter/Resources/EnemyVisuals.h敵の種類と画像、色、表示倍率の対応
Source/AutoShooter/Tuning/GameTuning.h主人公の表示サイズや、画像とは別の判定設定

画像を読み込んでコマを切り出す処理は、SpriteImagesが引き受けます。ゲーム本体は「今は走行中」といった動作を決め、表示側がその動作に対応する画像とコマを選びます。

この分担により、画像のコマ数や再生速度を変えるときに、移動やHPの処理を書き換える必要はありません。

PNGを配置してゲームへ反映する ​

画像はプロジェクト側のAssetsへ置きます。例えば経験値アイテムなら、次のファイルを差し替えます。

text
Assets/Images/Experience.png

Experience.pngは1コマの静止画です。敵のEnemy.pngも1コマで、通常敵と突進敵が同じ画像を使っています。

ファイルを保存したら、Visual Studioで再ビルドして実行します。ビルド時にプロジェクト側のAssetsから出力先へコピーされ、起動時に読み込まれます。

透過部分も含めた画像全体が、1コマの範囲です。背景を透明にしたPNGを使うと、キャラクターの周囲に背景色の四角が出ない形で描画できます。

素材を利用した時点で、出典、作者、利用条件をAssets/ThirdPartyへ記録します。加工や配布の条件も、その素材のライセンスで確認してください。

元画像の大きさと画面上の大きさ ​

SpriteImages.cppのDefinitionsを開きます。経験値アイテムの定義は次の1行です。

cpp
{ SpriteImages::Id::Experience, L"Assets/Images/Experience.png", 1, 1, 32, 32, 1, 2 },

各値には次の意味があります。

値意味
SpriteImages::Id::Experienceこの画像のID
L"Assets/Images/Experience.png"読み込むファイル
1, 1横1コマ、縦1コマ
32, 32画面上で表示する幅と高さ
1, 2縦方向の基準位置を、1コマの高さの1/2にする

L"..."は、このプログラムでファイル名などに使うワイド文字列です。

元画像が64×64pxでも、画面上では32×32pxに縮小して描きます。元画像の解像度と表示サイズは別の設定です。

例えば経験値アイテムを48×48pxで表示するなら、先ほどの定義1行を置き換えます。

cpp
{ SpriteImages::Id::Experience, L"Assets/Images/Experience.png", 1, 1, 48, 48, 1, 2 },

表示サイズを変えても、吸引や回収の距離は変わりません。見た目と判定を別々に設定できるため、大きな画像を使いながら回収条件を保つこともできます。

コマ分割と基準点 ​

スプライトシートの読み方 ​

複数のコマを一枚に並べた画像を、スプライトシートと呼びます。配布版のPlayer.pngは横8列、縦1行です。

SpriteImages.cppでは次のように登録されています。

cpp
{ SpriteImages::Id::Player, L"Assets/Images/Player.png", 8, 1,
    tuning::player::DrawWidth, tuning::player::DrawHeight,
    tuning::player::PivotNumerator, tuning::player::PivotDenominator },

画像の幅を8、高さを1で割り、1コマの寸法を求めます。コマ番号は左上を0として右へ進み、複数行なら次の行へ続きます。

画像全体の幅は列数、高さは行数で割り切れる必要があります。例えば1コマ64×96pxの8列1行なら、画像全体は512×96pxです。

静止画1枚をPlayer.pngへ置いた場合も、定義が8列のままなら横に8分割されます。画像の並びとcolumns・rowsを対応させ、アニメーションが参照するコマ番号も合わせて変更します。

表示位置を合わせる基準点 ​

画像のどこをキャラクターの座標へ合わせるかを、基準点で指定します。横方向は1コマの中央です。縦方向は次の式で求めます。

基準位置 = 1コマの高さ × pivotNumerator ÷ pivotDenominator

主人公の設定はGameTuning.hのplayer名前空間にあります。

cpp
constexpr int DrawWidth = 64;
constexpr int DrawHeight = 96;
constexpr int PivotNumerator = 2;
constexpr int PivotDenominator = 3;

配布版では64×96pxで表示し、縦の基準位置を高さの2/3にしています。これらを変えると、座標に対する画像の大きさや上下位置が変わります。

透明な余白もコマの寸法に含まれます。同じ表示サイズでも、絵がコマの中央に小さく描かれていれば、画面上のキャラクターも小さく見えます。足元や影との位置は、絵の配置と基準点の両方で確かめてください。

主人公の当たり判定半径は、同じplayer名前空間のRadiusです。DrawWidthやDrawHeightから自動計算しているわけではありません。

待機・移動アニメーションを設定する ​

動作とコマを対応させる ​

CharacterAnimation.hのPlayerClipsは、主人公の動作と再生方法を対応させる表です。配布版は次の定義です。

cpp
inline constexpr std::array<AnimationClip, static_cast<std::size_t>(Player::Motion::Count)> PlayerClips = {{
    { SpriteImages::Id::Player, 0, 2, 0.3f, true },
    { SpriteImages::Id::Player, 2, 4, 0.1f, true },
    { SpriteImages::Id::Player, 0, 1, 0.2f, false },
    { SpriteImages::Id::Player, 0, 1, 0.2f, false }
}};

4行はPlayer::MotionのIdle・Run・Hurt・Defeatedに対応します。

動作開始コマコマ数1コマの秒数繰り返し
Idle・待機020.3する
Run・移動240.1する
Hurt・被弾010.2しない
Defeated・やられ010.2しない

structは、関連する値を一組にまとめる型です。ここではAnimationClipが画像ID、開始コマ、コマ数、1コマの秒数、ループ有無をまとめています。

PlayerClipsは、そのAnimationClipを並べた固定長配列です。std::arrayの要素数をPlayer::Motion::Countから求めています。Countは動作そのものではなく、動作の種類数を表す値です。

再生速度を変える ​

元の画像のままでも、再生速度は変更できます。例えばRunの1行を次のように置き換えます。

cpp
{ SpriteImages::Id::Player, 2, 4, 0.08f, true },

1コマ0.1秒から0.08秒へ変えると、4コマの一巡は0.4秒から0.32秒になります。足の動きは速くなりますが、主人公の移動速度は変わりません。移動速度はGameTuning.hのplayer::Speedで決めています。

実行して、止まったときの待機と、移動中の走行を見比べます。作品の主人公に合う動きになるよう、コマと時間を設定してください。

コマ番号を求める仕組み ​

AnimationClip::FrameAtは、動作の経過時間から表示コマを求めます。繰り返すアニメーションなら、一巡した後に先頭へ戻します。繰り返さない場合は最後のコマで止めます。

例えば開始コマ2、コマ数4なら、使う番号は2・3・4・5です。コマ数4を「最後の番号4」と読み違えないように、実際に使う範囲を確認してください。

ループしない設定は、動作の終了やゲーム内状態の変更を意味しません。画像を最後のコマで止める指定です。動作を選ぶ条件は第05回で扱います。

被弾・やられの画像を割り当てる ​

配布版のHurtとDefeatedは、どちらも0番を1コマだけ表示します。Player.pngの6・7番が被弾専用、やられ専用として定義されているわけではありません。

例えば、自分で用意した8列1行の画像で、6番を被弾、7番をやられとして描いた場合は、PlayerClipsのHurtとDefeatedの2行を次に置き換えられます。

cpp
{ SpriteImages::Id::Player, 6, 1, 0.2f, false },
{ SpriteImages::Id::Player, 7, 1, 0.2f, false }

複数コマを使う場合も、開始番号とコマ数で範囲を指定します。例えば開始6・コマ数3なら6〜8番が必要なので、読み込むシートにもその範囲が存在するようにします。

接触して被弾したときと、HPが0になったときに、設定した外見になるか確かめてください。被弾画像が表示される時間と、無敵時間は別の仕組みです。

別の画像ファイルでアニメーションを追加する ​

動作ごとに異なるシートも使えます。ここでは、被弾用の4コマを横一列に並べたPlayerHurt.pngを使う例を示します。

画像IDを追加する ​

SpriteImages.hでIdの列挙型を探し、Countの直前へPlayerHurtを追加します。ほかのIDは残します。

cpp
PlayerHurt,
 Count

enum classは、種類に名前を付ける型です。PlayerHurtというIDを登録することで、ファイルの場所を毎回書かずに画像を指定できます。

読み込み定義を追加する ​

画像を次の場所へ置きます。

text
Assets/Images/PlayerHurt.png

SpriteImages.cppのDefinitionsで、Shadowの行の直後へ追加します。

cpp
 { SpriteImages::Id::Shadow, L"Assets/Images/Shadow.png", 1, 1, 64, 32, 1, 2 }
,{ SpriteImages::Id::PlayerHurt, L"Assets/Images/PlayerHurt.png", 4, 1, 64, 96, 2, 3 }

これは横4列・縦1行、表示64×96px、縦基準点2/3の例です。追加行の先頭のカンマは、前の要素との区切りです。実際の画像に合わせて分割数や表示サイズを設定します。

Definitionsには、画像IDごとに1件の登録が必要です。登録数やIDの重複はstatic_assertで検査されます。

被弾動作から参照する ​

CharacterAnimation.hのPlayerClipsで、Hurtに対応する3行目を置き換えます。

cpp
{ SpriteImages::Id::PlayerHurt, 0, 4, 0.08f, false },

これで被弾時はPlayerHurtの0〜3番を再生します。ファイルを分けても、PlayerのHPや被弾処理を追加する必要はありません。

同じ方法で待機、移動、やられの画像も分けられます。新しい画像IDと読み込み定義を登録し、使いたい動作のimageへ指定します。

敵や攻撃の色を合わせる ​

画像を交換しても、色が元画像と異なって見える場合があります。描画時に元の色へ指定色を掛ける、乗算色を使っているためです。

EnemyVisuals.hには、敵ごとの画像と色が登録されています。

cpp
{ Enemy::Type::Chaser, SpriteImages::Id::Enemy, 0xEF7288, 1.00f, 56, 48, 40 },
{ Enemy::Type::Dasher, SpriteImages::Id::Enemy, 0xFFC16C, 1.25f, 72, 64, 48 }

列は敵ID、画像ID、色、倍率、影の幅、HPバーの幅、HPバーの縦位置です。同じEnemy.pngでも、通常敵と突進敵は色と倍率が異なります。

例えば元画像の色をそのまま使う通常敵なら、Chaserの1行を置き換えます。

cpp
{ Enemy::Type::Chaser, SpriteImages::Id::Enemy, 0xFFFFFF, 1.00f, 56, 48, 40 },

0xFFFFFFは赤・緑・青を弱めない白の指定です。乗算では、元画像で暗い部分を明るい色へ塗り替えることはできません。白を基調にして色を付けるか、画像自体へ色を描くかで表現が変わります。

攻撃画像はBolt.png、Fan.png、Pulse.pngです。描画には武装の定義色も使います。画像と色の対応は第07回の武装定義でも扱います。

作品で確かめること ​

待機から移動へ切り替え、走行のテンポと実際の移動速度が、主人公の動きとして合うか確かめます。被弾・やられの外見も、ゲーム内の状態が読み取れるか見てください。

画像全体、1コマの範囲、表示サイズ、基準点、乗算色、当たり判定は別の設定です。例えば「足元がずれる」ならコマ内の余白と基準点、「大きく描いたのに接触距離が同じ」なら表示サイズと判定半径を調べます。

読み込み時には、画像を指定の列数・行数で等分できるか、アニメーションの参照先がコマ数の範囲内かを検査しています。ファイル名や分割数、開始コマ、コマ数、秒数を照合すると、設定の違いを見つけられます。

画像と動きを決めると、その主人公がどんな相手と戦い、どんな攻撃を使うかも考えやすくなります。作った外見を引き継ぎ、第03回では敵の発生と行動を改造します。