ゲーム用プログラムやアプリで利用される乱数はゲーム内の遊びの面白さに直接関係する。 この乱数をどのように作成するかを考える。 sdccにはlong型があるので普通に標準関数のrand()関数を使うことは可能。ただし8bitのCPMやDOS用のC言語環境 などでは32bitのlong型を利用できないことがあるかもしれない。 ここで問題が発生する。なぜなら一般的なCの実装の乱数は32bit長の整数で計算するためlong型がないと計算できないため。 この場合にはsdccのC標準ライブラリではなく独自の乱数生成コードを使うことになる。 以下に比較的よく知られていて他でも紹介されているような有名な乱数ルーチンを示す。 *Rレジスタ参照法 色々な所で紹介されているこの手法はZ80CPUの特徴であるDRAMリフレッシュカウンタを参照して乱数とする方法。 リフレッシュカウンタとはハード上のDRAMメモリ内容を一定間隔で保持するために必要なメモリアクセスでCPUに DRAM(リフレッシュ)コントローラを内蔵するZ80固有の機能である。読み取るタイミングでリフレッシュカウンタが不定のため このランダムな性質を利用する。 計算式で求めているわけではないのでタイミングによっては物理乱数に近くなる。値は256で一巡するがRレジスタを得る 方法はアセンブラで書く必要があるためCインターフェースを作る必要がある。 アセンブラでは単に任意のタイミングでRレジスタを参照するだけである。 LD A,R ;Rレジスタの値を乱数としてAレジスタに得る この命令をソースコードの任意の場所で実行して戻り値としてAレジスタを参照するだけでよい。 次に簡単に利用するために関数を用意してインラインでアセンブラを埋め込みCから利用する方法を考えてみる。 sdccではcharの戻り値はHLのLレジスタを使うらしく、ここではunsigned charで戻り値を得るためLレジスタに 値を入れて終わるだけでよいだろう(警告が表示されるがコンパイルは通る) unsigned char rand_r(){ __asm LD A,R LD L,A __endasm; } このようなインラインアセンブラをCのソース中に埋め込むことで簡易的な乱数生成が可能となる。 *(N*5)+1法 名前については良くわからない。この方法は種となる値=Nに対して,(N*5)+1により計算で求めるものである。 線形合同法の一種に分類されるらしく、この方法は古くはナムコのゲーム機にも採用されて居た方法らしい。 この計算方法はアセンブラ以外にもCで実装が可能である。 乱数としては得られた数の下位桁のみを参照すると比較的良い特性となる。ここでは16bitの長さで計算し下位8bitの範囲で 乱数を得ている。 unsigned int seed=234; unsigned int rd=0; void rinit(){ rd=seed; } int rnd_fplus(){ rd=(rd*5)+1; return(rd&0xff); } *xorshift この方法は一般によく知られている方法であるxorshiftを8bit用に修正したもの。簡易的な実装と高速で比較的良い品質 を持つのでjavascriptなど色々な分野で利用されている。オリジナルは32bit長の計算を行が、ここでは8bit向けなので コンパクトに256個の乱数を得るよう修正した。8bitに小さくするとその周期的特徴がよくわかるようになるが乱数としては 周期が短く実用的ではない。(N*5)+1法よりも劣るので利用する場合は工夫が必要。 また8bit範囲で数値を求める場合は変数シフトするため16bit長が必要となる。16bitサイズの乱数を得る場合には24/32bitサイズで計算する必要がある。 unsigned int rl=234; char rnd_xorshift(){ rl=rl^(rl<<3); rl=rl^(rl>>4); rl=rl^(rl<<1); rl=rl&0xff; return(rl); } *クロサワ式 某所で資料が示されているMC68Kのメガドライブ方面などで利用されていたらしき計算方法。 元のコードは16bitの値の乱数を得るものだが、8bit用に変更した。アセンブラで書く必要があるのでCとの インターフェースは必要。 ;クロサワ式の8bit実装で乱数を得る ; ;-----init ; ld ix,8000h ;インデクスのポインタ先を変数領域とする ; ;-----処理開始 ; START: ld (ix+0),123 ;変数へ初期値(2byte)の設定 ld (ix+1),2 ;------------------------------------- ; Mainルーチン ; Dレジスタをサブルーチンの引数で使用 ; main: ld d,(ix+0) ;一回で1byte処理 call subrand ;サブルーチン呼び出し ld (ix+0),d ret ;loop: ; jp loop ;実行停止またはret命令 ; ;------------------------------------ ;Aレジスタをサブルーチン内で使用。8bitを超える場合は値を丸める ;BCレジスタをテンポラリとしてサブルーチン内で使用 ;Dレジスタを受け取りパラメータとして利用 ; subrand: push af push bc ld a,d ;変数ロード ld b,96h ;定数xor用 xor b ld c,66h ;引き算 sub c rlca ;ローテート(<< 2回) rlca ld d,a ;変数ストア pop bc pop af ret ;------------------------------------ end *DRAM参照法 この方法は計算によって乱数を求めるのではなく、ROMに書かれたデータを数値として読み取るという方法である。 有名なパックマンなどのゲーム機で採用されていたといわれる方式である。元のコードはアセンブラで書かれているが、 Cでポインタを用いたコードを書く事もできる。一例として紹介する。 ここでは参照元のROMをBASICやBIOSなどが記録されているファームウエアとした。 数値の呼び出しが毎回ランダムとなるようにチェック処理をおこなっている。 unsigned int *addr; unsigned char rnd; unsigned char rnd_old; addr=(unsigned int)0x1000; int rnd_refmem(){ addr++; rnd=*addr; /* 毎回得られる数が同じとならないようにチェック */ while (rnd==rnd_old){ addr++; rnd=*addr; } rnd_old=rnd; return(rnd); } *M系列 1bit単位で処理するハードウエア実装が可能な方式。ただし得られる数には周期性があり初期値の設定によっては 乱数とはならないので癖がある。利用時には注意が必要である。