TIC-80 デバッグガイド - 概要
TIC-80でゲーム開発を行う際、効果的なデバッグは開発効率を大きく向上させます。このガイドでは、TIC-80特有のデバッグ機能とテクニックを詳しく解説します。
📋 関連ガイド: エラーメッセージの一覧は TIC-80 エラーメッセージ一覧 をご覧ください。
デバッグの重要性
ゲーム開発において、バグは避けられません。適切なデバッグスキルを身につけることで:
- 開発時間を大幅に短縮できる
- コードの品質を向上させられる
- 複雑な問題も段階的に解決できる
- より安定したゲームを作れる
TIC-80のデバッグ機能
TIC-80には以下のデバッグ機能が用意されています:
主要なデバッグ機能:
• print() - 画面にテキストを表示
• trace() - コンソールにログを出力
• peek/poke - メモリの読み書き
• F1キー - コンソールの表示/非表示
このガイドで学べること
- 基本的なデバッグコマンドの使い方
- 効果的なログ出力の方法
- エラーメッセージの読み方と対処法
- パフォーマンス問題の特定方法
- 実践的なデバッグテクニック
基本的なデバッグ方法
TIC-80でのデバッグは、主にprint()
とtrace()
関数を使って行います。
画面へのデバッグ出力 - print()
基本的な使い方
function TIC() {
cls(0)
// 変数の値を画面に表示
print("Player X: " + player.x, 10, 10, 12);
print("Player Y: " + player.y, 10, 20, 12);
print("Score: " + score, 10, 30, 12);
// プレイヤーを描画
spr(1, player.x, player.y);
}
print()関数のパラメータ
パラメータ | 説明 | 例 |
---|---|---|
text | 表示するテキスト | "Hello World" |
x | X座標 | 10 |
y | Y座標 | 20 |
color | 色番号(0-15) | 12(緑) |
fixed | 画面固定(オプション) | true/false |
scale | 文字サイズ(オプション) | 1, 2, 3... |
複数の値を表示する例
function debugDisplay() {
// デバッグ情報を右上に表示
let y = 5;
print("=== DEBUG ===", 170, y, 11);
y += 10;
print("FPS: " + Math.floor(1000/time()), 170, y, 12);
y += 10;
print("Enemies: " + enemies.length, 170, y, 12);
y += 10;
print("Bullets: " + bullets.length, 170, y, 12);
}
コンソールへの出力 - trace()
基本的な使い方
function TIC() {
// F1キーを押してコンソールを開く
trace("Game started");
trace("Player position: " + player.x + ", " + player.y);
if (collision) {
trace("Collision detected!");
trace("Enemy type: " + enemy.type);
}
}
💡 ヒント: trace()の出力を見るには、F1キーを押してコンソールを開きます。ゲーム画面とコンソールを同時に見ることができます。
条件付きデバッグ
// デバッグモードフラグ
let DEBUG = true;
function debugLog(message) {
if (DEBUG) {
trace("[DEBUG] " + message);
}
}
function TIC() {
debugLog("Frame: " + time());
// 特定の条件でのみログ出力
if (player.health < 30) {
debugLog("Low health warning: " + player.health);
}
}
print()とtrace()の詳細
print()関数の高度な使い方
カラフルなデバッグ表示
function colorDebug() {
// 状態によって色を変える
let healthColor = player.health > 50 ? 11 : 6; // 緑 or 赤
print("Health: " + player.health, 10, 10, healthColor);
// 点滅効果でエラーを強調
if (error) {
let blinkColor = (time() % 1000 < 500) ? 6 : 14;
print("ERROR!", 100, 50, blinkColor, false, 2);
}
}
デバッグ情報のオーバーレイ
let showDebug = false;
function TIC() {
cls(0);
// ゲームの描画
drawGame();
// F2キーでデバッグ表示切り替え
if (keyp(59)) { // F2
showDebug = !showDebug;
}
if (showDebug) {
drawDebugOverlay();
}
}
function drawDebugOverlay() {
// 半透明の背景
rect(0, 0, 240, 50, 0);
// デバッグ情報
print("X:" + Math.floor(player.x), 5, 5, 12);
print("Y:" + Math.floor(player.y), 5, 15, 12);
print("State:" + player.state, 5, 25, 12);
print("Frame:" + player.animFrame, 5, 35, 12);
}
trace()関数の効果的な使い方
構造化されたログ出力
function logGameState() {
trace("=== Game State ===");
trace("Time: " + time());
trace("Score: " + score);
trace("Level: " + currentLevel);
trace("================");
}
function logError(functionName, error) {
trace("!!! ERROR in " + functionName + " !!!");
trace("Error: " + error);
trace("Stack trace:");
trace(new Error().stack);
}
配列やオブジェクトのデバッグ
function logArray(name, array) {
trace(name + " (length: " + array.length + "):");
for (let i = 0; i < array.length; i++) {
trace(" [" + i + "]: " + JSON.stringify(array[i]));
}
}
function logObject(name, obj) {
trace(name + ":");
for (let key in obj) {
trace(" " + key + ": " + obj[key]);
}
}
パフォーマンス測定
let performanceStats = {
updateTime: 0,
drawTime: 0,
frameCount: 0
};
function TIC() {
let startTime = time();
// 更新処理
updateGame();
let updateEnd = time();
performanceStats.updateTime = updateEnd - startTime;
// 描画処理
drawGame();
let drawEnd = time();
performanceStats.drawTime = drawEnd - updateEnd;
// 統計表示
if (performanceStats.frameCount++ % 60 === 0) {
trace("Update: " + performanceStats.updateTime + "ms");
trace("Draw: " + performanceStats.drawTime + "ms");
}
}
エラーメッセージの理解
TIC-80で発生する一般的なエラーメッセージとその対処法を解説します。
📋 完全なエラー一覧: より詳細なエラーメッセージの一覧は TIC-80 エラーメッセージ一覧 をご覧ください。
文法エラー (Syntax Errors)
1. unexpected symbol near '...'
unexpected symbol near 'then'
意味: 予期しない記号が見つかりました
原因: 括弧の閉じ忘れ、セミコロンの付け忘れ、スペルミスなど
// ❌ 間違い
if (x > 10 // 括弧が閉じていない
print("hello")
}
// ✅ 正解
if (x > 10) {
print("hello")
}
2. Unexpected token
Uncaught SyntaxError: Unexpected token '}'
意味: 予期しないトークン(文字)があります
原因: 括弧の対応が取れていない、セミコロン忘れなど
// ❌ 間違い
function update() {
x = x + 1
if (x > 100) {
x = 0
// }が足りない!
}
// ✅ 正解
function update() {
x = x + 1;
if (x > 100) {
x = 0;
}
}
実行時エラー (Runtime Errors)
1. Cannot read property of undefined
TypeError: Cannot read property 'x' of undefined
意味: 存在しない値にアクセスしようとしています
原因: 初期化されていない変数や配列の範囲外アクセス
// ❌ 間違い
enemies[5].x = 100 // enemies[5]が存在しない
// ✅ 正解
if (enemies[5]) { // 存在チェック
enemies[5].x = 100;
}
2. ReferenceError: variable is not defined
ReferenceError: player_x is not defined
意味: 定義されていない変数を使用しています
原因: 変数が初期化されていない状態で計算に使用
// ❌ 間違い
function TIC() {
player_x = player_x + 1 // player_xが未定義
}
// ✅ 正解
let player_x = 0 // 初期化
function TIC() {
player_x = player_x + 1
}
3. undefined is not a function
Error: undefined is not a function
at line 42
原因: 存在しない関数を呼び出そうとしている
解決方法:
// 間違い
player.upddate(); // タイポ
// 正しい
player.update();
4. Cannot read property of undefined
Error: Cannot read property 'x' of undefined
at line 15
原因: undefinedやnullのプロパティにアクセスしようとしている
解決方法:
// 間違い
enemies[10].x = 100; // enemies[10]が存在しない
// 正しい
if (enemies[10]) {
enemies[10].x = 100;
}
3. Maximum call stack size exceeded
Error: Maximum call stack size exceeded
原因: 無限再帰が発生している
解決方法:
// 間違い
function moveEnemy(enemy) {
enemy.x += 1;
moveEnemy(enemy); // 無限再帰!
}
// 正しい
function moveEnemy(enemy) {
enemy.x += 1;
// 再帰の終了条件を設ける
if (enemy.x < 240) {
// 次のフレームで処理
}
}
初心者がよくやるミス TOP10
1. 変数の初期化忘れ
ReferenceError: score is not defined
// ❌ 使う前に宣言・初期化していない
function TIC() {
score = score + 1 // エラー!
}
// ✅ グローバル変数として初期化
let score = 0
function TIC() {
score = score + 1
}
2. 配列のインデックスミス
注意: JavaScriptの配列は0から始まります!
// ❌ 間違い
let items = [10, 20, 30]
console.log(items[3]) // undefined(範囲外)
// ✅ 正解
let items = [10, 20, 30]
console.log(items[0]) // 10(最初の要素)
console.log(items[2]) // 30(最後の要素)
console.log(items[items.length - 1]) // 30(安全な最後の要素の取得)
3. スプライト番号の範囲外
// ❌ スプライトは0〜255まで
spr(300, x, y) // エラー!
// ✅ 有効な範囲内
spr(0, x, y) // 0番のスプライト
4. 画面座標の間違い
// ❌ TIC-80の画面は240×136
player.x = 300 // 画面外!
// ✅ 画面内に収める
player.x = Math.min(232, Math.max(0, player.x)) // 8x8スプライトの場合
5. btn()の番号間違い
ボタン番号:
0=上, 1=下, 2=左, 3=右, 4=Z, 5=X, 6=A, 7=S
6. 無限ループ
// ❌ 終了条件がない
while (true) {
// 処理
}
// ✅ 適切な終了条件
while (enemies.length > 0) {
// 処理
break; // または適切な終了条件
}
7. 関数名のタイポ
// ❌ よくあるタイプミス
player.udpate() // updateのつづり間違い
Math.randoom() // randomのつづり間違い
// ✅ 正しいつづり
player.update()
Math.random()
8. グローバル変数の意図しない上書き
// ❌ ローカル変数のつもりがグローバル変数を上書き
function updateEnemy() {
x = enemy.x // グローバルのxを上書き!
}
// ✅ ローカル変数として宣言
function updateEnemy() {
let x = enemy.x // ローカル変数
}
9. 文字列と数値の混同
// ❌ 文字列として連結される
score = "10"
score = score + 1 // "101"になる!
// ✅ 数値として扱う
score = 10
score = score + 1 // 11になる
10. 描画順序の間違い
// ❌ 背景が前面に来てしまう
function TIC() {
drawPlayer()
cls(0) // 全部消える!
}
// ✅ 正しい順序
function TIC() {
cls(0) // まず画面クリア
drawBackground()
drawEnemies()
drawPlayer() // 最前面
}
エラーの追跡方法
try-catch文の使用
function safeExecute(func, funcName) {
try {
func();
} catch (error) {
trace("Error in " + funcName + ": " + error.message);
trace("Stack: " + error.stack);
}
}
function TIC() {
safeExecute(() => updatePlayer(), "updatePlayer");
safeExecute(() => updateEnemies(), "updateEnemies");
safeExecute(() => checkCollisions(), "checkCollisions");
}
アサーション関数
function assert(condition, message) {
if (!condition) {
trace("ASSERTION FAILED: " + message);
trace(new Error().stack);
// ゲームを一時停止
while (true) {
// 無限ループでゲームを停止
}
}
}
// 使用例
function damagePlayer(amount) {
assert(amount >= 0, "Damage amount must be positive");
assert(player.health !== undefined, "Player health not initialized");
player.health -= amount;
}
デバッグテクニック
視覚的デバッグ
当たり判定の可視化
let DEBUG_COLLISION = true;
function drawHitbox(obj, color) {
if (DEBUG_COLLISION) {
// 当たり判定の範囲を表示
rectb(obj.x - obj.width/2, obj.y - obj.height/2,
obj.width, obj.height, color);
// 中心点を表示
pix(obj.x, obj.y, 12);
}
}
function TIC() {
cls(0);
// ゲームオブジェクトを描画
drawPlayer();
drawEnemies();
// デバッグ表示
if (DEBUG_COLLISION) {
drawHitbox(player, 11); // 緑
enemies.forEach(enemy => drawHitbox(enemy, 6)); // 赤
}
}
移動パスの表示
let pathHistory = [];
let MAX_HISTORY = 60;
function recordPath(obj) {
pathHistory.push({x: obj.x, y: obj.y});
if (pathHistory.length > MAX_HISTORY) {
pathHistory.shift();
}
}
function drawPath() {
for (let i = 1; i < pathHistory.length; i++) {
let alpha = i / pathHistory.length;
let color = Math.floor(alpha * 3) + 10;
line(pathHistory[i-1].x, pathHistory[i-1].y,
pathHistory[i].x, pathHistory[i].y, color);
}
}
ステート管理のデバッグ
状態遷移の記録
let stateHistory = [];
let currentState = "idle";
function changeState(newState) {
let timestamp = time();
stateHistory.push({
from: currentState,
to: newState,
time: timestamp
});
trace("State change: " + currentState + " -> " + newState);
currentState = newState;
// 最新の10件だけ保持
if (stateHistory.length > 10) {
stateHistory.shift();
}
}
function drawStateHistory() {
let y = 100;
print("State History:", 5, y, 12);
y += 10;
for (let i = stateHistory.length - 1; i >= 0; i--) {
let entry = stateHistory[i];
print(entry.from + "->" + entry.to, 5, y, 11);
y += 8;
}
}
メモリ使用量の監視
function countObjects() {
let counts = {
enemies: enemies.length,
bullets: bullets.length,
particles: particles.length,
total: 0
};
counts.total = counts.enemies + counts.bullets + counts.particles;
return counts;
}
function drawMemoryStats() {
let counts = countObjects();
let y = 5;
print("=== Memory ===", 160, y, 14);
y += 10;
for (let key in counts) {
let color = counts[key] > 100 ? 6 : 11; // 赤 or 緑
print(key + ": " + counts[key], 160, y, color);
y += 8;
}
}
フレームレートの監視
let fps = 0;
let frameCount = 0;
let lastTime = 0;
function updateFPS() {
frameCount++;
let currentTime = time();
if (currentTime - lastTime >= 1000) {
fps = frameCount;
frameCount = 0;
lastTime = currentTime;
}
}
function drawFPS() {
let color = fps < 30 ? 6 : (fps < 50 ? 14 : 11);
print("FPS: " + fps, 200, 5, color);
}
よくあるエラーと対処法
配列関連のエラー
範囲外アクセス
問題:配列の範囲外にアクセスしている
// 問題のあるコード
for (let i = 0; i <= enemies.length; i++) { // <= が問題
enemies[i].update(); // 最後のループでエラー
}
// 修正版
for (let i = 0; i < enemies.length; i++) { // < に変更
enemies[i].update();
}
// またはfor...ofを使用
for (let enemy of enemies) {
enemy.update();
}
削除中の配列操作
// 問題のあるコード
for (let i = 0; i < bullets.length; i++) {
if (bullets[i].y < 0) {
bullets.splice(i, 1); // インデックスがずれる!
}
}
// 修正版1:逆順でループ
for (let i = bullets.length - 1; i >= 0; i--) {
if (bullets[i].y < 0) {
bullets.splice(i, 1);
}
}
// 修正版2:filterを使用
bullets = bullets.filter(bullet => bullet.y >= 0);
スプライト関連のエラー
スプライト番号の間違い
// デバッグ用スプライト番号表示
function drawSpriteDebug() {
for (let i = 0; i < 16; i++) {
for (let j = 0; j < 16; j++) {
let id = i * 16 + j;
spr(id, j * 8, i * 8);
print(id, j * 8, i * 8 + 8, 12, false, 1);
}
}
}
透明色の問題
// スプライトの透明色を確認
function checkTransparency(spriteId, transparentColor) {
// スプライトを描画
cls(5); // 別の色で塗りつぶし
spr(spriteId, 50, 50, transparentColor);
// 透明色が機能しているか確認
print("Transparent: " + transparentColor, 10, 10, 12);
}
パフォーマンス関連の問題
処理が重い場合の対処
// 問題:毎フレーム全ての敵との距離計算
function checkAllCollisions() {
for (let enemy of enemies) {
for (let bullet of bullets) {
let dist = distance(enemy, bullet);
if (dist < 10) {
// 衝突処理
}
}
}
}
// 改善版:空間分割を使用
let grid = {};
let GRID_SIZE = 32;
function getGridKey(x, y) {
let gx = Math.floor(x / GRID_SIZE);
let gy = Math.floor(y / GRID_SIZE);
return gx + "," + gy;
}
function updateGrid() {
grid = {};
// オブジェクトをグリッドに登録
for (let enemy of enemies) {
let key = getGridKey(enemy.x, enemy.y);
if (!grid[key]) grid[key] = [];
grid[key].push(enemy);
}
}
メモリリーク対策
// オブジェクトプールを使用
let particlePool = [];
let activeParticles = [];
function createParticle(x, y) {
let particle;
if (particlePool.length > 0) {
// プールから再利用
particle = particlePool.pop();
particle.x = x;
particle.y = y;
particle.life = 30;
} else {
// 新規作成
particle = {x: x, y: y, life: 30};
}
activeParticles.push(particle);
}
function updateParticles() {
for (let i = activeParticles.length - 1; i >= 0; i--) {
let particle = activeParticles[i];
particle.life--;
if (particle.life <= 0) {
// プールに戻す
activeParticles.splice(i, 1);
particlePool.push(particle);
}
}
}
ベストプラクティス
デバッグコードの管理
デバッグフラグの一元管理
// デバッグ設定オブジェクト
const DEBUG = {
enabled: true,
showFPS: true,
showHitboxes: false,
showPaths: false,
logCollisions: false,
logStateChanges: true
};
// デバッグモードの切り替え
function toggleDebug(feature) {
if (feature in DEBUG) {
DEBUG[feature] = !DEBUG[feature];
trace("Debug " + feature + ": " + DEBUG[feature]);
}
}
// キーボードショートカット
function handleDebugKeys() {
if (key(17)) { // Ctrl押下中
if (keyp(49)) toggleDebug('showFPS'); // 1
if (keyp(50)) toggleDebug('showHitboxes'); // 2
if (keyp(51)) toggleDebug('showPaths'); // 3
}
}
条件付きコンパイル風の実装
// 本番環境では削除されるデバッグコード
function debugOnly(callback) {
if (DEBUG.enabled) {
callback();
}
}
function TIC() {
cls(0);
updateGame();
drawGame();
debugOnly(() => {
drawDebugInfo();
handleDebugKeys();
});
}
エラーハンドリング
グレースフルな失敗
function safeSprite(id, x, y, colorkey, scale, flip, rotate) {
// スプライトIDの範囲チェック
if (id < 0 || id > 255) {
trace("Invalid sprite ID: " + id);
return;
}
// 座標の範囲チェック
if (x < -8 || x > 248 || y < -8 || y > 144) {
// 画面外なのでスキップ
return;
}
spr(id, x, y, colorkey, scale, flip, rotate);
}
function safeArrayAccess(array, index, defaultValue) {
if (index >= 0 && index < array.length) {
return array[index];
}
trace("Array index out of bounds: " + index);
return defaultValue;
}
ログシステムの実装
const LogLevel = {
ERROR: 0,
WARN: 1,
INFO: 2,
DEBUG: 3
};
let currentLogLevel = LogLevel.DEBUG;
let logBuffer = [];
let MAX_LOG_SIZE = 100;
function log(level, message) {
if (level <= currentLogLevel) {
let timestamp = Math.floor(time() / 1000);
let entry = {
time: timestamp,
level: level,
message: message
};
logBuffer.push(entry);
if (logBuffer.length > MAX_LOG_SIZE) {
logBuffer.shift();
}
// コンソールにも出力
let prefix = ["ERROR", "WARN", "INFO", "DEBUG"][level];
trace("[" + prefix + "] " + message);
}
}
// 使いやすいヘルパー関数
function logError(msg) { log(LogLevel.ERROR, msg); }
function logWarn(msg) { log(LogLevel.WARN, msg); }
function logInfo(msg) { log(LogLevel.INFO, msg); }
function logDebug(msg) { log(LogLevel.DEBUG, msg); }
開発効率を上げるツール
ホットリロード風の実装
let lastReloadTime = 0;
let RELOAD_INTERVAL = 2000; // 2秒
function checkReload() {
// F5キーでマニュアルリロード
if (keyp(63)) {
reloadGame();
}
// 自動リロード(開発時のみ)
if (DEBUG.enabled && time() - lastReloadTime > RELOAD_INTERVAL) {
if (hasCodeChanged()) { // 仮想的な関数
reloadGame();
lastReloadTime = time();
}
}
}
function reloadGame() {
trace("Reloading game state...");
// ゲーム状態をリセット
initGame();
// リソースの再読み込み
loadResources();
trace("Reload complete!");
}
ステート保存/復元
function saveGameState() {
let state = {
player: {
x: player.x,
y: player.y,
health: player.health,
score: player.score
},
enemies: enemies.map(e => ({
x: e.x,
y: e.y,
type: e.type,
health: e.health
})),
level: currentLevel,
time: gameTime
};
// pmem(永続メモリ)に保存
pmem(0, JSON.stringify(state).length);
for (let i = 0; i < state.length; i++) {
pmem(i + 1, state.charCodeAt(i));
}
trace("Game state saved!");
}
function loadGameState() {
let length = pmem(0);
if (length === 0) return false;
let stateStr = "";
for (let i = 0; i < length; i++) {
stateStr += String.fromCharCode(pmem(i + 1));
}
try {
let state = JSON.parse(stateStr);
// ゲーム状態を復元
player = state.player;
enemies = state.enemies;
currentLevel = state.level;
gameTime = state.time;
trace("Game state loaded!");
return true;
} catch (e) {
trace("Failed to load game state: " + e);
return false;
}
}
まとめ
効果的なデバッグのために:
• 問題を小さく分解して調査する
• ログを活用して問題の発生箇所を特定する
• 視覚的なデバッグツールを活用する
• エラーハンドリングを適切に実装する
• デバッグコードを整理して管理する
これらのテクニックを組み合わせることで、TIC-80でのゲーム開発がより効率的になり、バグの少ない高品質なゲームを作ることができます。