前回、画面表示に必要な機構を準備しました。今回は車のスプライトを動かします。
実装
学習済みQテーブルの実行
スプライトの動きの実装に集中するため、まずはPCで学習したQテーブルを用いて車の運動を計算し、うまく坂を上るアニメーションを実現します。
Qテーブルをソフトに埋め込むため、PCで学習した結果得られたQLearningState
構造体の内容を、そのままC言語の定数配列に変換します。
const unsigned char pc_trained_params[] = {0xED, 0xEF, ...}
以下のコードで、Qテーブルに従って行動を決めて車の座標を計算し、スプライトの座標に反映します。MountainCar環境自体はx座標しか考慮しませんが、自然な表示を実現するため、地面のカーブに沿ってy座標も計算します。初期化処理以外はすべてVBLANK割り込みハンドラで処理しています。割り込みは1秒間に60回呼び出されますが、車の動きが速すぎるため、2回に1回だけ更新処理を実行します。表示に関する状態は、RunPretrainedState
という型にまとめ、グローバル変数にそのポインタを置きました。この例ではポインタではなく実体を静的に確保しても構わないのですが、後で実装する学習モードを実行する場合には不要な領域となりますので動的確保しています。しかし実際のところ、malloc
の割り当て管理用のメモリ領域のほうが大きいかもしれません。ゲームボーイのRAMは8kBですが、ルーズな実装をしても意外と余裕があるという印象です。
#define STEP_CYCLE 2 // xフレームに1回だけ移動(60fpsは速すぎるので) typedef struct { MountainCarState state; uint8_t cycle; } RunPretrainedState; // 学習済みモデル実行用の状態 static RunPretrainedState *rps; // x座標に対応するy座標を格納したテーブル const uint8_t display_y_table[160] = {54, 55, 57, 59, 60, 62, 64, /* ... */}; void step_display(const QLearningState *q_state, MountainCarState *state) { // 現在の状態をスプライトの座標に反映 // 車の中心のx座標(画面左端=0)を計算 uint8_t center_x = (uint8_t)((uint16_t)(state->position + 12000) / (18000 / 160)); // move_spriteの引数xは、x=8でスプライトの左端が画面の左端になる。x=center_xのときに、スプライト中心が画面の左端に来てほしい。スプライトの幅は8。 move_sprite(0, center_x + 4, display_y_table[center_x]); // 状態を1ステップ進める MountainCarAction action = GetBestAction(q_state, state); Reward reward = MountainCarStep(state, action); if (state->done) { MountainCarReset(state); } } // VBLANK割り込みで呼び出される関数(60fps) void run_pretrained_vbl() { const QLearningState *q_state = (const QLearningState *)(pc_trained_params); if (--rps->cycle == 0) { rps->cycle = STEP_CYCLE; step_display(q_state, &rps->state); } } // main関数で背景等の初期化が完了した後に呼ばれる void run_pretrained_main() { gotoxy(0, 16); printf("Running pre-trained model"); rps = malloc(sizeof(RunPretrainedState)); MountainCarReset(&rps->state); rps->cycle = STEP_CYCLE; __critical { add_VBL(run_pretrained_vbl); } while (1) { wait_vbl_done(); } }
ここまでの実装を終えると、学習済みQテーブルを用いて車が左右に移動し、坂を上るアニメーションを見ることができます。