ウィンドウ管理とイベント処理
Human-Computer Interaction(HCI)の歴史
「すべてのデモの母」
1968年12月9日、サンフランシスコのフォールジョイントコンピュータカンファレンスで、Douglas Engelbart(ダグラス・エンゲルバート)は90分間のデモンストレーションを行いました。後に「The Mother of All Demos」(すべてのデモの母)と呼ばれるこのプレゼンテーションは、現代コンピューティングの基礎となる概念をほぼすべて初めて公開しました。
Engelbartが示した革新的な概念:
- マウス:画面上のカーソルを制御する入力デバイス
- ハイパーテキスト:リンクでつながったテキスト
- ウィンドウ:画面を複数の作業領域に分割
- ビデオ会議:遠隔地とのリアルタイム通信
- 共同編集:複数人による同時ドキュメント編集
Engelbartのビジョン(1962年)
"Augmenting Human Intellect: A Conceptual Framework"
人間の知性を拡張するための
コンピュータシステムの設計原理
┌─────────────────────┐
│ Human Intellect │
│ 人間の知性 │
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ Computer System │
│ コンピュータ拡張 │
└──────────┬──────────┘
│
┌──────────┬────────────┬────────────┐
▼ ▼ ▼ ▼
┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐
│ Mouse │ │Display│ │Keyboard│ │Symbols│
│マウス │ │画面 │ │キーボード│ │記号 │
└───────┘ └───────┘ └───────┘ └───────┘
Xerox PARCとGUI革命
1970年代、Xerox Palo Alto Research Center(PARC) では、Engelbartのビジョンを実用的なシステムに発展させる研究が行われました。
Alto(1973年)は、世界初のGUIを搭載したパーソナルコンピュータでした:
Xerox Alto(1973年)
┌─────────────────────────────────────┐
│ ┌─────────────────────────────┐ │
│ │ ビットマップディスプレイ │ │
│ │ │ │
│ │ ┌─────────────────────┐ │ │
│ │ │ ウィンドウ │ │ │
│ │ │ ┌──────────────────┐│ │ │
│ │ │ │ メニュー ││ │ │
│ │ │ ├──────────────────┤│ │ │
│ │ │ │ テキスト領域 ││ │ │
│ │ │ │ ││ │ │
│ │ │ └──────────────────┘│ │ │
│ │ └─────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────┘ │
├─────────────────────────────────────┤
│ キーボード + マウス │
└─────────────────────────────────────┘
PARCで開発された重要な概念:
- WIMP:Windows, Icons, Menus, Pointer
- What You See Is What You Get(WYSIWYG)
- イベント駆動プログラミング
- Smalltalkプログラミング言語
イベント駆動アーキテクチャの理論
Observerパターン
1994年、Gang of Four(Gamma, Helm, Johnson, Vlissides)は著書「Design Patterns: Elements of Reusable Object-Oriented Software」で、オブジェクト指向設計パターンを体系化しました。その中のObserverパターンは、イベント駆動プログラミングの理論的基盤となっています。
Observerパターン(GoF, 1994)
目的:オブジェクト間の1対多の依存関係を定義し、
あるオブジェクトの状態が変化したとき、
依存するすべてのオブジェクトに自動的に通知する
┌─────────────────────┐
│ Subject │
│ (イベント発生源) │
├─────────────────────┤
│ + attach(Observer) │
│ + detach(Observer) │
│ + notify() │
└─────────┬───────────┘
│ 通知
▼
┌─────┴─────┐
│ │
▼ ▼
┌───────┐ ┌───────┐
│Obs. A │ │Obs. B │
│購読者A│ │購読者B │
└───────┘ └───────┘
形式的な定義:
Observer Pattern ::= (S, O, notify, update)
where:
S = 主体(Subject)
O = {o₁, o₂, ..., oₙ} 観察者の集合(Observers)
notify: S → void (状態変化時に全観察者に通知)
update: O × State → void (各観察者の更新処理)
不変条件:
∀ o ∈ O: S.stateChanged ⟹ o.update(S.state)
Publish-Subscribeモデル
Observerパターンを分散システムに拡張したのがPublish-Subscribe(Pub/Sub)モデルです。メッセージングシステムやイベントバスの基礎となっています。
Publish-Subscribeモデル
Publishers Subscribers
発行者 購読者
┌────────┐ ┌──────────┐ ┌────────┐
│Pub A │───▶│ │───▶│Sub 1 │
└────────┘ │ Event │ └────────┘
│ Bus │
┌────────┐ │ イベント │ ┌────────┐
│Pub B │───▶│ バス │───▶│Sub 2 │
└────────┘ │ │ └────────┘
│ │
┌────────┐ │ │ ┌────────┐
│Pub C │───▶│ │───▶│Sub 3 │
└────────┘ └──────────┘ └────────┘
発行者と購読者は直接結合しない
(疎結合:Loose Coupling)
X Window Systemのイベントモデル
X11イベントアーキテクチャ
X Window System(1984年、MIT)は、クライアント-サーバーモデルに基づくウィンドウシステムです。MiniLibXはX11の上に構築されており、X11のイベントモデルを継承しています。
X Window System アーキテクチャ
┌─────────────────────────────────────────────────┐
│ Application │
│ アプリケーション │
├─────────────────────────────────────────────────┤
│ MiniLibX │
│ Xlib のラッパーライブラリ │
├─────────────────────────────────────────────────┤
│ Xlib │
│ X11クライアントライブラリ │
├─────────────────────────────────────────────────┤
│ X Protocol │
│ ネットワーク通信 │
├─────────────────────────────────────────────────┤
│ X Server │
│ ディスプレイ・入力デバイス管理 │
├─────────────────────────────────────────────────┤
│ Hardware(GPU, Input Devices) │
│ ハードウェア │
└─────────────────────────────────────────────────┘
イベントマスクとイベントキュー
X11では、アプリケーションが関心のあるイベントをイベントマスクで指定します。発生したイベントはイベントキューに格納され、アプリケーションが順次処理します。
X11イベント処理フロー
入力デバイス
┌───────────────┐
│ Keyboard │
│ Mouse │
│ etc. │
└───────┬───────┘
│ ハードウェア割り込み
▼
┌───────────────┐
│ X Server │
│ イベント生成 │
└───────┬───────┘
│ X Protocol
▼
┌───────────────┐
│ Event Queue │ ←── イベントはFIFO順序で格納
│ イベントキュー │
└───────┬───────┘
│ XNextEvent()
▼
┌───────────────┐
│ Application │
│ イベント処理 │
└───────────────┘
有限状態機械による入力処理
状態遷移理論
入力処理を体系的に設計するには、有限状態機械(Finite State Machine, FSM)の概念が有効です。
ゲームにおける入力処理のFSM:
ゲーム入力処理のFSM
States = {Idle, Moving, Interacting, Menu}
┌─────────┐
┌───▶│ Idle │◀───┐
│ │ 待機中 │ │
│ └────┬────┘ │
│ │ │
│ WASD │ │ 移動完了
│ 入力 │ │
│ ▼ │
│ ┌─────────┐ │
│ │ Moving │────┘
│ │ 移動中 │
│ └────┬────┘
│ │
│ E/Enter
│ インタラクション
│ │
│ ▼
│ ┌─────────┐
│ │Interact │
│ │ 対話中 │
│ └────┬────┘
│ │
│ 完了 │
│ │
└─────────┘
ESC → Menu状態への遷移(どの状態からでも可能)
状態パターンの実装
Gang of FourのStateパターンを使用すると、状態遷移を明確に実装できます:
// 状態パターンのC言語実装概念
typedef enum e_game_state
{
STATE_IDLE,
STATE_MOVING,
STATE_PAUSED,
STATE_GAME_OVER
} t_game_state;
typedef struct s_game
{
t_game_state state;
void (*handle_input)(struct s_game *, int keycode);
void (*update)(struct s_game *);
void (*render)(struct s_game *);
} t_game;
// 各状態に対応するハンドラ関数群
void idle_handle_input(t_game *game, int keycode);
void moving_handle_input(t_game *game, int keycode);
void paused_handle_input(t_game *game, int keycode);
void change_state(t_game *game, t_game_state new_state)
{
game->state = new_state;
// 状態に応じた関数ポインタの設定
if (new_state == STATE_IDLE)
{
game->handle_input = idle_handle_input;
// ...
}
}
ゲームループの理論
ゲームプログラミングパターン
Robert Nystromの著書「Game Programming Patterns」(2014年)は、ゲーム開発における設計パターンを体系化した重要な文献です。特にGame Loopパターンは、リアルタイムアプリケーションの基本構造を定義しています。
ゲームループの基本構造(Nystrom, 2014)
┌─────────────────────────────────────────┐
│ while (game.running) │
│ { │
│ process_input(); │
│ update(); │
│ render(); │
│ } │
└─────────────────────────────────────────┘
各フェーズの責務:
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Process Input │ │ Update │ │ Render │
│ 入力処理 │ │ 状態更新 │ │ 描画 │
├─────────────────┤ ├─────────────────┤ ├─────────────────┤
│・キーボード │ │・物理演算 │ │・画面クリア │
│・マウス │ │・AI処理 │ │・スプライト描画 │
│・ゲームパッド │ │・衝突判定 │ │・UI描画 │
│・ネットワーク │ │・ゲームロジック │ │・バッファスワップ│
└─────────────────┘ └─────────────────┘ └─────────────────┘
固定タイムステップ vs 可変タイムステップ
ゲームループには主に2つのアプローチがあります:
1. 固定タイムステップ(Fixed Timestep)
メリット:
- 物理演算の決定論的な結果
- 再現性のあるゲームプレイ
- デバッグが容易
デメリット:
- CPUが遅いと遅延
- CPUが速いと待機時間が発生
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ U │ R │ U │ R │ U │ R │ U │ R │ → 時間
└───┴───┴───┴───┴───┴───┴───┴───┘
16ms 16ms 16ms 16ms ...
U = Update, R = Render
2. 可変タイムステップ(Variable Timestep)
メリット:
- ハードウェアに適応
- 待機時間なし
デメリット:
- 非決定論的
- 物理演算の精度問題
┌───┬────┬──┬─────┬──┐
│ U │ R │U │ R │U │ → 時間
└───┴────┴──┴─────┴──┘
12ms 18ms 8ms 25ms ...
delta_time = 前フレームからの経過時間
セミ固定タイムステップ
現代のゲームエンジンでは、両方の利点を組み合わせたセミ固定タイムステップが使用されます:
// セミ固定タイムステップの擬似コード
double current_time = get_time();
double accumulator = 0.0;
const double DT = 1.0 / 60.0; // 60 Hz
while (game_running)
{
double new_time = get_time();
double frame_time = new_time - current_time;
current_time = new_time;
accumulator += frame_time;
// 固定タイムステップで更新
while (accumulator >= DT)
{
update_physics(DT);
update_game_logic(DT);
accumulator -= DT;
}
// 可変タイムステップで描画
// 補間により滑らかな表示
double alpha = accumulator / DT;
render(alpha);
}
Commandパターンによる入力抽象化
入力とアクションの分離
Gang of FourのCommandパターンを入力処理に適用すると、キー割り当ての変更やリプレイ機能の実装が容易になります。
Commandパターン(入力処理への適用)
┌─────────────┐ ┌─────────────┐
│ Input │ │ Command │
│ Handler │ ───▶ │ Interface │
│ 入力処理 │ │ コマンドIF │
└─────────────┘ └──────┬──────┘
│
┌──────────────────┼──────────────────┐
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│ MoveUp │ │ MoveDown │ │ Jump │
│ Command │ │ Command │ │ Command │
└───────────┘ └───────────┘ └───────────┘
利点:
1. 入力デバイスとゲームアクションの疎結合
2. キーリマッピングが容易
3. 入力履歴の記録(リプレイ機能)
4. アンドゥ/リドゥの実装が可能
// Commandパターンのシンプルな実装
typedef struct s_command
{
void (*execute)(t_game *game);
} t_command;
void move_up_execute(t_game *game)
{
try_move_player(game, 0, -1);
}
void move_down_execute(t_game *game)
{
try_move_player(game, 0, 1);
}
// キーバインディングテーブル
t_command *key_bindings[256];
void init_key_bindings(void)
{
static t_command move_up = { move_up_execute };
static t_command move_down = { move_down_execute };
key_bindings[KEY_W] = &move_up;
key_bindings[KEY_UP] = &move_up;
key_bindings[KEY_S] = &move_down;
key_bindings[KEY_DOWN] = &move_down;
}
// 入力処理
int handle_input(int keycode, t_game *game)
{
if (keycode >= 0 && keycode < 256 && key_bindings[keycode])
key_bindings[keycode]->execute(game);
return (0);
}
MiniLibXイベントシステム
イベントフック関数
理論的背景を理解した上で、MiniLibXのイベントシステムを見てみましょう。
int mlx_hook(void *win_ptr, int x_event, int x_mask,
int (*funct)(), void *param);
この関数は、X11のイベント処理をラップしています:
| パラメータ | 説明 | 理論的対応 | |-----------|------|-----------| | win_ptr | ウィンドウポインタ | Subject(イベント発生源) | | x_event | X11イベント番号 | イベントタイプ | | x_mask | イベントマスク | フィルタリング条件 | | funct | コールバック関数 | Observer.update() | | param | ユーザーデータ | Observer状態 |
X11イベント番号
// X11イベント番号(X.h由来)
// キーボードイベント
#define KeyPress 2 // キー押下
#define KeyRelease 3 // キー解放
// マウスイベント
#define ButtonPress 4 // ボタン押下
#define ButtonRelease 5 // ボタン解放
#define MotionNotify 6 // マウス移動
// ウィンドウイベント
#define Expose 12 // 再描画要求
#define DestroyNotify 17 // ウィンドウ破棄
// フォーカスイベント
#define FocusIn 9 // フォーカス取得
#define FocusOut 10 // フォーカス喪失
イベントハンドラの実装
#include <mlx.h>
#include <stdlib.h>
#define KEY_PRESS 2
#define KEY_RELEASE 3
#define DESTROY_NOTIFY 17
#define KEY_ESC 53 // macOS
// #define KEY_ESC 65307 // Linux
typedef struct s_game
{
void *mlx_ptr;
void *win_ptr;
int running;
} t_game;
// キー押下ハンドラ(Observer.update()に相当)
int handle_keypress(int keycode, t_game *game)
{
if (keycode == KEY_ESC)
{
game->running = 0;
mlx_destroy_window(game->mlx_ptr, game->win_ptr);
exit(0);
}
return (0);
}
// ウィンドウ閉じるボタンのハンドラ
int handle_close(t_game *game)
{
game->running = 0;
mlx_destroy_window(game->mlx_ptr, game->win_ptr);
exit(0);
return (0);
}
int main(void)
{
t_game game;
game.mlx_ptr = mlx_init();
game.win_ptr = mlx_new_window(game.mlx_ptr, 800, 600, "Event Demo");
game.running = 1;
// Observerの登録(attach)
mlx_hook(game.win_ptr, KEY_PRESS, 0, handle_keypress, &game);
mlx_hook(game.win_ptr, DESTROY_NOTIFY, 0, handle_close, &game);
// イベントループ開始(Subjectのnotify()を継続的に呼び出す)
mlx_loop(game.mlx_ptr);
return (0);
}
So_longにおけるゲームループ実装
mlx_loop_hook による継続的更新
int mlx_loop_hook(void *mlx_ptr, int (*funct)(), void *param);
この関数は、イベントキューが空の時に呼び出されるコールバックを設定します。ゲームループのupdate()とrender()フェーズに対応します。
typedef struct s_game
{
void *mlx_ptr;
void *win_ptr;
t_map map;
int moves;
int frame_count;
} t_game;
// ゲームループの主処理
int game_loop(t_game *game)
{
game->frame_count++;
// ここでアニメーション更新などを行う
// update_animations(game);
// 必要に応じて再描画
// if (needs_redraw(game))
// render_game(game);
return (0);
}
int main(void)
{
t_game game;
init_game(&game, "maps/level1.ber");
// イベントハンドラの登録
mlx_hook(game.win_ptr, KEY_PRESS, 0, handle_input, &game);
mlx_hook(game.win_ptr, DESTROY_NOTIFY, 0, handle_close, &game);
// ゲームループの登録
mlx_loop_hook(game.mlx_ptr, game_loop, &game);
// 初回描画
render_game(&game);
// イベントループ開始
mlx_loop(game.mlx_ptr);
return (0);
}
プラットフォーム間のキーコード差異
キーコード抽象化
異なるプラットフォームでは、同じキーでも異なるコードが使用されます。条件付きコンパイルで対応します:
// keys.h - プラットフォーム抽象化
#ifndef KEYS_H
# define KEYS_H
# ifdef __APPLE__
// macOS (Carbon HIToolbox KeyCodes)
# define KEY_ESC 53
# define KEY_W 13
# define KEY_A 0
# define KEY_S 1
# define KEY_D 2
# define KEY_UP 126
# define KEY_DOWN 125
# define KEY_LEFT 123
# define KEY_RIGHT 124
# elif defined(__linux__)
// Linux (X11 keysym)
# define KEY_ESC 65307
# define KEY_W 119
# define KEY_A 97
# define KEY_S 115
# define KEY_D 100
# define KEY_UP 65362
# define KEY_DOWN 65364
# define KEY_LEFT 65361
# define KEY_RIGHT 65363
# endif
#endif
入力ハンドラの実装
// Commandパターンを簡略化した実装
int handle_input(int keycode, t_game *game)
{
int dx;
int dy;
dx = 0;
dy = 0;
// 入力を方向ベクトルに変換
if (keycode == KEY_W || keycode == KEY_UP)
dy = -1;
else if (keycode == KEY_S || keycode == KEY_DOWN)
dy = 1;
else if (keycode == KEY_A || keycode == KEY_LEFT)
dx = -1;
else if (keycode == KEY_D || keycode == KEY_RIGHT)
dx = 1;
else if (keycode == KEY_ESC)
return (close_game(game));
// 移動の試行
if (dx != 0 || dy != 0)
try_move(game, dx, dy);
return (0);
}
void try_move(t_game *game, int dx, int dy)
{
int new_x;
int new_y;
new_x = game->player.x + dx;
new_y = game->player.y + dy;
if (is_valid_move(game, new_x, new_y))
{
execute_move(game, new_x, new_y);
render_game(game);
}
}
衝突判定と移動検証
2Dタイルベース衝突判定
タイルベースゲームでは、グリッド座標を使用した衝突判定がシンプルかつ効率的です。
タイルベース衝突判定
グリッドマップ:
0 1 2 3 4 5
0 1 1 1 1 1 1
1 1 0 C 0 0 1 C = Collectible
2 1 P 0 0 0 1 P = Player
3 1 0 0 1 E 1 E = Exit
4 1 1 1 1 1 1 1 = Wall, 0 = Floor
移動可能条件:
1. 移動先がマップ境界内
2. 移動先が壁(1)でない
3. 移動先が出口(E)の場合、全収集物を取得済み
// 移動検証関数
int is_valid_move(t_game *game, int x, int y)
{
// 境界チェック
if (x < 0 || x >= game->map.width)
return (0);
if (y < 0 || y >= game->map.height)
return (0);
// 壁との衝突チェック
if (game->map.grid[y][x] == WALL)
return (0);
// 出口条件チェック
if (game->map.grid[y][x] == EXIT)
{
if (game->collected < game->total_collectibles)
return (0);
}
return (1);
}
移動の実行
void execute_move(t_game *game, int new_x, int new_y)
{
char destination_tile;
// 移動先タイルの取得
destination_tile = game->map.grid[new_y][new_x];
// 現在位置を床に戻す
game->map.grid[game->player.y][game->player.x] = FLOOR;
// 収集物の処理
if (destination_tile == COLLECTIBLE)
{
game->collected++;
ft_printf("Collected: %d/%d\n", game->collected,
game->total_collectibles);
}
// ゲームクリア判定
if (destination_tile == EXIT &&
game->collected == game->total_collectibles)
{
game->moves++;
ft_printf("Victory! Total moves: %d\n", game->moves);
cleanup_and_exit(game, EXIT_SUCCESS);
}
// プレイヤー位置の更新
game->player.x = new_x;
game->player.y = new_y;
game->map.grid[new_y][new_x] = PLAYER;
// 移動カウントの更新と表示
game->moves++;
ft_printf("Moves: %d\n", game->moves);
}
レンダリングの最適化
ダーティ矩形アルゴリズム
画面全体を毎フレーム再描画するのは非効率です。ダーティ矩形(Dirty Rectangle)アルゴリズムは、変更があった領域のみを再描画します。
ダーティ矩形アルゴリズム
基本原理:
1. 変更があった領域を「ダーティ」としてマーク
2. 描画時はダーティ領域のみを更新
3. 描画後、ダーティフラグをクリア
変更前 変更後
┌───────────┐ ┌───────────┐
│ . . . . . │ │ . . . . . │
│ . P . . . │ │ . × . . . │ P→の移動で
│ . . . . . │ → │ . . P . . │ 2箇所のみ再描画
│ . . . . . │ │ . . . . . │
└───────────┘ └───────────┘
↑
ダーティ領域
// 効率的な再描画
void render_player_move(t_game *game, int old_x, int old_y,
int new_x, int new_y)
{
// 旧位置を床で描画
render_tile(game, old_x, old_y, game->textures.floor);
// 新位置をプレイヤーで描画
render_tile(game, new_x, new_y, game->textures.player);
}
void render_tile(t_game *game, int grid_x, int grid_y, void *image)
{
int pixel_x;
int pixel_y;
pixel_x = grid_x * TILE_SIZE;
pixel_y = grid_y * TILE_SIZE;
mlx_put_image_to_window(game->mlx_ptr, game->win_ptr,
image, pixel_x, pixel_y);
}
全画面再描画
マップの初期表示やエクスポーズイベント時は、全画面を描画します:
void render_game(t_game *game)
{
int x;
int y;
y = 0;
while (y < game->map.height)
{
x = 0;
while (x < game->map.width)
{
render_tile_at(game, x, y);
x++;
}
y++;
}
}
void render_tile_at(t_game *game, int x, int y)
{
void *img;
char tile;
tile = game->map.grid[y][x];
img = select_texture(game, tile);
render_tile(game, x, y, img);
}
void *select_texture(t_game *game, char tile)
{
if (tile == WALL)
return (game->textures.wall);
if (tile == PLAYER)
return (game->textures.player);
if (tile == COLLECTIBLE)
return (game->textures.collectible);
if (tile == EXIT)
{
if (game->collected == game->total_collectibles)
return (game->textures.exit_open);
return (game->textures.exit_closed);
}
return (game->textures.floor);
}
RAIIとリソース管理
Resource Acquisition Is Initialization
RAII(Resource Acquisition Is Initialization)は、Bjarne Stroustrupが提唱したC++のイディオムですが、C言語でも原則を適用できます。リソース(メモリ、ファイルハンドル、グラフィックスオブジェクト)の獲得と解放を対にして管理します。
RAII原則のCにおける適用
初期化関数 解放関数
init_xxx() ←→ cleanup_xxx()
┌────────────────┐ ┌────────────────┐
│ mlx_init() │ → │ free() │
│ mlx_new_window│ → │mlx_destroy_win │
│ mlx_xpm_file │ → │mlx_destroy_img │
│ malloc() │ → │ free() │
└────────────────┘ └────────────────┘
適切なクリーンアップ実装
// リソース管理構造体
typedef struct s_game
{
void *mlx_ptr;
void *win_ptr;
t_textures textures;
t_map map;
int moves;
} t_game;
// 初期化(リソース獲得)
int init_game(t_game *game, char *map_file)
{
ft_bzero(game, sizeof(t_game));
game->mlx_ptr = mlx_init();
if (!game->mlx_ptr)
return (error_exit("MLX init failed", NULL));
if (!load_map(&game->map, map_file))
return (cleanup_game(game), 0);
game->win_ptr = mlx_new_window(game->mlx_ptr,
game->map.width * TILE_SIZE,
game->map.height * TILE_SIZE,
"so_long");
if (!game->win_ptr)
return (cleanup_game(game), 0);
if (!load_textures(game))
return (cleanup_game(game), 0);
return (1);
}
// 解放(リソース解放)
void cleanup_game(t_game *game)
{
// テクスチャの解放
destroy_textures(game);
// マップの解放
free_map(&game->map);
// ウィンドウの破棄
if (game->win_ptr && game->mlx_ptr)
mlx_destroy_window(game->mlx_ptr, game->win_ptr);
// Linux版MiniLibX特有の処理
#ifdef __linux__
if (game->mlx_ptr)
{
mlx_destroy_display(game->mlx_ptr);
free(game->mlx_ptr);
}
#endif
}
void destroy_textures(t_game *game)
{
if (!game->mlx_ptr)
return;
if (game->textures.wall)
mlx_destroy_image(game->mlx_ptr, game->textures.wall);
if (game->textures.floor)
mlx_destroy_image(game->mlx_ptr, game->textures.floor);
if (game->textures.player)
mlx_destroy_image(game->mlx_ptr, game->textures.player);
if (game->textures.collectible)
mlx_destroy_image(game->mlx_ptr, game->textures.collectible);
if (game->textures.exit_open)
mlx_destroy_image(game->mlx_ptr, game->textures.exit_open);
if (game->textures.exit_closed)
mlx_destroy_image(game->mlx_ptr, game->textures.exit_closed);
}
終了処理の統合
複数の終了パスの統合
ゲームには複数の終了方法があります:
- ESCキー押下
- ウィンドウの×ボタン
- ゲームクリア
これらすべてを統一した関数で処理します:
// 統一された終了処理
int close_game(t_game *game)
{
cleanup_game(game);
exit(EXIT_SUCCESS);
return (0); // mlx_hookの戻り値要件
}
// キー入力ハンドラ
int handle_input(int keycode, t_game *game)
{
// ESCキーで終了
if (keycode == KEY_ESC)
return (close_game(game));
// 移動処理...
// ...
return (0);
}
// ウィンドウ閉じるボタン
int handle_destroy(t_game *game)
{
return (close_game(game));
}
// メイン関数
int main(int argc, char **argv)
{
t_game game;
if (argc != 2)
return (error_exit("Usage: ./so_long map.ber", NULL));
if (!init_game(&game, argv[1]))
return (EXIT_FAILURE);
// イベントハンドラ登録
mlx_hook(game.win_ptr, KEY_PRESS, 0, handle_input, &game);
mlx_hook(game.win_ptr, DESTROY_NOTIFY, 0, handle_destroy, &game);
mlx_hook(game.win_ptr, EXPOSE, 0, handle_expose, &game);
// 初回描画
render_game(&game);
// イベントループ開始
mlx_loop(game.mlx_ptr);
return (EXIT_SUCCESS);
}
まとめ
この章では、HCIの歴史からゲームループの実装まで、イベント駆動プログラミングの理論と実践を学びました:
これらの概念は、So_longに限らず、あらゆるインタラクティブアプリケーション開発で活用できる普遍的な知識です。
次の章では、マップファイルの解析と検証について学びます。.berファイルの読み込み、マップの妥当性チェック、そしてFlood Fillアルゴリズムを使用した経路検証を実装します。