オンラインでコードを共有して発見することの大きな喜びの 1 つは、本当に壮大で素晴らしいものに出くわしたときです。 https://github.com/axle-h/Retro.Net にアクセスして、この GitHub プロジェクトの星が 20 個しかない理由を自問してみてください。
アレックス・ハスルハースト Angular フロント エンドを使用して、オープン ソースの .NET Core でいくつかのレトロなハードウェア ライブラリを作成しました。
翻訳?
マルチプレイヤー サーバー側のゲーム ボーイ エミュレーター。エピック。
で数分で実行できます
docker run -p 2500:2500 alexhaslehurst/server-side-gameboy
次に、http://localhost:2500 にアクセスして、元のゲームボーイでテトリスをプレイしてください!
いくつかの理由でこれが気に入っています。
まず、私は彼の視点が大好きです:
<ブロック引用>私の .NET Core で書かれたゲームボーイ エミュレータをチェックしてください。 Retro.Net .はい、.NET Core で記述されたゲームボーイ エミュレーターです。なんで?なぜだめですか。このプロジェクトでの経験について、いくつかの記事を書く予定です。最初に:それが悪い考えだった理由 <オール>
.NET のようなプラットフォームで CPU をエミュレートしようとする際の最大の問題は、信頼できる高精度のタイミングがないことです。しかし、彼は Z80 プロセッサのゼロからの素晴らしいエミュレーションを管理し、非常に高レベルの C# でレジスタなどの低レベルのものをモデル化しています。パブリック クラスの GameBoyFlagsRegister が重要であることが気に入っています。;) 15 年前の「Tiny CPU」を .NET Core/C# に移植したときも、同様のことを行いました。

アレックスが Z80 マイクロプロセッサをどのようにモデル化したかについての非常に詳細な説明を必ずチェックしてください。
<ブロック引用>幸運なことに、Sharp LR35902 であるゲームボーイ CPU は、人気があり、非常によく文書化されている Zilog Z80 から派生したものです。Zilog Z80 は、導入から 40 年以上経った今でも信じられないほどまだ生産されているマイクロプロセッサです。
Z80 は 8 ビットのマイクロプロセッサです。つまり、各操作は 1 バイトでネイティブに実行されます。命令セットには 16 ビット演算がいくつかありますが、これらは 8 ビット ロジックの複数サイクルとして実行されるだけです。 Z80 には 16 ビット幅のアドレス バスがあり、これは論理的に 64K のメモリ マップを表します。データは 8 ビット幅のデータ バスを介して CPU に転送されますが、これはステート マシン レベルでのシステムのシミュレーションには関係ありません。 Z80 とそれが派生した Intel 8080 には、外部周辺機器にアクセスするための 256 の I/O ポートがありますが、ゲームボーイ CPU には何もありません - 代わりにメモリ マップド I/O を優先します
彼はただエミュレーターを作成しただけではありません - エミュレーターはたくさんあります - 独自にサーバー側でそれを実行し、ブラウザーでコントロールを共有できるようにしました。 「それぞれの一意のフレームの間に、接続されているすべてのクライアントが、次の制御入力がどうあるべきかについて投票できます。サーバーは、投票数が最も多いものを選択します...ほとんどの場合。」 大規模マルチプレイヤー オンライン ゲームボーイ! その後、彼は次のフレームをストリーミングします。 「GPU レンダリングはサーバー上で一意のフレームごとに 1 回完了し、LZ4 で圧縮され、接続されているすべてのクライアントに WebSocket 経由でストリーミングされます。」
これは優れた学習リポジトリです:
- サーバー側には複雑なビジネス ロジックがありますが、フロント エンドでは Angular と Web ソケット、およびオープン Web テクノロジが使用されています。
- Docker で .NET Core アプリと Angular アプリの両方を構築する方法の優れた例である、完全なマルチステージ Dockerfile を彼が持っていることも素晴らしいことです。
- Shouldly Assertion Framework と Moq Mocking Framework を使用した広範な (数千の) 単体テスト。
- リアクティブ プログラミングの優れた使用例
- Angular の Karma Unit Testing を使用した、サーバーとクライアントの両方での単体テスト
この巨大なレポジトリにあるお気に入りのエレガントなコード スニペットをいくつか紹介します。
リアクティブ ボタンの押下:
_joyPadSubscription = _joyPadSubject
.Buffer(FrameLength)
.Where(x => x.Any())
.Subscribe(presses =>
{
var (button, name) = presses
.Where(x => !string.IsNullOrEmpty(x.name))
.GroupBy(x => x.button)
.OrderByDescending(grp => grp.Count())
.Select(grp => (button: grp.Key, name: grp.Select(x => x.name).First()))
.FirstOrDefault();
joyPad.PressOne(button);
Publish(name, $"Pressed {button}");
Thread.Sleep(ButtonPressLength);
joyPad.ReleaseAll();
});
GPU レンダラー:
private void Paint()
{
var renderSettings = new RenderSettings(_gpuRegisters);
var backgroundTileMap = _tileRam.ReadBytes(renderSettings.BackgroundTileMapAddress, 0x400);
var tileSet = _tileRam.ReadBytes(renderSettings.TileSetAddress, 0x1000);
var windowTileMap = renderSettings.WindowEnabled ? _tileRam.ReadBytes(renderSettings.WindowTileMapAddress, 0x400) : new byte[0];
byte[] spriteOam, spriteTileSet;
if (renderSettings.SpritesEnabled) {
// If the background tiles are read from the sprite pattern table then we can reuse the bytes.
spriteTileSet = renderSettings.SpriteAndBackgroundTileSetShared ? tileSet : _tileRam.ReadBytes(0x0, 0x1000);
spriteOam = _spriteRam.ReadBytes(0x0, 0xa0);
}
else {
spriteOam = spriteTileSet = new byte[0];
}
var renderState = new RenderState(renderSettings, tileSet, backgroundTileMap, windowTileMap, spriteOam, spriteTileSet);
var renderStateChange = renderState.GetRenderStateChange(_lastRenderState);
if (renderStateChange == RenderStateChange.None) {
// No need to render the same frame twice.
_frameSkip = 0;
_framesRendered++;
return;
}
_lastRenderState = renderState;
_tileMapPointer = _tileMapPointer == null ? new TileMapPointer(renderState) : _tileMapPointer.Reset(renderState, renderStateChange);
var bitmapPalette = _gpuRegisters.LcdMonochromePaletteRegister.Pallette;
for (var y = 0; y < LcdHeight; y++) {
for (var x = 0; x < LcdWidth; x++) {
_lcdBuffer.SetPixel(x, y, (byte) bitmapPalette[_tileMapPointer.Pixel]);
if (x + 1 < LcdWidth) {
_tileMapPointer.NextColumn();
}
}
if (y + 1 < LcdHeight){
_tileMapPointer.NextRow();
}
}
_renderer.Paint(_lcdBuffer);
_frameSkip = 0;
_framesRendered++;
} ゲームボーイ フレームはサーバー側で構成され、圧縮されて WebSocket 経由でクライアントに送信されます。彼は背景とスプライトを機能させましたが、まだやるべきことが残っています.
Raw LCD は HTML5 キャンバスです:
<canvas #rawLcd [width]="lcdWidth" [height]="lcdHeight" class="d-none"></canvas>
<canvas #lcd
[style.max-width]="maxWidth + 'px'"
[style.max-height]="maxHeight + 'px'"
[style.min-width]="minWidth + 'px'"
[style.min-height]="minHeight + 'px'"
class="lcd"></canvas>
すべてが揃っているので、このプロジェクト全体が大好きです。 TypeScript、2D JavaScript Canvas、レトロゲーム、その他多数!
const raw: HTMLCanvasElement = this.rawLcdCanvas.nativeElement;
const rawContext: CanvasRenderingContext2D = raw.getContext("2d");
const img = rawContext.createImageData(this.lcdWidth, this.lcdHeight);
for (let y = 0; y < this.lcdHeight; y++) {
for (let x = 0; x < this.lcdWidth; x++) {
const index = y * this.lcdWidth + x;
const imgIndex = index * 4;
const colourIndex = this.service.frame[index];
if (colourIndex < 0 || colourIndex >= colours.length) {
throw new Error("Unknown colour: " + colourIndex);
}
const colour = colours[colourIndex];
img.data[imgIndex] = colour.red;
img.data[imgIndex + 1] = colour.green;
img.data[imgIndex + 2] = colour.blue;
img.data[imgIndex + 3] = 255;
}
}
rawContext.putImageData(img, 0, 0);
context.drawImage(raw, lcdX, lcdY, lcdW, lcdH); STAR and CLONE https://github.com/axle-h/Retro.Net にアクセスして、Docker で実行することをお勧めします。その後、Visual Studio Code と .NET Core を使用してローカルでコンパイルおよび実行できます。彼は、ゲームボーイのサウンドとデバッガーに関するヘルプを探しています。
スポンサー: サードパーティの .NET コードをデバッグするための最新の JetBrains Rider、Smart Step Into、デバッガーの改善、C# インタラクティブ、新しいプロジェクト ウィザード、および列のコードの書式設定を入手してください。