Atari2600開発

このスクラップについて
Atari2600の開発について書いていくスクラップです

参考記事
AtariAgeコミュニティ
- Atari 2600 Programming
-
Docs Tutorials and Tools for Atari 2600 Programming
- 色々なツールなど紹介してくれてるページ
- "ATARI AR" a new art project is looking for a master of the homebrew
開発関連のサイト
- Atari 2600 Programming for Newbies
- STELLA PROGRAMMER'S GUIDE
- Atari2600詳細 - WentWayUp: WebLog
- Collision Registers
- Atari VCSはなぜ死んだ?--知られざるゲーム古代王朝の謎にせまる
- Atari 2600 VCS Programming
- Atari 2600 Tech Page
- 6502 と 6800 の減算時キャリーフラグの挙動の違い
- 6502 アセンブラ プログラミング入門
- 6502 | NES研究室
- TIA Hardware Notes
- The HMOVE artifacts
- [stella] The scores / 48-pixel highres routine explained!
- 6502マシン語ゲームプログラミング
- Atari 2600 Schematics - PAL
- Atari 2600 on a breadboard, part II: reading a cart
- My 2600 cart dumper
開発関連の記事
- When do I use Resp0 command to put sprite on left of screen?
- 7 Digit Full Sized Score Display
- Multi-Sprite Trick behavior
開発ツール
- Atari Dev Studio
- Dasm macro assembler for VSCode
-
Tiny 8-Bit Sprite Editor
- シンプルなスプライトの作成
-
playerpal 2600
- 色付きアニメとかだとこっち
-
atari-background-builder v0.98
- タイトル作る時にめっちゃ便利らしい。画像読み込んだり、ファイルの保存もできる。
-
dasm
- macro.h, vsc.hのヘッダーファイルをここから取得する
エミュレータ
ハード関連の情報
- eBay 実機購入
-
Harmony
- SDカード内のロムを起動できるので開発に便利なアイテム
-
Custom Atari 2600 Cartridge
- ラベルプリントも含めてカートリッジを作ってくれるサービス
-
Atari 2600 8K/16K/32K/64K PC Board
- ここでも焼ける
書籍
その他

TIAについて1
Atari 2600 Programming for NewbiesのSession_1からSession_12まで読んだ
とにかくグラフィクスに関する勝手がファミコンとは全然違うなぁという感じ。
- ファミコンだとBGを描画するにはVRAMのパターンテーブルの領域にキャラクタデータを書き込んでおいて、その番号をネームテーブルなりに書き込んでおくと、勝手にBGが敷き詰められたが
- Atari2600の場合はTIA(Television Interface Adaptor)というグラフィクス用のチップがあって、そのチップに背景色を保持するレジスタ領域があり、それをテレビの走査線のタイミングを考慮しながら書き換えて背景を表示するという感じ。(Session 7: The TV and our Kernel)Atari2600のゲームを見るとファミコンみたいにタイルな感じではないと思ったが、そういうことか。
- ゲームプログラミングだとティアリングを起こさないためにしばしばVBLANKを意識することはあったが、走査線の細かい位置まで考慮して背景色レジスタを書き換えるなどしなければならないとは、という感じ
- ファミコンの場合はPPU(PictureProcessingUnit)というグラフィクス用のチップがあり、これがAtari2600とは使い勝手が全然違うなぁという印象。同じ6502のCPUでも、グラフィクス用のチップで作り方が全然変わるなと。
それ以外の要素として面白いのが
- TIAのレジスタにはストロボタイプのレジスタというのがあって、例えば
sta WSYNC
は、一見するとAレジスタの値をWSYNCのアドレスに書き込んでいるように見えるが、実際はWSYNC(水平同期)まで停止するというもの。これで水平同期や垂直同期のタイミングまで停止といった動作が実現できる

TIAについて2
ファミコンだとBGを描画するには
と思ったけど
- Session 13: Playfield Basicsを見ると、どうもさっきのは単なる背景色に関する話で、ファミコンで言うBGみたいなものは、プレイフィールドという概念がTIAにあって、それを使うような気がする
- TIAにはスプライトとしてプレイヤー0, 1、ミサイル0, 1、ボールという5つのスプライト機能があって、これとプレイフィールドを組み合わせてゲーム性を構築する感じっぽい(ファミコンと比べてかなり独自のスプライト機能という印象。でも昔のゲーム機はコントローラーも独特で、やれることが限られていたからファミコンみたいな汎用ゲーム機とは違うのだろう)

TIAについて3
なんとなく分かってきたことは
- Atari2600にはVRAMがなく、グラフィクスに関するレジスタを設定するのみで、ファミコンのようにあらかじめBG情報やパレット情報をVRAMに書き込んでおくなどはできない
- HBLANKの間にそのスキャンラインにかかっているオブジェクトのそのラインのビット情報をTIAのプレイフィールドやらプレイヤーのスプライトのレジスタに送っておかないといけない(毎スキャンライン毎にそのスキャンラインで必要な描画情報を渡すという処理が必要)
- 画面の表現は前述の通り
背景色
,プレイフィールド
,プレイヤー
,ミサイル
,ボール
のみで、これをHBLANK中(場合によってはスキャンライン中)に書き換えて表現する

ゾーンというテクニックについて
たった5つのスプライトでどうやって表現するのかと思っていたが、ゾーンで分けてレンダリングを行うという考え方があるらしいことを知った
例えば
こういう感じで画面を6つのゾーンに分けて、各ゾーンのレンダリング処理に入る手前でプレイヤー0, 1のスプライトを書き換えてこのゾーンではこのスプライトを表示し...といった感じでゾーンごとに処理するというもの。

当たり判定について1
スプライトにはY軸情報がなく、じゃあどうやって当たり判定をするんだ?と思ったら、この記事が参考になった
どうやら、プレイヤー0, 1やミサイル0, 1をレンダリングしたタイミングで、同じピクセル領域にレンダリングされたら、CXM0PやらCXM1Pやらのコリジョン検出用のレジスタの該当のビットが1になるということだった。
(つまり、ピクセル単位で当たり判定がチェックされる)
そうなると、例えばゾーンごとの処理の最後に当たり判定をチェックなどすれば、複数のスプライトの当たり判定もできそうではある。
(ゾーンを超えた大きさのスプライトを表現する場合はちょっと悩むかも)
ゾーンの考え方は4分木空間分割と同じように、描画領域を分けるという意味もあるが、当たり判定のチェックを分けるという用途にもなるなぁと感じた。

Atari2600の全ゲーム集の動画
この動画はAtari2600の全ゲーム集↓
The Atari 2600 Project - All 511 Games
なんでこの制約でこんな表現ができるのか、すごい。

VSYNCについて
VBLANKやVSYNCを待つときに
lda #2
sta VBLANK
sta VSYNC
REPEAT 3
sta WSYNC
REPEND
lda #0
sta VSYNC
REPEAT 37
sta WSYNC
REPEND
sta VBLANK
といったコードがあり、VSYNCやVBLANKに値2(正確にはD1に1という意味)を入れて、その後に値0を入れるという処理がある。
以前この記事を見て sta VSYNC
でVSYNCまでAtari2600を停止するという意味だと思っていたので、この2を入れたり0を入れたりするのがよくわからない。
で STELLA PROGRAMMER'S GUIDE を読んでいると、3.3項に
3.3 Vertical timing
When the electron beam has scanned 262 lines, the TV set must be signaled
to blank the beam and position it at the top of the screen to start a new
frame. This signal is called vertical sync, and the TIA must transmit this
signal for at least 3 scan lines. This is accomplished by writing a “1” in D1
of VSYNC to turn it on, count at least 2 scan lines, then write a “0” to D1 of
VSYNC to turn it off.
3.3 縦 タイミング
電子ビームが262ラインを走査したとき、テレビはビームをブラン
クにし、新しいフレームを開始するために画面上部に位置させる
よう信号を送らなければならない。この信号は垂直同期と呼ばれ、
TIAはこの信号を少なくとも3走査線分送信しなければならない。
これは、VSYNCのD1に "1 "を書き込んでオンにし、少なくとも2走
査線をカウントした後、VSYNCのD1に "0 "を書き込んでオフにす
ることで達成される。
という記載がある。これを見ると、どうも垂直同期信号を受け取るのではなく、送るものだというニュアンスが分かる。
で、そもそもテレビとゲーム機は映像信号を送るだけ(と思っている)で制御しているのだから、どのようにして垂直同期のタイミングを知ることができるのかと調べたりChatGPTに聞いてみたところ
次のような点によって垂直同期タイミングが確立されます:
信号フォーマット: テレビは特定の信号フォーマット(例: NTSC、PAL)を解釈します。これには、垂直同期パルスと水平同期パルスが含まれます。
信号の構造: 映像信号には、画像データの他に垂直同期パルスと水平同期パルスが含まれています。これらのパルスは画面の描画を制御するために使用されます。
信号の規格: 映像信号は標準化されており、各信号の要素は特定の規格に従います。これにより、テレビは信号を解釈し、垂直同期や水平同期などのタイミングを特定することができます。
ゲーム機はこれらの信号を生成し、テレビに送信することで、テレビの画面描画を制御します。そのため、テレビは受信した信号から垂直同期や水平同期のタイミングを把握し、正確な映像を表示します。
とのこと。
そもそもテレビに対する信号に垂直同期パルスや水平同期パルスを送ることができ、これをTIAを通して行っているというのがおそらく正しそう
(テレビ→ゲーム機に同期信号が送られていると勘違いしていたがそうではなく、ゲーム機側から同期信号を送るというのが正しそう)

ビデオ信号について
そもそもアナログテレビに対する信号の理解が足りていないかと思い調べていたら
が役にたった。
ビデオ信号には同期信号が必要
動画像は静止画像の集まりですが,走査により電気信号
に変換すると,どこが画像の始まりか分からなくなります.
そこで画像の始まりには印が付けられます.これはアナロ
グもディジタルも同じです.これを同期信号といいます.
ビデオ信号には画像の始まり(上端)と水平走査(ラインと
もいう)の始まり(左端)におのおの,垂直同期信号と水平
同期信号が挿入されています.図5は正常な画像(a)に対
して,(b)は垂直同期が外れた場合,(c)は水平同期が外
れた場合の画像を示します.
とあり、この点からも、同期信号を送るのは映像を出力したい側(ゲーム機)であることが分かる。
テレビとゲーム機で双方向でなんらかの信号を送り合うことはできないが、ゲーム機側から同期信号を送って、双方ともそれに合わせてタイミングを合わせることで、期待した映像を出力できるようになるのかなと思う。

VBLANKについて
sta VSYNC
は垂直同期信号を送るということで理解したが sta VBLANK
がいまいち何をやっているのか判然としない
To physically turn the beam off during its repositioning time, the TV set
needs 37 scan lines of vertical blanks signal from the TIA. This is
accomplished by writing a “1” in D1 of VBLANK to turn it on, count 37
lines, then write a “0” to D1 of VBLANK to turn it off. The microprocessor
is of course free to execute other software during the vertical timing
commands, VSYNC and VBLANK.
再配置時間中にビームを物理的にオフにするには、TV セットは
TIA から 37 スキャンラインの垂直ブランク信号が必要である。こ
れは、VBLANKのD1に "1 "を書き込んでオンにし、37ラインをカウ
ントした後、VBLANKのD1に "0 "を書き込んでオフにすることで達
成される。もちろんマイクロプロセッサは、垂直タイミング・コマ
ンドであるVSYNCとVBLANKの間、自由に他のソフトウェアを実
行することができる。
とあったり
I don't understand screen synchronisation
のフォーラムの質問だとVBLANKは画面上部にリセットみたいな旨が書かれている。(それはVSYNCではないのか?)
色々情報があるが、現時点の解釈だと sta VBLANK
は、TIAに垂直ブランク期間であることを伝え、それによってTIAがテレビに映像信号を送らないようにするという感じもしている(その結果テレビのビームはOFFになる)
つまりやってもやらなくてもどうせ垂直ブランク期間は映像が映されないから、どっちでも良いのか?

スプライト描画について
HMP0, RESP0, HMOVE 周りがかなり理解するのに苦戦
以下の記事を参考にした
- Session 22: Sprites, Horizontal Positioning (Part 1)
- When do I use Resp0 command to put sprite on left of screen?
- Atari 2600 TIA Hardware Notes
- The HMOVE artifacts
なんとなく得られたのは
- まずそもそもとして、スプライトを表示するX座標を指定することはできない
- 表示したいタイミングに走査線が来たら
sta RESP0
でスプライトの表示タイマーをリセットするとそこから描画が開始され、以降次の走査線でも同じタイミングで描画される - つまり、自分の表示したいタイミングまで(クロック数を調整して)待って開始する
- これに加えて
HMP0
の上位4ビットに-7~8の値をセットすることで若干左右に調整できる - HMOVE によって水平位置の調整が反映される(多分)
というところ

サンプルコードを読んで日本語コメント付けた
単純にスプライトを表示して動かすだけの以下のようなサンプルコードを読んで日本語コメントつけて理解を深めたのでそのコードを張っておく
processor 6502
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; インクルード文
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
include "vcs.h"
include "macro.h"
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; RAM領域 RAMは $80 から128バイト使える
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
seg.u Variables
org $80
JetXPos byte ; 機体のX座標
JetYPos byte ; 機体のY座標
Random byte ; ランダム値
Temp byte ; 一時領域
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; 定数
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
JET_HEIGHT = 9 ; 機体の高さ
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; プログラムコードを $F000 から開始する
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
seg Code
org $F000
Reset:
CLEAN_START ; メモリとレジスタをクリアするマクロ
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; 変数とTIAレジスタを初期化する
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;s
lda #60
sta JetXPos ; 機体のX座標を 60 に設定
lda #60
sta JetYPos ; 機体のY座標を 60 に設定
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; フレームの開始
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
StartFrame:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; 垂直同期前の計算と処理
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
lda JetXPos ; Aレジスタに機体のX座標をロード
ldy #0 ; Yレジスタに0をロード
jsr SetObjectXPos ; SetObjectXPosサブルーチンを呼び出す(多分AとYレジスタを引数代わりにしてる) jsrはそのアドレスの命令にジャンプすること
sta WSYNC ; 水平同期を待つ
sta HMOVE ; 水平位置を反映する
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; 垂直同期の開始
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
lda #%00000010
sta VSYNC ; 垂直同期信号を送る
REPEAT 3
sta WSYNC ; 水平同期を3つ待つ(垂直同期信号を送ってから3ライン分待つ必要がある)
REPEND
lda #%00000000
sta VSYNC ; 垂直同期信号を停止
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; 垂直ブランク中の処理
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
lda #%00000010
sta VBLANK ; 垂直ブランクを開始(多分この間TIAからテレビに信号が送られない)
REPEAT 37
sta WSYNC ; 垂直ブランク分の期間(水平同期37回分)を待つ
REPEND
lda #%00000000
sta VBLANK ; 垂直ブランクを終了
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; 画面の描画処理(192スキャンライン分)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
ldx #192 ; 192を X に入れる
.GameLineLoop:
.AreWeInsideJetSprite: ; 機体を描画するかどうかを判定する処理
txa ; X を A にコピー
sec ; キャリーフラグを1にセット(キャリーフラグは計算命令で繰り上がりや繰り下がりが起きたときに立つフラグ)
sbc JetYPos ; A - 機体のY座標 の計算を行いその結果を A に入れる
cmp JET_HEIGHT ; Aと機体の高さを比較(A - 機体の高さを計算してそのフラグを残す。もし繰り下がりが起きたらキャリーフラグが1になる)
bcc .DrawSpriteP0 ; キャリーフラグが0なら繰り下がりが発生しており描画範囲内なので機体を描画する
lda #0 ; 描画範囲内でない場合はA に 0 をセットして描画されないように調整(JetSprite+0は#%00000000のため結果的に何も描画されない)
; 処理の流れの例:
; [ ] X:192, JetYPos:60 の場合は 192-60=132 が A に入りそこから 132-9=123 になり繰り下がりが発生せずキャリーフラグは1のままで描画されない
; ...
; [ ] X:70, JetYPos:60 の場合は 70-60=10 が A に入りそこから 10-9=1 になり繰り下がりが発生せずキャリーフラグは1のままで描画されない
; [ ] X:69, JetYPos:60 の場合は 69-60=9 が A に入りそこから 9-9=0 になり繰り下がりが発生せずキャリーフラグは1のままで描画されない
; [#] X:68, JetYPos:60 の場合は 68-60=8 が A に入りそこから 8-9=-1 になり繰り下がりが発生してキャリーフラグが0になり描画される
; [#] X:67, JetYPos:60 の場合は 67-60=7 が A に入りそこから 7-9=-2 になり繰り下がりが発生してキャリーフラグが0になり描画される
; [#] X:66, JetYPos:60 の場合は 66-60=6 が A に入りそこから 6-9=-3 になり繰り下がりが発生してキャリーフラグが0になり描画される
; [#] X:65, JetYPos:60 の場合は 65-60=5 が A に入りそこから 5-9=-4 になり繰り下がりが発生してキャリーフラグが0になり描画される
; [#] X:64, JetYPos:60 の場合は 64-60=4 が A に入りそこから 4-9=-5 になり繰り下がりが発生してキャリーフラグが0になり描画される
; [#] X:63, JetYPos:60 の場合は 63-60=3 が A に入りそこから 3-9=-6 になり繰り下がりが発生してキャリーフラグが0になり描画される
; [#] X:62, JetYPos:60 の場合は 62-60=2 が A に入りそこから 2-9=-7 になり繰り下がりが発生してキャリーフラグが0になり描画される
; [#] X:61, JetYPos:60 の場合は 61-60=1 が A に入りそこから 1-9=-8 になり繰り下がりが発生してキャリーフラグが0になり描画される
; [#] X:60, JetYPos:60 の場合は 60-60=0 が A に入りそこから 0-9=-9 になり繰り下がりが発生してキャリーフラグが0になり描画される
; [ ] X:59, JetYPos:60 の場合は 59-60=-1 が A に入りそこから 255-9=246 になり繰り下がりが発生せずキャリーフラグは1のままで描画されない
; [ ] X:58, JetYPos:60 の場合は 58-60=-2 が A に入りそこから 254-9=245 になり繰り下がりが発生せずキャリーフラグは1のままで描画されない
; ...
; [X] X:0 , JetYPos:60 の場合は 0-60=-60 が A に入りそこから 196-9=187 になり繰り下がりが発生せずキャリーフラグは1のままで描画されない
.DrawSpriteP0:
tay ; A を Y にコピー(もし描画範囲内なら9~0の値がAに入っている。JetSpriteは上下反転になっているので9のときに上端が描画される)
lda JetSprite,Y ; JetSpriteのアドレスに Y を足してその値を A にロード
sta WSYNC ; 水平同期を待つ
sta GRP0 ; プレイヤー0に A の値をセット
lda JetColor,Y ; JetColorのアドレスに Y を足してその値を A にロード
sta COLUP0 ; プレイヤー0の色に A の値をセット
dex ; X をデクリメント
bne .GameLineLoop ; 計算結果が 0 でない場合は .GameLineLoop に戻る
sta WSYNC ; 水平同期を待つ
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; オーバースキャン中の処理
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
lda #%00000010
sta VBLANK ; 垂直ブランクを開始(多分この間TIAからテレビに信号が送られない)
REPEAT 30
sta WSYNC ; オーバースキャン分の期間(水平同期30回分)を待つ
REPEND
lda #%00000000
sta VBLANK ; 垂直ブランクを終了
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; ジョイスティックの処理
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
CheckP0Up:
lda #%00010000 ; ジョイスティック上のビットを立てた値を A にセット
bit SWCHA ; SWCHA とのビット比較演算(AND演算される)
bne CheckP0Down ; 計算結果が 0 でない場合はジョイスティック下の処理にジャンプ
jsr UpJetYPos ; 機体を上に移動するサブルーチンを呼び出す
CheckP0Down:
lda #%00100000 ; ジョイスティック下のビットを立てた値を A にセット
bit SWCHA ; SWCHA とのビット比較演算(AND演算される)
bne CheckP0Left ; 計算結果が 0 でない場合はジョイスティック左の処理にジャンプ
jsr DownJetYPos ; 機体を下に移動するサブルーチンを呼び出す
CheckP0Left:
lda #%01000000 ; ジョイスティック左のビットを立てた値を A にセット
bit SWCHA ; SWCHA とのビット比較演算(AND演算される)
bne CheckP0Right ; 計算結果が 0 でない場合はジョイスティック右の処理にジャンプ
jsr LeftJetXPos ; 機体を左に移動するサブルーチンを呼び出す
CheckP0Right:
lda #%10000000 ; ジョイスティック右のビットを立てた値を A にセット
bit SWCHA ; SWCHA とのビット比較演算(AND演算される)
bne EndInputCheck ; 計算結果が 0 でない場合はジョイスティック処理の終了にジャンプ
jsr RightJetXPos ; 機体を右に移動するサブルーチンを呼び出す
EndInputCheck: ; ジョイスティック処理の終了
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; フレームの終了処理
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
jmp StartFrame ; フレーム開始にジャンプ
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; 対象のX座標の位置をセットする
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; A は対象のピクセル単位のX座標
;; Y は対象の種類 (0:player0, 1:player1, 2:missile0, 3:missile1, 4:ball)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
SetObjectXPos subroutine
sta WSYNC ; 水平同期を待つ
sec ; キャリーフラグを1にセット(キャリーフラグは計算命令で繰り上がりや繰り下がりが起きたときに立つフラグ)
.Div15Loop
sbc #15 ; A から 15 を減算してその結果を A にセット
bcs .Div15Loop ; キャリーフラグが 0 になるまで繰り返す(このループを抜ける時は A を 15 で割った余りが A に入る)
eor #%0111 ; A と 7(%0111) でXORして A を -8~7 に調整する
asl ; A を左に4ビットシフト(このあとのHMP0には上位4ビットにセットする必要があるため)
asl
asl
asl
sta HMP0,Y ; 指定のスプライト(Y の値によってどのスプライトかが変わる)の水平位置のオフセット値をセット(-7~8)
sta RESP0,Y ; 指定のスプライト(Y の値によってどのスプライトかが変わる)の描画を開始
rts
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; ジョイスティックの操作によって機体の座標を動かすサブルーチン群
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
LeftJetXPos subroutine
ldx JetXPos
dex
stx JetXPos
rts
RightJetXPos subroutine
ldx JetXPos
inx
stx JetXPos
rts
UpJetYPos subroutine
ldx JetYPos
inx
stx JetYPos
rts
DownJetYPos subroutine
ldx JetYPos
dex
stx JetYPos
rts
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; データ
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
JetSprite:
.byte #%00000000 ;
.byte #%00010100 ; # #
.byte #%01111111 ; #######
.byte #%00111110 ; #####
.byte #%00011100 ; ###
.byte #%00011100 ; ###
.byte #%00001000 ; #
.byte #%00001000 ; #
.byte #%00001000 ; #
JetColor:
.byte #$00
.byte #$FE
.byte #$0C
.byte #$0E
.byte #$0E
.byte #$04
.byte #$BA
.byte #$0E
.byte #$08
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; 末尾
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
org $FFFC
word Reset
word Reset

簡単なプレイフィールドの描画
囲う感じでプレイフィールドを描画した
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; 画面の描画処理(192スキャンライン分)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
ldx #192 ; 192を X に入れる
.GameLineLoop:
sta WSYNC ; 水平同期を待つ
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; プレイフィールドの描画処理
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
cpx #176
bcs .DrawTopBottom16Line
cpx #16
bcc .DrawTopBottom16Line
.DrawPlayField
lda #%00110000
sta PF0
lda #%00000000
sta PF1
lda #%00000000
sta PF2
lda #$80
sta COLUPF
lda #%00000001
sta CTRLPF
jmp .AreWeInsideJetSprite
.DrawTopBottom16Line
lda #%11110000
sta PF0
lda #%11111111
sta PF1
lda #%11111111
sta PF2
lda #$80
sta COLUPF
lda #%00000000
sta CTRLPF

タイマーの使い方
他のサンプルコードを見ているとVBLANKを待つのに TIM64T
や INTIM
というニーモニックが出てくるのでそれについて。
STELLA PROGRAMMER'S GUIDE を見ると、これはタイマー機能で例えば
lda #10
sta TIM64T
とすると INTIM
の値がデクリメントされていき、マイナスになったらその時間が過ぎたことを表す
TIM64T
以外にもいくつかあり、下記
16進アドレス | インターバル | ニーモニック |
---|---|---|
294 | 1クロック | TIM1T |
295 | 8クロック | TIM8T |
296 | 64クロック | TIM64T |
297 | 1024クロック | T1024T |
サンプルコードだとVBLANK待ちに37回 sta WSYNC
をリピートする代わりにタイマーで
lda #42
sta TIM64T
としており、これは1スキャンラインが76クロックなので
- タイマー 64x42 = 2688
- 37スキャンライン = 2812
なので、だいたい合う?のか?(だいたいで良いのかよくわからないが)

疑似乱数テーブルを使った疑似乱数の生成
乱数カウンタとその値で擬似乱数テーブルから乱数値を引くサブルーチンを書いた
seg.u Variables
org $80
RandomCounter byte ; 乱数カウンタ
RandomValue byte ; 乱数値
; ...
NextRandomValue subroutine
inc RandomCounter ; 乱数カウンタをインクリメント
ldx RandomCounter ; 乱数カウンタを X にロード
lda RandomTable,X ; 乱数テーブル + X の値を A にロード
sta RandomValue ; A をRandomValueにセット
rts
; ...
; 乱数テーブル
RandomTable:
.byte $24, $3A, $0D, $C3, $56, $AF, $4E, $97
.byte $1C, $78, $FA, $D5, $09, $B2, $6E, $8C
.byte $3F, $40, $B9, $E6, $2D, $51, $A8, $C7
; 省略(乱数カウンタが1バイトなので0~255の値を取るので、256バイト分乱数テーブルを用意する

1つのゾーンでスプライト2つとプレイフィールドを同時に表示するには
※準備中
※スプライト2つとプレイフィールドを表示するには処理時間が足りずに思った表示にはならないのでそのことについて書く予定

音の出し方
音を出すには3種類のレジスタを操作する必要がある。(同じレジスタが2セットあるので、2つの音を同時に出すことが可能
レジスタ | 説明 |
---|---|
AUDC0,1 | トーン。音の種類(0~15)と思ってOK。フルートのような音だったりビームみたいな音だったり |
AUDF0,1 | 頻度・周波数。音の高さ(0~31)と思ってOK |
AUDV0,1 | ボリューム。音の大きさ(0~15) |
単純に音を出すだけなら以下のようになる
lda #9 ; 音の種類は9版
sta AUDC0
lda #8 ; 音の高さは8
sta AUDF0
lda #15 ; 音の大きさは最大の15
sta AUDV0
この値をデータとしてROMに入れておいて、フレームに合わせて次の音を出すというのをやっていくことになる。