Zig選択課題03:ゲーム開発

課題説明

背景

Zigは、低レベル制御と高性能を実現する言語として、ゲーム開発に適しています。C/C++で書かれた既存のゲームエンジンとの相互運用性が高く、メモリ管理の明示性により、パフォーマンスクリティカルなゲームロジックを効率的に実装できます。この課題では、Zigを使用して2Dゲームを開発し、ゲームループ、レンダリング、物理演算、衝突検出などの基本的なゲーム開発技術を学びます。

要件

必須機能

  • ゲームループ
- 固定タイムステップ - 可変フレームレート対応 - デルタタイムの計算 - FPS表示

  • レンダリングシステム
- スプライト描画 - テクスチャ管理 - レイヤー管理 - カメラシステム

  • 入力処理
- キーボード入力 - マウス入力 - ゲームパッド対応(オプション) - 入力マッピング

  • ゲームオブジェクト管理
- Entity Component System(ECS)の実装 - オブジェクトのライフサイクル管理 - コンポーネントベースの設計

  • 物理とコリジョン
- 基本的な物理演算(重力、速度) - AABB衝突検出 - 衝突応答 - タイルマップとの衝突

  • サウンド
- BGMの再生 - 効果音の再生 - ボリューム制御

技術的制約

  • Zigの標準ライブラリを使用
  • グラフィクスライブラリ:SDL2またはraylib(Zigバインディング)
  • メモリアロケータの明示的な管理
  • エラーハンドリングの適切な実装
  • 60FPS以上の安定したフレームレート
  • 評価ポイント

  • ゲームループの正確な実装
  • メモリ管理の効率性
  • コンポーネントシステムの設計品質
  • 物理演算と衝突検出の正確性
  • コードの可読性と保守性
  • パフォーマンス最適化
  • ---

    想定解答

    プロジェクト構造

    zig-game/
    ├── src/
    │   ├── main.zig
    │   ├── game.zig
    │   ├── renderer.zig
    │   ├── input.zig
    │   ├── ecs/
    │   │   ├── entity.zig
    │   │   ├── component.zig
    │   │   └── system.zig
    │   ├── physics/
    │   │   ├── collision.zig
    │   │   └── vector.zig
    │   └── assets/
    │       ├── texture.zig
    │       └── sound.zig
    ├── assets/
    │   ├── sprites/
    │   ├── sounds/
    │   └── fonts/
    ├── build.zig
    └── build.zig.zon
    

    src/main.zig

    const std = @import("std");
    const Game = @import("game.zig").Game;
    
    pub fn main() !void {
        var gpa = std.heap.GeneralPurposeAllocator(.{}){};
        defer _ = gpa.deinit();
        const allocator = gpa.allocator();
    
        var game = try Game.init(allocator);
        defer game.deinit();
    
        try game.run();
    }
    

    src/game.zig

    const std = @import("std");
    const c = @cImport({
        @cInclude("SDL2/SDL.h");
        @cInclude("SDL2/SDL_image.h");
        @cInclude("SDL2/SDL_mixer.h");
    });
    const Renderer = @import("renderer.zig").Renderer;
    const Input = @import("input.zig").Input;
    const ECS = @import("ecs/entity.zig");
    
    pub const Game = struct {
        allocator: std.mem.Allocator,
        window: *c.SDL_Window,
        renderer: Renderer,
        input: Input,
        entities: std.ArrayList(ECS.Entity),
        running: bool,
        target_fps: f64,
        frame_delay: f64,
    
        const SCREEN_WIDTH = 800;
        const SCREEN_HEIGHT = 600;
        const TARGET_FPS = 60;
    
        pub fn init(allocator: std.mem.Allocator) !Game {
            // SDL初期化
            if (c.SDL_Init(c.SDL_INIT_VIDEO | c.SDL_INIT_AUDIO) < 0) {
                std.debug.print("SDL初期化エラー: {s}\n", .{c.SDL_GetError()});
                return error.SDLInitFailed;
            }
    
            // SDL_image初期化
            const img_flags = c.IMG_INIT_PNG;
            if ((c.IMG_Init(img_flags) & img_flags) != img_flags) {
                std.debug.print("SDL_image初期化エラー: {s}\n", .{c.IMG_GetError()});
                return error.IMGInitFailed;
            }
    
            // SDL_mixer初期化
            if (c.Mix_OpenAudio(44100, c.MIX_DEFAULT_FORMAT, 2, 2048) < 0) {
                std.debug.print("SDL_mixer初期化エラー: {s}\n", .{c.Mix_GetError()});
                return error.MixerInitFailed;
            }
    
            // ウィンドウ作成
            const window = c.SDL_CreateWindow(
                "Zig Game",
                c.SDL_WINDOWPOS_CENTERED,
                c.SDL_WINDOWPOS_CENTERED,
                SCREEN_WIDTH,
                SCREEN_HEIGHT,
                c.SDL_WINDOW_SHOWN,
            ) orelse {
                std.debug.print("ウィンドウ作成エラー: {s}\n", .{c.SDL_GetError()});
                return error.WindowCreationFailed;
            };
    
            // レンダラー作成
            const sdl_renderer = c.SDL_CreateRenderer(
                window,
                -1,
                c.SDL_RENDERER_ACCELERATED | c.SDL_RENDERER_PRESENTVSYNC,
            ) orelse {
                std.debug.print("レンダラー作成エラー: {s}\n", .{c.SDL_GetError()});
                return error.RendererCreationFailed;
            };
    
            return Game{
                .allocator = allocator,
                .window = window,
                .renderer = Renderer.init(sdl_renderer, allocator),
                .input = Input.init(),
                .entities = std.ArrayList(ECS.Entity).init(allocator),
                .running = true,
                .target_fps = TARGET_FPS,
                .frame_delay = 1000.0 / TARGET_FPS,
            };
        }
    
        pub fn deinit(self: *Game) void {
            for (self.entities.items) |*entity| {
                entity.deinit();
            }
            self.entities.deinit();
    
            self.renderer.deinit();
            c.SDL_DestroyWindow(self.window);
            c.Mix_CloseAudio();
            c.IMG_Quit();
            c.SDL_Quit();
        }
    
        pub fn run(self: *Game) !void {
            try self.loadAssets();
            try self.createEntities();
    
            var last_time = c.SDL_GetTicks64();
            var frame_count: u32 = 0;
            var fps_timer: u64 = 0;
    
            while (self.running) {
                const current_time = c.SDL_GetTicks64();
                const delta_time = @as(f32, @floatFromInt(current_time - last_time)) / 1000.0;
                last_time = current_time;
    
                // FPS計算
                fps_timer += current_time - last_time;
                frame_count += 1;
                if (fps_timer >= 1000) {
                    std.debug.print("FPS: {d}\n", .{frame_count});
                    frame_count = 0;
                    fps_timer = 0;
                }
    
                // 入力処理
                self.handleInput();
    
                // 更新
                try self.update(delta_time);
    
                // 描画
                try self.render();
    
                // フレーム制限
                const frame_time = c.SDL_GetTicks64() - current_time;
                if (frame_time < self.frame_delay) {
                    c.SDL_Delay(@intFromFloat(self.frame_delay - @as(f64, @floatFromInt(frame_time))));
                }
            }
        }
    
        fn loadAssets(self: *Game) !void {
            // アセットの読み込み
            try self.renderer.loadTexture("player", "assets/sprites/player.png");
            try self.renderer.loadTexture("enemy", "assets/sprites/enemy.png");
            try self.renderer.loadTexture("background", "assets/sprites/background.png");
        }
    
        fn createEntities(self: *Game) !void {
            // プレイヤーエンティティの作成
            var player = try ECS.Entity.init(self.allocator, "player");
            try player.addComponent(.{
                .transform = .{
                    .position = .{ .x = 400, .y = 300 },
                    .size = .{ .x = 32, .y = 32 },
                },
            });
            try player.addComponent(.{
                .sprite = .{
                    .texture_name = "player",
                    .layer = 1,
                },
            });
            try player.addComponent(.{
                .physics = .{
                    .velocity = .{ .x = 0, .y = 0 },
                    .acceleration = .{ .x = 0, .y = 0 },
                    .mass = 1.0,
                },
            });
            try player.addComponent(.{
                .collider = .{
                    .bounds = .{ .x = 0, .y = 0, .w = 32, .h = 32 },
                    .is_trigger = false,
                },
            });
            try self.entities.append(player);
    
            // 敵エンティティの作成
            var i: u32 = 0;
            while (i < 5) : (i += 1) {
                var enemy = try ECS.Entity.init(self.allocator, "enemy");
                try enemy.addComponent(.{
                    .transform = .{
                        .position = .{
                            .x = @as(f32, @floatFromInt(i * 100 + 50)),
                            .y = 100,
                        },
                        .size = .{ .x = 32, .y = 32 },
                    },
                });
                try enemy.addComponent(.{
                    .sprite = .{
                        .texture_name = "enemy",
                        .layer = 1,
                    },
                });
                try self.entities.append(enemy);
            }
        }
    
        fn handleInput(self: *Game) void {
            var event: c.SDL_Event = undefined;
            while (c.SDL_PollEvent(&event) != 0) {
                switch (event.type) {
                    c.SDL_QUIT => self.running = false,
                    c.SDL_KEYDOWN => self.input.keyDown(@intCast(event.key.keysym.scancode)),
                    c.SDL_KEYUP => self.input.keyUp(@intCast(event.key.keysym.scancode)),
                    else => {},
                }
            }
        }
    
        fn update(self: *Game, delta_time: f32) !void {
            // プレイヤー入力処理
            if (self.entities.items.len > 0) {
                var player = &self.entities.items[0];
    
                if (player.getComponent(.physics)) |*physics| {
                    const speed: f32 = 200.0;
                    physics.velocity.x = 0;
                    physics.velocity.y = 0;
    
                    if (self.input.isKeyPressed(c.SDL_SCANCODE_LEFT)) {
                        physics.velocity.x = -speed;
                    }
                    if (self.input.isKeyPressed(c.SDL_SCANCODE_RIGHT)) {
                        physics.velocity.x = speed;
                    }
                    if (self.input.isKeyPressed(c.SDL_SCANCODE_UP)) {
                        physics.velocity.y = -speed;
                    }
                    if (self.input.isKeyPressed(c.SDL_SCANCODE_DOWN)) {
                        physics.velocity.y = speed;
                    }
                }
            }
    
            // 物理システムの更新
            for (self.entities.items) |*entity| {
                if (entity.getComponent(.physics)) |*physics| {
                    if (entity.getComponent(.transform)) |*transform| {
                        // 速度の適用
                        transform.position.x += physics.velocity.x * delta_time;
                        transform.position.y += physics.velocity.y * delta_time;
    
                        // 画面外に出ないように制限
                        if (transform.position.x < 0) transform.position.x = 0;
                        if (transform.position.y < 0) transform.position.y = 0;
                        if (transform.position.x > SCREEN_WIDTH - transform.size.x) {
                            transform.position.x = SCREEN_WIDTH - transform.size.x;
                        }
                        if (transform.position.y > SCREEN_HEIGHT - transform.size.y) {
                            transform.position.y = SCREEN_HEIGHT - transform.size.y;
                        }
                    }
                }
            }
    
            // 衝突検出
            try self.checkCollisions();
        }
    
        fn checkCollisions(self: *Game) !void {
            const Collision = @import("physics/collision.zig");
    
            var i: usize = 0;
            while (i < self.entities.items.len) : (i += 1) {
                var j: usize = i + 1;
                while (j < self.entities.items.len) : (j += 1) {
                    const entity_a = &self.entities.items[i];
                    const entity_b = &self.entities.items[j];
    
                    if (Collision.checkAABB(entity_a, entity_b)) {
                        std.debug.print("衝突検出: {s} と {s}\n", .{ entity_a.name, entity_b.name });
                        // 衝突応答の処理
                        try self.handleCollision(entity_a, entity_b);
                    }
                }
            }
        }
    
        fn handleCollision(self: *Game, entity_a: *ECS.Entity, entity_b: *ECS.Entity) !void {
            _ = self;
            _ = entity_a;
            _ = entity_b;
            // 衝突時の処理(例:エネミーを削除、スコア加算など)
        }
    
        fn render(self: *Game) !void {
            // 背景クリア
            try self.renderer.clear(0x1a, 0x1a, 0x2e, 0xff);
    
            // 背景描画
            try self.renderer.drawTexture("background", 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
    
            // エンティティ描画
            for (self.entities.items) |*entity| {
                if (entity.getComponent(.transform)) |transform| {
                    if (entity.getComponent(.sprite)) |sprite| {
                        try self.renderer.drawSprite(
                            sprite.texture_name,
                            @intFromFloat(transform.position.x),
                            @intFromFloat(transform.position.y),
                            @intFromFloat(transform.size.x),
                            @intFromFloat(transform.size.y),
                        );
                    }
                }
            }
    
            // 画面更新
            self.renderer.present();
        }
    };
    

    src/ecs/entity.zig

    const std = @import("std");
    
    pub const Vec2 = struct {
        x: f32,
        y: f32,
    };
    
    pub const Transform = struct {
        position: Vec2,
        size: Vec2,
        rotation: f32 = 0.0,
    };
    
    pub const Sprite = struct {
        texture_name: []const u8,
        layer: i32 = 0,
        flip_x: bool = false,
        flip_y: bool = false,
    };
    
    pub const Physics = struct {
        velocity: Vec2,
        acceleration: Vec2,
        mass: f32 = 1.0,
        drag: f32 = 0.0,
    };
    
    pub const Collider = struct {
        bounds: struct {
            x: f32,
            y: f32,
            w: f32,
            h: f32,
        },
        is_trigger: bool = false,
    };
    
    pub const ComponentType = enum {
        transform,
        sprite,
        physics,
        collider,
    };
    
    pub const Component = union(ComponentType) {
        transform: Transform,
        sprite: Sprite,
        physics: Physics,
        collider: Collider,
    };
    
    pub const Entity = struct {
        name: []const u8,
        components: std.ArrayList(Component),
        active: bool,
    
        pub fn init(allocator: std.mem.Allocator, name: []const u8) !Entity {
            return Entity{
                .name = name,
                .components = std.ArrayList(Component).init(allocator),
                .active = true,
            };
        }
    
        pub fn deinit(self: *Entity) void {
            self.components.deinit();
        }
    
        pub fn addComponent(self: *Entity, component: Component) !void {
            try self.components.append(component);
        }
    
        pub fn getComponent(self: *Entity, comptime component_type: ComponentType) ?*Component {
            for (self.components.items) |*component| {
                if (@as(ComponentType, component.*) == component_type) {
                    return component;
                }
            }
            return null;
        }
    
        pub fn hasComponent(self: *Entity, component_type: ComponentType) bool {
            return self.getComponent(component_type) != null;
        }
    };
    

    src/physics/collision.zig

    const std = @import("std");
    const ECS = @import("../ecs/entity.zig");
    
    pub fn checkAABB(entity_a: *const ECS.Entity, entity_b: *const ECS.Entity) bool {
        const transform_a = entity_a.getComponent(.transform) orelse return false;
        const transform_b = entity_b.getComponent(.transform) orelse return false;
    
        const a_left = transform_a.transform.position.x;
        const a_right = a_left + transform_a.transform.size.x;
        const a_top = transform_a.transform.position.y;
        const a_bottom = a_top + transform_a.transform.size.y;
    
        const b_left = transform_b.transform.position.x;
        const b_right = b_left + transform_b.transform.size.x;
        const b_top = transform_b.transform.position.y;
        const b_bottom = b_top + transform_b.transform.size.y;
    
        return a_left < b_right and
            a_right > b_left and
            a_top < b_bottom and
            a_bottom > b_top;
    }
    
    pub fn resolveCollision(entity_a: *ECS.Entity, entity_b: *ECS.Entity) void {
        _ = entity_a;
        _ = entity_b;
        // 衝突解決ロジック
    }
    

    ---

    解説と応用

    Entity Component System (ECS)

    ECSは、データ指向設計の代表的なパターンです:

  • Entity: ID とコンポーネントのコンテナ
  • Component: データのみを保持
  • System: ロジックを実装
  • パフォーマンス最適化

  • メモリレイアウト
- データの局所性を考慮 - Structure of Arrays (SoA) - キャッシュフレンドリーな設計

  • バッチ処理
- 同じタイプのエンティティをまとめて処理 - SIMD命令の活用

---

参考リソース

グラフィクスライブラリ

ゲーム開発理論

---

発展課題

レベル1:基本機能の拡張

  • アニメーションシステム
  • パーティクルエフェクト
  • タイルマップエディター

レベル2:高度な機能

  • シーン管理システム
  • UI システム
  • ステートマシン

レベル3:最適化

  • オブジェクトプーリング
  • 空間分割(Quadtree)
  • マルチスレッド対応

---

まとめ

Zigによるゲーム開発では、低レベル制御とメモリ安全性のバランスを取りながら、高性能なゲームを開発できます。ECSパターンとZigの言語機能を組み合わせることで、保守性の高いゲームエンジンを構築できます。