プログラミング
コモドール64でのポングウォーズ
Pong Wars on the Commodore 64 (imrannazar.com)
要約
この記事は、JavaScriptで作成された「ポングウォーズ」というゲームを、コモドール64のアセンブリ言語で再実装する過程を詳細に解説しています。ゲームのロジック、画面描画、そして当時のハードウェア制約の中で効率的なコードを書くための技術的な課題と解決策が、多くのデモコードと共に説明されています。
全文翻訳
記事に戻る
2026年7月15日
この記事は、2024年初頭に最初に約束されました[0]Pongwars on the C64, @Two9A, Feb 22nd 2024、しかし非常に長くなったため、目次を追加しました。これはおそらく初めてでしょう...
はじめに
始める前に
脱線:C64ディスクイメージの構築
ゲームループ:移動計算
第二の脱線:負の数の表現
移動の後に位置決めが来る
チャンスの要素
プレイフィールド
スコアリング
第三の脱線:バイナリ長除算
スコアのレンダリング
将来の考慮事項
脚注と引用
2024年の初めのある日、私はマストドンを漫然と見ていたところ、Koen van Gilst[1]Pongwars recreated in JavaScript, @vnglst, Jan 2024によるこのトゥートに出くわしました。
図1:狂気への降下を引き起こした@vnglstによるトゥート、現在議論中
これは、「昼」のボールと「夜」のボールが、反対側のボールの色を反転させるための永遠の戦いを描いたものです。何千もの「いいね」があったことから判断すると、多くの人が魅了されていました。私もその一人で、ふとこう思いました。「van Gilstがこれをオンラインで見つけてJavaScriptで再実装しなければならなかったなら、そして今私がこれをオンラインで見つけたなら、私はこれをコモドール64で再実装しなければならない、そうだろうか?そんなことは可能なのだろうか?」
「はい、可能です!」
これは比較的単純なアニメーションで、ボールが表面に接触して跳ね返るポイントにランダムな要素が追加されています。「跳ね返る表面」を定義することが難しい部分のようです。
したがって、計画を立てる必要があります。そして最初のステップとして、定義があります。
ボール(2つ)があり、直線でスムーズに移動します。
ブロックフィールドがあります。ボールが移動する20x20のブロック領域です。
ブロックフィールドには、ボールが移動できない境界があり、ボールは境界から跳ね返ります。
また、ボールが跳ね返るエッジを持つ2つの側面もありますが、ヒットしたブロックは反対側に反転します。
これらすべては、C64でのハッキングに生涯興味を持っていたにもかかわらず、これまでこのマシンでアセンブリ言語で実質的なものを書いたことがなかったという事実によって複雑になっています。学びながら進むことになりそうです。
当時のほとんどのマシンでは、ビデオはCRTに出力されていました。これは、私が数年前に1541ディスクドライブについて書いたメガ・スレッド[2]"Why Was the Commodore 64 Disk Drive So Slow?", Imran Nazar, Dec 2024で説明したとおりです。ビデオ出力の各フレームの後には短い垂直ブランキング期間があり、その間、ビデオチップはメモリに要求をかけていません。このアニメーションに必要な計算を、ブランキング期間内に完了させ、フレームレンダリング期間中にマシンで発生している他のことに不当に影響を与えないように、計算を制限することを目指します。
コモドール64では、BASICインタープリタがその期間中に実行されている主なものであり、この効果に役立つものがいくつか提供されています。そのうちの1つは、各フレームの底でトリガーされるフック[3]"Raster interrupt", C64 Wiki, updated Mar 2024で、BASICを実行し続けながら、各フレームでカスタムコードを呼び出すことができます。したがって、コードの計画は次のようになります。
図2:メインループのフローチャート
始める前に
初期化部分を見てみましょう。ブロックフィールドの内部表現を保持しますが、画面上のブロックフィールドも設定する必要があります。C64は、デフォルトのテキストモードで40x25文字の領域を持っています(各文字は8x8ピクセルの正方形です)。そして、ここに2つのメモリ領域があります。
スクリーンRAMはデフォルトで$0400にあり、文字値が格納されている千バイトの領域です。
カラーRAMは$D800にあり、千個のニブル[A]ニブルは4ビットで、8ビットバイトとは対照的です。各場所は、C64の16色のうちの1つを表すことができます。
スクリーンRAMの直後にスプライトデータのポインタがありますが、まだスプライトを設定していないので、ショートカットとしてスクリーンに1024バイトを書き込みます。これはスクリーンの終端を超えて書き込みますが、コードを大幅に簡素化します。
SCRRAM = $0400
BLOCKPTR = $03 ; BASICによって使用されない2バイトのブロック
init:
; スクリーンRAMへのポインタを設定します。左上から開始します。
; 65xx CPUはリトルエンディアンなので、ローバイトが先です。
lda #<SCRRAM
sta BLOCKPTR
lda #>SCRRAM
sta BLOCKPTR + 1
; 文字0(@)ではなく、スペース(文字32)を書き込みます。
lda #32
; 1024書き込みのために、256の4倍です。
ldx #4
ldy #0
.blank:
; 内側のループ:Y=0から255、254、...0まで実行します。
sta (BLOCKPTR), y
dey
bne .blank
; 外側のループ:ポインタを256ずつ進めます。
inc BLOCKPTR + 1
dex
bne .blank
(余談ですが、BLOCKPTRは「BASICによって使用されない」値に設定されていますが、どうやってそれを知るのでしょうか? Joe Forster(STAデモグループ)による詳細なC64メモリマップ[4]Commodore 64 memory map, STA, last updated probably a while ago as the page is HTML 4.01によると、BASICとKernal ROMが標準操作で触れない場所があり、プログラムが使用するために空いていることがわかります。)
スクリーンが均一なコモドールブルーで空白になったので、ブロックフィールドを20x20の領域として描画できます。C64のディスプレイは、テキストモードで各文字に前景色を持たせることができますが、背景色は画面全体で共有されます。ブロックが白か黒かを示すためにカラーRAMを使用したい場合(そして使用します)、穴のない完全に前景色の文字が必要です。
図3:C64文字セット1、128文字の2セット出典:Jodigi, C64 Wikiの管理者[5]"Character set", C64 Wiki, updated Aug 2025
文字セットの後半には前半の逆ビデオコピーが含まれているため、スペース(文字160)の逆ビデオバージョンを、完全に前景色のブロックとして使用できます。
ループは少し複雑になります。幅40の画面に20文字の行を書き込むには、20文字書き込んで20文字スキップする必要があります。さらに、このブロックを画面の中央に配置したいので、1行下、10文字内側から開始する必要があります。結果のポインタは、実際にはスクリーンRAMの開始位置から1行と9文字後になります。なぜなら、1行あたり20バイトを書き込む効果は、0から開始して19まで進む(20で中止)か、20から開始して1まで下がる(0で中止)かのどちらかで達成できるからです。後者の方が比較を節約できるため、コードサイズが速く小さくなります。
lda #<(SCRRAM + 40 + 9)
sta BLOCKPTR
lda #>(SCRRAM + 40 + 9)
sta BLOCKPTR + 1
; 今回は20行×20文字、文字160を書き込みます。
; 通常、Xを横方向、Yを縦方向と考えますが、
; 65xxはYをインデックスとする間接アドレッシングしか提供しません。
ldx #20
.field:
lda #160
ldy #20
.row:
sta (BLOCKPTR), y
dey
bne .row
; BLOCKPTRに40を加算します(16ビット加算)。
; Aを破壊するので、.fieldループの先頭で再ロードする必要があります。
lda BLOCKPTR
adc #40
sta BLOCKPTR
lda BLOCKPTR + 1
adc #0
sta BLOCKPTR + 1
dex
bne .field
スクリーンRAMが完全に設定されたので、次はカラーRAMです。後で(先読みして)わかるように、ブロックフィールド用の400個の連続した黒/白の値のダブルバッファリング技術を使用します。これはフレームごとにカラーRAMにレンダリングされます。したがって、初期化は内部ブロックフィールドバッファの設定になります。
BLOCKBUF = $CE00 ; 512バイトの未使用RAM、十分です。
lda #<BLOCKBUF
sta BLOCKPTR
lda #>BLOCKBUF
sta BLOCKPTR + 1
; 初期ブロックフィールドは、半分が黒(色0)、半分が白(色1)です。
; 言い換えれば、20行×10個の黒と10個の白です。
; 言い換えれば、40個の交互の黒と白の領域です。
lda #0
ldx #40
.fill:
; 内側のループ、10個の領域(黒または白)を書き込みます。
; 今回は、9から0までを順番に書き込み、-1で停止します。
ldy #9
.fill_row:
sta (BLOCKPTR), y
dey
bpl .fill_row
; BLOCKPTRに10を加算します(16ビット加算)。
; Aを破壊しますが、今回は固定値を新しく書き込むことはできません。
; なのでAをスタックにプッシュします。
pha
lda BLOCKPTR
adc #10
sta BLOCKPTR
lda BLOCKPTR + 1
adc #0
sta BLOCKPTR + 1
; Aをスタックからプルし、XORします。