今回は最終回、KeyPad の実装を見ていきます。
毎度のことながら NES の仕様や仕組みについては参考にしたページが詳しいので、そちらを参照してください。
Nes のコントローラには以下のボタンがあります。
- A
- B
- Select
- Start
- 上キー
- 下キー
- 左キー
- 右キー
それぞれのボタンの状態が 0x4016 番地(2コンは 0x4017番地ですがそこは実装されていません)を読み込むたびに順番に読み込まれる仕組みです。
これは CPU_BUS.swift でこのように実装されています。
else if address == 0x4016 { // KeyPad let val: Byte = keyPad.read() ? 1 : 0 return val }
keyPad.read() で Bool 値を読んで、それを 1か 0かで返します。
keyPad.read() は KeyPad.swift でこのように実装されています。
func read() -> Bool { guard keyRegisters.count > index else { return false } let readIndex = index index += 1 return keyRegisters[readIndex] }
プロパティの keyRegisters の index ビットの値(Bool値)を返しています。
この keyRegisters は現在のコントローラの値を持っているプロパティです。
ビット毎の対応は Int の extension として宣言されています。
extension Int { static let aKey = 0 static let bKey = 1 static let selectKey = 2 static let startKey = 3 static let upKey = 4 static let downKey = 5 static let leftKey = 6 static let rightKey = 7 }
これを index が 0から順に 7まで KeyPad.read() が呼ばれる度に返します。
この index は KeyPad.write() で 1, 0が順に書き込まれると 0にリセットされます。
また、この時プロパティ keyBuffer に保存されていたコントローラの状態が keyRegisters にコピーされ、以降この値が読み出されます。
func write(_ data: Byte) { if (data & 0x01) != 0 { isSet = true } else if isSet && !((data & 0x01) != 0) { isSet = false index = 0 for (i, value) in keyBuffer.enumerated() { keyRegisters[i] = value } } }
keyBuffer への値の書き込みは onKeyDown と onKeyUp で行われます。
func onKeyDown(_ index: Int) { if index < keyBuffer.count { keyBuffer[index] = true } } func onKeyUp(_ index: Int) { if index < keyBuffer.count { keyBuffer[index] = false } }
押す、もしくは放されたキーに対応するビット情報を引数に取り、keyBuffer のビットをオン/オフしています。
mac のキーボードからの入力を取得して KeyPad の onKeyDown/onKeyUp に送っているのは GameInputController です。
NSEvent.addLocalMonitorForEvents を使ってキー入力を取得します。
NSEvent.addLocalMonitorForEvents(matching: .keyDown) { guard let keyPad = self.keyPad, let keyPadCode = GameInputController.keyboardKeyCodeTable[Int($0.keyCode)] else { return $0 } keyPad.onKeyDown(keyPadCode) return nil } NSEvent.addLocalMonitorForEvents(matching: .keyUp) { guard let keyPad = self.keyPad, let keyPadCode = GameInputController.keyboardKeyCodeTable[Int($0.keyCode)] else { return $0 } keyPad.onKeyUp(keyPadCode) return nil }
取得したキー入力の keyCode をキーにして keyboardKeyCodeTable を使って keyCode から押されたキーに対応するビット情報(keyPadCode)に変換します。ここで変換できなかった場合は、KeyPad で使用するキーではないので無視します。
変換できた場合は onKeyDown/onKeyUp に keyPadCode を送って KeyPad の keyBuffer に書き込まれます。
ちなみに別のキー入力を使いたい場合はこの keyboardKeyCodeTable の keyCode の値を変更すれば OK です。
以上で、キーボードの入力を KeyPad へ渡して操作するコードは終了です。
手元に古い USB 接続のゲームコントローラがあったので、mac に接続して使えるようにしてみました。
絵
まず、大まかな処理の流れは以下のようになります。
- USB デバイスからの入力
- KeyPad で使用する各キーのビット位置情報への変換
- KeyPad の onKeyDown/onKeyUp へ情報送信
それぞれについて実装を見ていきます。
USB 機器の接続およびデータの取得に関しては既にあるライブラリの一部を使用しました。
USB 接続されたデバイスとのやりとりができるライブラリですが、この中の HIDDevice と HIDDeviceMonitor を使っています。
USB のゲームコントローラは HIDDevice として扱われるので、アプリケーション起動時に HIDDevice に vendorID と productID を指定してモニタするようにします。今回使用した Justy のゲームコントローラの vendorID は 1112、productID は 4098になります。
この設定は AppDelegate.swift で行われます。
let rfDeviceMonitor = HIDDeviceMonitor([HIDMonitorData(vendorId: 1112, productId: 4098)], reportSize: 64) func applicationDidFinishLaunching(_ aNotification: Notification) { // Insert code here to initialize your application let rfDeviceDaemon = Thread(target: self.rfDeviceMonitor, selector: #selector(self.rfDeviceMonitor.start), object: nil) rfDeviceDaemon.start() }
モニタした情報は Notification で取得できます。
これは GameInputController の中で行なっています。
func setupHidInput() { NotificationCenter.default.addObserver(self, selector: #selector(self.hidReadData), name: .HIDDeviceDataReceived, object: nil) } @objc func hidReadData(notification: Notification) { if let dic = notification.object as? NSDictionary, let data = dic["data"] as? Data { if data.count == 3 { sendHidKey(PadValue(data: data)) } } }
HIDDeviceMonitor はデバイスの接続・取り外し・データ入力をモニタしてくれますが今回はデータ入力のみを使っています。
取得したデータは今回使用しているゲームコントローラの場合 3Byte の Data として送られてきます。
データは以下のように struct で定義して扱っています。
struct PadValue { var hVal: UInt8 = 64 var vVal: UInt8 = 64 var button: UInt8 = 0 init(data: Data) { if data.count == 3 { hVal = data[0] vVal = data[1] button = data[2] } } static func ==(lhs: PadValue, rhs: PadValue) -> Bool { return (lhs.hVal == rhs.hVal) && (lhs.vVal == rhs.vVal) && (lhs.button == rhs.button) } static func !=(lhs: PadValue, rhs: PadValue) -> Bool { return (lhs.hVal != rhs.hVal) || (lhs.vVal != rhs.vVal) || (lhs.button != rhs.button) } }
hVal, vVal はそれぞれ水平方向の値、垂直方向の値です。
何も押されていない場合は 64で、上もしくは左が押されると 0、下もしくは右が押されると 127 になります。アナログ入力の min/center/max の値になる感じです。
button は 8ビットのそれぞれが各ボタンの状態を表しています。
0 : A
1 : B
2 : C
3 : X
4 : Y
5 : Z
6 : L
7 : R
今回使用したゲームコントローラは 8つボタンを持っているので、今回は A, B をそのまま Aボタン、Bボタンとして使用して、X, Y を Select, Start として使用することにします。
入力される情報の詳細がわかったので後は KeyPad でのビット位置情報への変換です。
これは GameInputController の sendHidKey で行なっています。
// check left, right button if val.hVal != lastPadValue.hVal { if val.hVal == 0 { if lastPadValue.hVal == 127 { keyPad.onKeyUp(.rightKey) } keyPad.onKeyDown(.leftKey) } else if val.hVal == 127 { if lastPadValue.hVal == 0 { keyPad.onKeyUp(.leftKey) } keyPad.onKeyDown(.rightKey) } else { if lastPadValue.hVal == 0 { keyPad.onKeyUp(.leftKey) } else { keyPad.onKeyUp(.rightKey) } } } // check up, down button if val.vVal != lastPadValue.vVal { if val.vVal == 0 { if lastPadValue.vVal == 127 { keyPad.onKeyUp(.downKey) } keyPad.onKeyDown(.upKey) } else if val.vVal == 127 { if lastPadValue.vVal == 0 { keyPad.onKeyUp(.upKey) } keyPad.onKeyDown(.downKey) } else { if lastPadValue.vVal == 0 { keyPad.onKeyUp(.upKey) } else { keyPad.onKeyUp(.downKey) } } } // check buttons let vXor = val.button ^ lastPadValue.button for bit in 0..<8 { if vXor[bit] { if let tableValue = GameInputController.gamePadKeyCodeTable[bit], let keyCode = tableValue { if val.button[bit] { keyPad.onKeyDown(keyCode) } else { keyPad.onKeyUp(keyCode) } } } } self.lastPadValue = val
思ったことをそのままベタ書きしただけなので汚いです…。
左右、上下、ボタン、それぞれ単純に前の状態と比べて違っていたら keyPad.onKeyDown/onkeyUp にデータを送るだけです。
ボタンはゲームコントローラのビット位置から KeyPad のビット位置への変換に gamePadKeyCodeTable を使っています。Select, Start はそれぞれ X, Y に割り当て、Nes にないその他のキーは nil にして送信をしないようにしています。
以上でゲームコントローラの入力を KeyPad へ送るコードは終了ですが、GameInputController に全部詰め込んでいて汚いですね。
KeyPad も実装してこれで当初の目標は達成できました。
NES のエミュレータでベンチマーク的に良く使われてるスーパーマリオも動いているし、NES研究室にあるサンプルの TkShoot も動きます!(大きいスプライトに対応していないので自機、敵機、爆発が微妙ですが…)
後は APU が残っているのですが、コードを読んでも全くチンプンカンプンなので諦めました。
年末年始に時間が取れたら少しサウンド関係を勉強してみようと思います。