1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

キーボードイベントやProxyを使ったレトロゲーム機もどき

Last updated at Posted at 2025-05-02

はじめに

執筆時点では、Switch2が話題になっています。
それに触発されて、子供時代に遊んだゲーム機のことを思い浮かべました。
当時を彷彿とさせる題材を使って、あまり使う機会のなかったProxyオブジェクトを使ってみようと思い、以下のようなものを作ってみました。

作ってみたもの

CodePenをご確認ください。
HTMLとCSSで見た目を作成し、JavaScriptでボタンアクションを実装しています

  • Escapeキーを入力すると中央画面が切り替わります(または左下の横長ボタン)
  • 表示内容は、キー(ボタン)入力に対応するボタン文字を表示するだけです

See the Pen sample by j (@eaqqryou-the-sans) on CodePen.

キーボード入力 対応するボタン
Esc 電源ON・OFF
Enter / Space / Z A
Shift / X B
W / ↑
D / →
S / ↓
A / ←

Proxyオブジェクトをざっくり補足

Proxyオブジェクトは、通常のオブジェクトを拡張したものです。
new Proxy()で宣言し、引数としてget()set()というハンドラー関数(トラップ)を用いて実装します。

// 普通のオブジェクトを定義
const object = {
    prop: true
}

// Proxyオブジェクト
// 第1引数:Proxyを設定するオブジェクト
// 第2引数: ハンドラーオブジェクト
const proxyObject = new Proxy(object, {
    // プロパティが代入されると実行
    set(target, property, value) {
        // target   => object
        // property => objectのプロパティ名
        // value    => objectのプロパティへの代入値
        target[property] = value;

        // 代入されたプロパティ名と値をコンソールログで出力する
        console.log(property, value);
        return true;
    },
    // プロパティにアクセスされると実行
    get(target, property) {
        // アクセスされたプロパティ名をコンソールログで出力する
        console.log(property);
        return target[property];
    }
})

Proxyオブジェクトのプロパティに代入があると、set()の処理が実行される。

proxyObject.property = true
proxyObject[property] = true

Proxyオブジェクトのプロパティにアクセスがあると、get()の処理が実行される。

proxyObject.property
proxyObject[property]

以降では、CodePenに貼ったソースコードの一部を補足します。

操作状態をProxyオブジェクトで管理する

変数inputStateをProxyで定義し、以下の目的で使用しています。

  • キーボードの入力キーをプロパティの値として更新する
  • 更新の度に、入力キーに対応するボタンがあればボタンアクションを実行する

inputState.current(文字列)
選択されたボタンの種類を保存しています。

inputState.eventType(文字列)
発生したイベントの種類を保存しています。
実際にはキーボードイベントだけではないので、pressreleaseという単語で管理しています。

// 特定のキーボード入力に対応するボタンアクションの準備, 対応表
const inputMap = {
    ...power, // 電源ボタンの設定
    ...dPad, // 十字ボタンの設定
    ...actionButtons // ABボタンの設定
};

// キー入力状態を管理
const inputState = new Proxy({
    // 現在入力されているキー
    current: "",
    // currentが更新されたときのイベントタイプ("press", "release"が入ります)
    eventType: ""
}, {
    set: (target, prop, value) => {
        // currentプロパティが更新されたとき
        if (isCurrentProperty(prop)) {
            // 入力されたキーに対応するボタンの種類を取得(a,b,up,right,down,left)
            const selectdKey = findSelectedKey(value, inputMap);
            // eventTypeプリパティの値で、ボタンアクションを振り分ける(押された時、離された時のアクション)
            executeButtonActionByEventType(target, selectdKey, inputMap);
        }
        return Reflect.set(target, prop, value);
    },
    get: (target, prop) => Reflect.get(target, prop)
});

キー入力に応じたボタンアクションを準備する

下記は十字ボタンの設定です。各ボタンに3つの設定項目をつけています。

triggerKeys(配列)
方向ボタンに対応させたいKeyboardEvent.keyの値を入れています。

onPress(関数)
キーが押された時に実行したい処理です。

onRelease(関数)
キーが離された時に実行したい処理です。

const dPad = {
    up: {
        // 「上ボタン」と判定するキー
        triggerKeys: [ "arrowup", "w" ],
        // 押した時の処理
        onPress: (target, settings) => {
            game.press?.Up(target, settings);
            setFocusButton(dPad, "up");
        }, 
        // 離したの処理
        onRelease: (target, settings) => {
            game.release?.Up(target, settings);
            setBlurButton(dPad, "up");
        }
    },
    right: {
        // 「右ボタン」と判定するキー
        triggerKeys: [ "arrowright", "d" ],
        // 押した時の処理
        onPress: (target, settings) => {
            game.press?.Right(target, settings);
            setFocusButton(dPad, "right");
        },
        // 離したの処理
        onRelease: (target, settings) => {
            game.release?.Right(target, settings);
            setBlurButton(dPad, "right");
        }
    },
    down: {
        // 「下ボタン」と判定するキー
        triggerKeys: [ "arrowdown", "s" ],
        // 押した時の処理
        onPress: (target, settings) => {
            game.press?.Down(target, settings);
            setFocusButton(dPad, "down");
        },
        // 離したの処理
        onRelease: (target, settings) => {
            game.release?.Down(target, settings);
            setBlurButton(dPad, "down");
        }
    },
    left: {
        // 「左ボタン」と判定するキー
        triggerKeys: [ "arrowleft", "a" ],
        // 押した時の処理
        onPress: (target, settings) => {
            game.press?.Left(target, settings);
            setFocusButton(dPad, "left");
        },
        // 離したの処理
        onRelease: (target, settings) => {
            game.release?.Left(target, settings);
            setBlurButton(dPad, "left");
        }
    }
};

イベント登録

キーボードイベント

keydownでボタンを押した時、keyupでボタンを離した時の処理を登録しています。
ProxyオブジェクトであるinputStateのプロパティに値を代入することで、setトラップに記述した処理が自動的に実行されます。

keydownでは、KeyboardEvent.repeatプロパティを利用することで1回目のキー入力のみ処理を実行しています。

// 特定のキー(ボタン)がリピートされたか判定する
function isPreventedKeyRepeat(e, keys = [ "escape", " ", "enter", "z", "shift", "x" ]) {
    return e.repeat && keys.includes(e.key.toLowerCase());
}

// keydownとkeyupイベントを登録
function addKeyEventListeners(target = window, handler) {
    target.addEventListener("keydown", (e => {
        if (isPreventedKeyRepeat(e)) return;
        // イベントタイプを"press"として実行
        handler("press", e.key.toLowerCase(), e);
    }));
    target.addEventListener("keyup", (e => {
        // イベントタイプを"release"として実行
        handler("release", e.key.toLowerCase(), e);
    }));
}

ポインターイベント

基本的には、キーボードイベントとほとんど同じです。

// pointerdownとpointerupイベントを登録
function addButtonEventListeners(buttons, handler) {
    buttons.forEach((button => {
        button.addEventListener("pointerdown", (e => {
            // イベントタイプを"press"として実行
            handler("press", button.dataset.key, e);
        }));
        button.addEventListener("pointerup", (e => {
            // イベントタイプを"release"として実行
            handler("release", button.dataset.key, e);
        }));
    }));
}

上記イベントでは、ターゲットとなるボタンのdata-keyを利用してボタン判定を行っています。こちらには、KeyboardEvent.key値を指定しており、キー入力を模している状態です。

index.html
<!-- data-keyをevent.keyの代替案として使う -->
<button data-key="shift" type="button">B</button>
<button data-key="enter" type="button">A</button>

ボタンアクション

下記の変数には、実際に行われるボタンアクションの内容を定義しています。

press(オブジェクト)
各ボタンが押された時のアクションです。

const press = {
    // Aボタンが押されたら、
    A: (target, settings) => {
        if (settings.switch.isSwitchOn) {
            // "A"という文字列を表示する。
            keyDisplayArea.textContent = "A";
            // コンソールログに文字列を出力する。
            console.log("Aを押しました");
        }
    },
    B: (target, settings) => {
        if (settings.switch.isSwitchOn) {
            keyDisplayArea.textContent = "B";
            console.log("Bを押しました");
        }
    },
    Up: (target, settings) => {
        if (settings.switch.isSwitchOn) {
            keyDisplayArea.textContent = "";
            console.log("上を押しました");
        }
    },
    Down: (target, settings) => {
        if (settings.switch.isSwitchOn) {
            keyDisplayArea.textContent = "";
            console.log("下を押しました");
        }
    },
    Left: (target, settings) => {
        if (settings.switch.isSwitchOn) {
            keyDisplayArea.textContent = "";
            console.log("左を押しました");
        }
    },
    Right: (target, settings) => {
        if (settings.switch.isSwitchOn) {
            keyDisplayArea.textContent = "";
            console.log("右を押しました");
        }
    }
};

release(オブジェクト)
各ボタンが離された時のアクションです。


const release = {
    // Aボタンが離されたら、
    A: (target, settings) => {
        if (settings.switch.isSwitchOn) {
            // コンソールログに文字列を出力する。
            console.log("Aを離しました");
        }
    },
    B: (target, settings) => {
        if (settings.switch.isSwitchOn) {
            console.log("Bを離しました");
        }
    },
    Up: (target, settings) => {
        if (settings.switch.isSwitchOn) {
            console.log("上を離しました");
        }
    },
    Down: (target, settings) => {
        if (settings.switch.isSwitchOn) {
            console.log("下を離しました");
        }
    },
    Left: (target, settings) => {
        if (settings.switch.isSwitchOn) {
            console.log("左を離しました");
        }
    },
    Right: (target, settings) => {
        if (settings.switch.isSwitchOn) {
            console.log("右を離しました");
        }
    }
};

実行

最後にgame.init()関数で初期化を行っています。

game.init(sample, {
    press: press,
    release: release,
    timeUntilLow: 60000,
    timeUntilOff: 180000
});

sample(関数)
電源起動後に表示するゲーム部分のHTMLを追加しています。

function sample(romSlot) {
    console.log("default");
    const html = `\n    <div id="default" class="game-default" data-rom="default">\n      <h1>PRESS</h1>\n      <div id="js-key">ここに押したボタンを表示する</div>\n    </div>`;
    romSlot.insertAdjacentHTML("afterbegin", html);
}

press, release(オブジェクト)
ボタンアクションとして変数宣言したpressreleaseが指定されています。

press: press,
release: release,

timeUntilLow(数値 or null)
電源ONにしてから1分経過すると、右上の電源ランプが赤色に変わります。

timeUntilLow: 60000,

timeUntilOff(数値 or null)
電源ONにしてから3分経過すると、問答無用で電源OFFになります。

timeUntilOff: 180000

最後に

こちらが暇つぶしや何かの足しになれば幸いです。
ご一読ありがとうございました。

参考文献

Proxy
Proxy - JavaScript | MDN

イベント
ポインターイベント
KeyboardEvent
キーボードイベントの key の値

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?