CompositionTarget.Renderingを使いキャラクタが動くサンプルコードを作成しました。
ソースコード
ファイル名:WalkAnime.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
</PropertyGroup>
</Project>
ファイル名:MainWindow.xaml.cs
<Window x:Class="WalkAnime.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WalkAnime"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid Background="Black">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock x:Name="FpsText"
Grid.Row="0"
Foreground="Lime"
FontSize="20"
Margin="8"
HorizontalAlignment="Left"/>
<Image x:Name="GameImage"
Grid.Row="1"
Stretch="Uniform"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Grid>
</Window>
ファイル名:MainWindow.xaml.cs
using System.Diagnostics;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace WalkAnime;
public partial class MainWindow : Window
{
const int SCR_WIDTH = 640;
const int SCR_HEIGHT = 400;
// 固定フレーム(60FPS)
const double FIXED_DT = 1.0 / 60.0;
// ダブルバッファ
private readonly WriteableBitmap[] _buffers =
{
new(SCR_WIDTH, SCR_HEIGHT, 96, 96, PixelFormats.Bgra32, null),
new(SCR_WIDTH, SCR_HEIGHT, 96, 96, PixelFormats.Bgra32, null),
};
private int _bufferIndex = 0;
private WriteableBitmap BackBuffer => _buffers[_bufferIndex % 2];
private WriteableBitmap FrontBuffer => _buffers[(_bufferIndex + 1) % 2];
// 背景色
private readonly byte[] _bgColor = { 0, 128, 0, 255 }; // BGRA
// 時間計測用
private readonly Stopwatch _stopwatch = new();
private double _prevTimeSec = 0.0;
private double _accumulator = 0.0;
// FPS表示用(描画側のFPS)
private double _renderFps = 0.0;
// キャラ状態
private double _playerX = SCR_WIDTH / 2.0;
private double _playerY = SCR_HEIGHT - 40;
private double _playerSpeed = 60.0; // px/sec(ゲームロジックは60FPS固定)
private int _animCounter = 0;
private double _animElapsed = 0.0;
public MainWindow()
{
InitializeComponent();
GameImage.Source = FrontBuffer;
_stopwatch.Start();
_prevTimeSec = 0.0;
CompositionTarget.Rendering += OnRendering;
}
private void OnRendering(object? sender, EventArgs e)
{
// 経過時間(実時間)
double nowSec = _stopwatch.Elapsed.TotalSeconds;
double frameDt = nowSec - _prevTimeSec;
_prevTimeSec = nowSec;
// 描画FPS計測(オマケ)
if (frameDt > 0)
_renderFps = 1.0 / frameDt;
// 固定ステップ用に貯める
_accumulator += frameDt;
// 「重すぎて dt が跳ねた」場合の保険(無限ループ防止)
const int MAX_STEPS = 5;
int steps = 0;
// FIXED_DT(1/60秒)ごとにロジック更新
while (_accumulator >= FIXED_DT && steps < MAX_STEPS)
{
UpdateFixed(FIXED_DT);
_accumulator -= FIXED_DT;
steps++;
}
// 描画は1回だけ(現在の状態をそのまま描く)
DrawScene(BackBuffer);
_bufferIndex++;
GameImage.Source = FrontBuffer;
FpsText.Text = $"Render: {_renderFps:0.0} FPS (Logic: 60 FPS)";
}
// ---------------- 固定ステップのゲームロジック ----------------
private void UpdateFixed(double dt)
{
// 左→右へ一定速度で歩く(60FPSでも144FPSでも同じゲーム速度)
_playerX += _playerSpeed * dt;
if (_playerX > SCR_WIDTH + 20)
_playerX = -20;
// 歩きアニメ(0.1秒ごとに足のパターンを切り替え)
_animElapsed += dt;
if (_animElapsed >= 0.1)
{
_animElapsed -= 0.1;
_animCounter++;
}
}
// ---------------- 描画 ----------------
private void DrawScene(WriteableBitmap wb)
{
int stride = SCR_WIDTH * 4;
byte[] pixels = new byte[SCR_HEIGHT * stride];
// 背景クリア
for (int i = 0; i < pixels.Length; i += 4)
{
pixels[i + 0] = _bgColor[0];
pixels[i + 1] = _bgColor[1];
pixels[i + 2] = _bgColor[2];
pixels[i + 3] = 255;
}
// 地面ライン
DrawRect(pixels, stride, 0, SCR_HEIGHT - 10, SCR_WIDTH, 10, 0, 0, 0);
// キャラクタ
DrawPlayer(pixels, stride);
wb.WritePixels(new Int32Rect(0, 0, SCR_WIDTH, SCR_HEIGHT), pixels, stride, 0);
}
private void DrawPlayer(byte[] pixels, int stride)
{
int wBody = 16;
int hBody = 24;
int wLeg = 6;
int hLeg = 10;
int centerX = (int)_playerX;
int baseY = (int)_playerY;
// 体
int bodyX = centerX - wBody / 2;
int bodyY = baseY - hBody - hLeg;
DrawRect(pixels, stride, bodyX, bodyY, wBody, hBody, 0, 0, 255);
// 頭
int headSize = 10;
int headX = centerX - headSize / 2;
int headY = bodyY - headSize;
DrawRect(pixels, stride, headX, headY, headSize, headSize, 255, 224, 192);
// 足(2コマ切り替え)
bool frameEven = (_animCounter % 2) == 0;
if (frameEven)
{
// 左足前、右足後ろ
DrawRect(pixels, stride,
centerX - wLeg - 2, baseY - hLeg,
wLeg, hLeg, 0, 0, 0);
DrawRect(pixels, stride,
centerX + 2, baseY - hLeg,
wLeg, hLeg, 0, 0, 0);
}
else
{
// 右足前、左足後ろ
DrawRect(pixels, stride,
centerX - wLeg - 2, baseY - hLeg,
wLeg, hLeg, 0, 0, 0);
DrawRect(pixels, stride,
centerX + 2 + 4, baseY - hLeg,
wLeg, hLeg, 0, 0, 0);
}
}
/// <summary>単色矩形描画(BGRA)</summary>
private static void DrawRect(
byte[] pixels,
int stride,
int x,
int y,
int w,
int h,
byte r,
byte g,
byte b)
{
for (int yy = 0; yy < h; yy++)
{
int py = y + yy;
if (py < 0 || py >= SCR_HEIGHT) continue;
int rowStart = py * stride;
for (int xx = 0; xx < w; xx++)
{
int px = x + xx;
if (px < 0 || px >= SCR_WIDTH) continue;
int idx = rowStart + px * 4;
pixels[idx + 0] = b;
pixels[idx + 1] = g;
pixels[idx + 2] = r;
pixels[idx + 3] = 255;
}
}
}
}
実行
実行すると、矩形で書かれたキャラクタが左から右へ移動するアニメーションが表示されます。
60FPS固定ルーチン
_accumulator … 直前のOnRenderingからの経過時間
FIXED_DT … 1フレームの時間 1.0/60
// 固定ステップ用に貯める
_accumulator += frameDt;
_accumulatorに経過時間を加算
while ( _accumulator >= FIXED_DT ..._accumulator直前のループからFIXED_DT以上経過した場合{}無いの処理を行う。UpdateFixed(FIXED_DT);更新描画処理。ここが秒間60回呼ばれるようにするのが目的_accumulator -= FIXED_DT;_accumulatorからFIXED_DTを減算することでループを抜け、次のOnRenderingに備える。このルーチンにより144Hzのモニターでも60FPSで動作するようになります。タイミングを調整するために待ち処理を行っていいるので、CPU時間的には効率が悪いです。タイミングが合わない場合、メソッドを抜けるコードにすることも考えられますが、イベントドリブンの精度を考えると、今回の方が現実的だと思います。
ちなみにstep関係のルーチンは、処理落ちが発生した場合、無限ループにならないようにする為の機構です。


コメント