本格的なゲームループ

いいですね!🎮
ここから「本格的なゲームループ」へ移行すれば、処理の管理がしやすくなり、FPS制御・時間経過管理・状態制御などが柔軟になります。


目次

✅ ゲームループ構成にする理由

現状は System.Timers.Timer を使ってる簡易ループですが、本格ゲームでは以下が重要になります:

必要な要素説明
Update()ロジック更新(位置計算など)
Render()描画処理
FixedTimeStep安定した物理演算
DeltaTime時間経過に基づくスムーズな動き
高FPS対応画面更新とロジックを分離し、60FPS以上にも対応

🧱 ゲームループ構成:実装概要

GTKはUIスレッドが必要なので、GLib.Timeout.Add() を使って描画と更新の統合ループを作成します。


💡 改良ポイント

🔁 GLib.Timeout.Add() でゲームループ開始

GLib.Timeout.Add(16, () => {
    double now = GetCurrentTime();
    double deltaTime = now - lastTime;
    lastTime = now;

    Update(deltaTime);
    drawingArea.QueueDraw();
    return true;
});

⏱ 時間取得(ミリ秒)

private double GetCurrentTime()
{
    return DateTime.Now.Ticks / (double)TimeSpan.TicksPerSecond;
}

🧠 Update(deltaTime) 関数でロジック制御

private void Update(double deltaTime)
{
    foreach (var img in images)
    {
        img.X += img.DX * deltaTime * Speed;
        img.Y += img.DY * deltaTime * Speed;

        // 壁でバウンド
        if (img.X < 0 || img.X + image.Width > Width) img.DX = -img.DX;
        if (img.Y < 0 || img.Y + image.Height > Height) img.DY = -img.DY;
    }

    // 衝突判定(略)
}

Main()でループ開始

lastTime = GetCurrentTime();
GLib.Timeout.Add(16, GameLoop);

🔁 フルリファクタ版をご希望なら…

「ゲームループ対応のフルコード」も提供できます!

次のようなスタイルで構築:

  • Game クラスを作る
  • Game.Update(deltaTime)
  • Game.Render(context)
  • イベントも分離

🎁 提供可能なフルコード

  • ✅ 本格ゲームループ
  • ✅ DeltaTime による滑らかさ
  • ✅ マルチ画像
  • ✅ 壁バウンド&衝突判定
  • ✅ クリックでスポーン

サンプルコード

GTK3 + C# + 本格ゲームループ構成のフルサンプルをお届けします!


✅ このサンプルで実装されていること

  • ゲームループ:GLib の Timeout.Add による毎フレーム処理
  • DeltaTime:経過時間に応じた移動(滑らかな動き)
  • 画像描画:Pixbuf を使って画像を描画
  • マウスクリックで画像追加
  • 壁バウンド&衝突判定
  • 全てがクラスで整理

🧾 GameApp.cs(プロジェクト直下に保存)

using Gtk;
using Cairo;
using Gdk;
using System;
using System.Collections.Generic;

public class GameApp : Window
{
    private DrawingArea drawingArea;
    private Pixbuf image;
    private Game game;

    public GameApp() : base("GTK3 - Game Loop Sample")
    {
        SetDefaultSize(800, 600);
        SetPosition(WindowPosition.Center);
        DeleteEvent += (o, args) => Application.Quit();

        drawingArea = new DrawingArea();
        drawingArea.AddEvents((int)EventMask.ButtonPressMask);
        drawingArea.Drawn += OnDraw;
        drawingArea.ButtonPressEvent += OnClick;

        Add(drawingArea);
        ShowAll();

        image = new Pixbuf("icon.png");
        game = new Game(image, drawingArea.AllocatedWidth, drawingArea.AllocatedHeight);

        // ゲームループ開始
        game.Start(() => drawingArea.QueueDraw());
    }

    private void OnClick(object o, ButtonPressEventArgs args)
    {
        game.Spawn(args.Event.X, args.Event.Y);
    }

    private void OnDraw(object o, DrawnArgs args)
    {
        Context cr = args.Cr;
        game.Render(cr);
    }

    public static void Main()
    {
        Application.Init();
        new GameApp();
        Application.Run();
    }
}

🧱 Game.cs(同じくプロジェクト内に)

using Cairo;
using Gdk;
using System;
using System.Collections.Generic;

public class Game
{
    private List<MovingImage> images = new List<MovingImage>();
    private Pixbuf image;
    private int width, height;
    private double lastTime;
    private const double Speed = 100.0; // pixels per second

    public Game(Pixbuf img, int width, int height)
    {
        this.image = img;
        this.width = width;
        this.height = height;
    }

    public void Start(Action requestDraw)
    {
        lastTime = GetTime();

        GLib.Timeout.Add(16, () =>
        {
            double now = GetTime();
            double delta = now - lastTime;
            lastTime = now;

            Update(delta);
            requestDraw();

            return true;
        });
    }

    public void Spawn(double x, double y)
    {
        var rnd = new Random();
        double dx = rnd.NextDouble() * 2 - 1;
        double dy = rnd.NextDouble() * 2 - 1;
        images.Add(new MovingImage(x, y, dx, dy));
    }

    public void Update(double dt)
    {
        foreach (var img in images)
        {
            img.X += img.DX * Speed * dt;
            img.Y += img.DY * Speed * dt;

            // 壁バウンド
            if (img.X < 0 || img.X + image.Width > width) img.DX = -img.DX;
            if (img.Y < 0 || img.Y + image.Height > height) img.DY = -img.DY;
        }

        // 簡単な衝突判定
        for (int i = 0; i < images.Count; i++)
        {
            for (int j = i + 1; j < images.Count; j++)
            {
                var a = images[i];
                var b = images[j];

                if (IsColliding(a, b))
                {
                    (a.DX, b.DX) = (b.DX, a.DX);
                    (a.DY, b.DY) = (b.DY, a.DY);
                }
            }
        }
    }

    public void Render(Context cr)
    {
        // 背景
        cr.SetSourceRGB(1, 1, 1);
        cr.Paint();

        // 画像を描画
        foreach (var img in images)
        {
            Gdk.CairoHelper.SetSourcePixbuf(cr, image, img.X, img.Y);
            cr.Paint();
        }
    }

    private bool IsColliding(MovingImage a, MovingImage b)
    {
        return !(a.X + image.Width < b.X ||
                 a.X > b.X + image.Width ||
                 a.Y + image.Height < b.Y ||
                 a.Y > b.Y + image.Height);
    }

    private double GetTime()
    {
        return DateTime.Now.Ticks / (double)TimeSpan.TicksPerSecond;
    }

    private class MovingImage
    {
        public double X, Y, DX, DY;
        public MovingImage(double x, double y, double dx, double dy)
        {
            X = x; Y = y; DX = dx; DY = dy;
        }
    }
}

🛠 ビルド方法(Linux)

mcs -pkg:gtk-sharp-3.0 -out:GameApp.exe GameApp.cs Game.cs
mono GameApp.exe

✅ 次にやってみる?

  • 🎮 キーボードで移動するプレイヤー追加
  • 🧠 状態(ゲームスタート/ゲームオーバー)
  • 🎵 音追加
  • 🎨 画像アニメーション(SpriteSheet)

気になる拡張あれば言ってね!
「敵を追いかけてくる」とか「スコア表示」とか、何でも追加できます!🔥

よかったらシェアしてね!
目次