Zig選択課題03:ゲーム開発
課題説明
背景
Zigは、低レベル制御と高性能を実現する言語として、ゲーム開発に適しています。C/C++で書かれた既存のゲームエンジンとの相互運用性が高く、メモリ管理の明示性により、パフォーマンスクリティカルなゲームロジックを効率的に実装できます。この課題では、Zigを使用して2Dゲームを開発し、ゲームループ、レンダリング、物理演算、衝突検出などの基本的なゲーム開発技術を学びます。
要件
必須機能
- ゲームループ
- レンダリングシステム
- 入力処理
- ゲームオブジェクト管理
- 物理とコリジョン
- サウンド
技術的制約
- 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は、データ指向設計の代表的なパターンです:
パフォーマンス最適化
- バッチ処理
---
参考リソース
グラフィクスライブラリ
ゲーム開発理論
---
発展課題
レベル1:基本機能の拡張
- アニメーションシステム
- パーティクルエフェクト
- タイルマップエディター
レベル2:高度な機能
- シーン管理システム
- UI システム
- ステートマシン
レベル3:最適化
- オブジェクトプーリング
- 空間分割(Quadtree)
- マルチスレッド対応
---
まとめ
Zigによるゲーム開発では、低レベル制御とメモリ安全性のバランスを取りながら、高性能なゲームを開発できます。ECSパターンとZigの言語機能を組み合わせることで、保守性の高いゲームエンジンを構築できます。