アセンブリにまったく慣れていないので、信じられないかもしれませんが、最初の割り当ては、アセンブリでヘビを作成することです。ヘビはどのように保管すればよいですか?スタックに入れるべきですか、それともレジスターに入れるべきですか?私はこの「ひどい」言語について約3日間調査しましたが、始めるための良い方法を見つけることができません。私はおそらくc++で何らかのリンクリストを使用するでしょうが、残念ながらこれはc++ではありません。
どんな助けでも大歓迎です
アセンブリにまったく慣れていないので、信じられないかもしれませんが、最初の割り当ては、アセンブリでヘビを作成することです。ヘビはどのように保管すればよいですか?スタックに入れるべきですか、それともレジスターに入れるべきですか?私はこの「ひどい」言語について約3日間調査しましたが、始めるための良い方法を見つけることができません。私はおそらくc++で何らかのリンクリストを使用するでしょうが、残念ながらこれはc++ではありません。
どんな助けでも大歓迎です
ヘビの頭は、x、y座標の1つのペアで追跡できます。そしてもちろん、あなたは頭の現在の方向が必要です。 (テールにはx、yペア以上のものが必要です。おそらく、x、yペアの循環バッファーが最善の策です。その場合、履歴バッファーとは別にテールx、yは実際には必要ありません。)
@Danielの答えが説明しているように、頭が動くときに描画しようとしている次のピクセルを見ると、ヘビが何を食べているかがわかります。何もない、リンゴ、壁、またはそれ自体です。
(ビデオRAMへの効率的な読み取りアクセスがある場合は、シャドウボードアレイを保持する代わりに、ピクセルを読み取ることができます。ただし、最近のほとんどのシステムには、効率的なビデオRAM読み取りアクセスがありません。通常のx86 VGAメモリでは、書き込みでキャッシュできません。しかし、実際の8086を作成しているふりをすると、VGA RAMはISAバスの反対側にあるため、速度も遅くなります。ただし、8086にはキャッシュがありませんでした。以前のシステムの中には、ビデオRAMが搭載されていたものもあります。メインメモリの一部、IDK。)
もう1つのオプションは、リンゴとヘビの正方形のリストを検索することですが、これは1つの配列エントリをチェックするよりもはるかに手間がかかります。ゲーム(または他のリアルタイムソフトウェア)を作成する際の重要な要素は、最悪の場合のパフォーマンスに厳しい上限を設けることです。ヘビが乗っているマス目がたくさんあり、ステップ時間が短い(ヘビの動きが速い)場合、検索に時間がかかりすぎるため、ゲームが分厚い状態になり、一部のステップが他のステップよりも長くなることは望ましくありません。それはすぐに再生できなくなります。
したがって、おそらく、空、壁などのrows * cols
コードを格納するバイトの配列(2Dインデックスを作成する)が必要になります0
1
。パレットビデオモードを使用している場合、VRAMに直接描画している場合、または最新のビデオAPI、スクリーンバッファー、またはOpenGLテクスチャを使用している場合は、ビデオRAMのピクセルと同じコードを使用できます。それ以外の場合は、状態配列にコードがあり、画面バッファーに24ビットまたは32ビットのピクセルがあります。
インデックス作成を簡素化するために、ボード幅がそうでない場合でも、ストレージフォーマットに2行ストライドの累乗を使用させることができます。したがって、すべての行の最後にパディングの列が存在する可能性があります。ただし、これは多くのメモリを浪費する可能性があり、通常は現在の位置を基準にしてインデックスを作成するだけで済みます(たとえば、左/右の次の列の場合は+ -1バイト、次の行を上下に移動するには+ -row_stride)。
ヘッド/テールのx、y座標は、フラットボード配列への単一のインデックスまたはポインタにすることができますが、グラフィックを個別に描画するには、実際のx、y座標も必要です。
すべてのステップの後、尾の四角もクリアする必要があります(ヘビが最近のリンゴからまだ成長している場合を除きます。保留中の成長がいくつあるかをカウントダウンする必要があります)。テールがx、yであるため、画面を黒に戻す場所がわかります。
しかし、次のステップが頭がたどった実際の経路をたどるように、尾x、yをどのように更新しますか? 尻尾の周りの四角を見ると、どれが次に古いかがわかるといいと思うかもしれません。2つの異なる弾道が同じヘッドとテールで同じボード位置につながる例ではそうではないことを証明できます。プレイヤーは、水平または垂直にジグザグに移動することで、このボードレイアウトを作成できます。
H123 H167 H is the head, T is the tail
654 258 snake segments are 1..8 in order of moves.
78T 34T
1..8の履歴がなければ、すべての正方形は単なる「蛇」であり、尾を消去した後、尾がどちらの方向に移動するかを明確に決定できるアルゴリズムはありません。(ボード全体を見渡すことができる遅いアルゴリズムでさえ。)
尾を囲む8つの正方形だけを見る合理的なアルゴリズムには、他にもあいまいなケースがあります。
54 H12 # defeating a "local" algorithm
T321H 34
T5
だから私たちはどういうわけか歴史を記録しなければなりません。 私は最良かつ最も単純なオプションは次のとおりだと思います:
スネークは、スネークが画面上をクロールするのとほぼ同じ方法でメモリアドレスをクロールするため、このデータ構造が単純な実装につながるのは適切であり、偶然ではありません(ステップごとに実行する作業はほとんどありません)。
このバッファのサイズは、サポートできる最大のヘビの長さを設定します。その時点で、バッファエントリを再利用して、尾を読み取った直後に頭を記録します。これを行*列より大きくすることができるので、必要に応じて、ゲームは実際の制限を課しません。
これが、多くの本物の古典的なヘビゲームが最大のヘビの長さを持っている技術的な理由かもしれません。(ゲームプレイの理由は別として。)CPUで実行されるのはゲームだけなので、常に同じサイズの循環バッファーを使用しない理由はありません。つまり、必要なだけの大きさで静的に割り当てられます。そうすれば、成長した後などにゲームをコピーすることでゲームが途切れることはありません。
あなたはこのCに似たasmを書くかもしれません:
typedef struct xy {uint8_t x, y;} xy_t;
static const unsigned max_snakelen = 512;
struct xy snakepath[max_snakelen];
// uint16_t [] board array offsets is great if we don't also need x,y for graphics
enum board_entry { SQUARE_EMPTY=0, SQUARE_APPLE, SQUARE_SNAKE, SQUARE_WALL };
static const unsigned rows = 40;
static const unsigned cols = 80; // including walls
board_entry board[rows * cols]; // we'll do 2D indexing manually because asm has to
static inline unsigned dir_to_board_byte_offset(enum cur_direction) { ... }
// top bit maps to +- rows, or bottom bit to +- 1
static inline xy_t dir_to_xy_offset(enum directions cur_direction) { ... }
// map 0..3 to {+1, 0}, {-1,0}, {0,+1}, {0,-1} in some order.
void step(enum directions cur_direction)
{
static unsigned tailidx = 0; // maybe kept in a register
static unsigned headidx = 5; // and arrange for the initial snake to be in the buffer somehow
if (!growth) // tail stays still while snake grows
--growth;
xy_t tail = snakepath[tailidx++]; // movzx edx, byte [snakepath + rbx*2] // movzx ecx, byte [snakepath + rbx*2 + 1]
tailidx &= max_snakelen - 1; // wrap the circular buffer idx. AND ebx, MAX_SNAKELEN - 1
board[tail.y * cols + tail.x] = SQUARE_EMPTY; // imul stuff
// and update graphics
}
// Most of the registers used for the above stuff are dead now, and can be reused below
// Tail segments disappear *before* the head moves: snake can be head-to-tail
// and go full Ouroboros without crashing.
xy_t headxy = snakepath[headidx++]; // or track head separately, maybe in a reg so we can more quickly like our function arg.
headidx &= max_snakelen - 1;
headxy += dir_to_xy_offset(cur_direction); // maybe use a 16-bit add to do both components in parallel, except that `-1` will carry into the upper element. So that only works with SIMD `paddb` or something.
// pretend that's a C++ overload and do the component adds separately
enum board_entry headtype = board[headxy.y * cols + headxy.x];
if (headtype != SQUARE_EMPTY) {
apple or death;
}
board[headxy.y * cols + headxy.x] = SQUARE_SNAKE;
// ADD NEW HEAD to circular buffer
snakepath[headidx] = headxy; // mov [snakepath + 2*rbx], ax
// and draw graphics for head, using its x,y.
}
これはあなたに一般的な考えを与えることを意図しているだけです、それはかなり不格好で、良いCスタイルではありません。(そしてそうすることを意図していません。)私はすべてが宣言されているわけではないことを知っています。キー押下とタイマーイベントを待機するイベントループのasmバージョンのレジスタにどの程度の状態を保持するかはあなた次第です。呼び出す関数はregを保存/復元する必要がありますが、標準の呼び出し規約を使用していて、とにかくそれを行う場合は、最も外側のループがその状態をregに保持することに害はありません。
以前は乗算が遅いため、スネーク構造体にx、y 、 1D配列のインデックスまたは実際のポインターを保持することを検討してください。しかし、最近のCPUは一般的に高速の乗算器を備えています。(Core 2以降のIntelでは3サイクルのレイテンシーが完全にパイプライン化されているのに対し、パイプライン化されていない8086では100を超えています)。
特に16ビットマシンでは、絶対アドレスを複数の命令に埋め込むことで、多くのコードバイトを消費することはありません。32ビットコードではさらに悪化します。x86-64は、Linuxの位置依存コードで32ビットの絶対アドレスを使用できますが[snakepath + rbx*2]
、のようなアドレッシングモードを使用できます。それ以外の場合は、RISCを使用して、レジスタ内の少なくとも1つのベースアドレスを取得し、それに関連する静的データを参照します。
ターゲットISAに応じて、レジスタに保持するポインタの数が増減する場合があります。
別の方法は、プレーヤーが向きを変えるたびに記録することです。ただし、最悪の場合、プレーヤーはステップごとに1回回転するため、ヘビの長さと同じ数のバッファーエントリが必要です。(または、ゲームプレイが少ない場合は、非常に望ましくないゲームプレイになります。過去にターンしすぎたために、プレーヤーが予期せずターンできなくなり、イライラする死を迎える可能性があります。)
各エントリには、少なくともx、yペアと同じスペースが必要であり、デコードには少し多くの作業が必要です。(毎ターンx、yペアだけを記録すると、最後のターンに対するテールx、yを見て、パスを再構築するのに十分な情報が得られます。)
ただし、前方向+長さは、実際にははるかにコンパクトになる可能性があります。レコードあたり1バイト。ビットをビットフィールドにパックする場合、方向は2ビットかかります。ボードが63 = 2 6 -1の正方形よりも幅が広い、または高い場合でも、長さを6ビットに保つために、新しいエントリを早期に作成することを選択できます。したがって、各エントリは簡単に1バイトになります。dir = x & 3
(とでデコードlen = x >> 2
)。
このdir+len形式を使用するには、テールエントリの長さを0に達するまでデクリメントします(つまり、テールがターンに到達し、次のバッファエントリの確認を開始する必要があります)。効率を上げるために、現在作業しているものを開梱したままsub byte [rsi], 4
にするか、キャリーを使用してチェックします(デクリメントしたときに長さがすでにゼロであったかどうかを検出し、それによってラップされました)。
6年後にはもう問題にならないことを願っていますが、私の場合はピクセルに基づいています。たとえば、ヘビの頭に割り当てられたピクセルがありました。私は常にその場所(x軸とy軸)を保存し、ループ内でヘビの方向にペイントされたピクセルがあるかどうかを確認しました。
それが黒い場合-私は何もせず、ヘビを動かし続けました。黒が背景でした。
もしそれが赤だったら-私はヘビがリンゴを食べようとしていることを知っていました。
青い場合は、ヘビが壁にぶつかろうとしていることなどがわかりました。
ヘビはどのように保管すればよいですか?スタックに入れるべきですか、それともレジスターに入れるべきですか?
あなたがヘビのアニメーション/ゲームについて話していると仮定すると、答えはおそらくどちらでもありません。ほとんどの場合、2次元配列を使用して「画面」上のセルを表し、ヘビの体を特定の「色」のセルとして表します。
私はおそらく、データ構造ライブラリを使用せずにCまたはC ++でコードを実装する最も簡単な方法を考え出すことから始め、次にアセンブラでアルゴリズムを再コーディングします。