ここでは、C/C++言語でのゲーム製作に関するあれこれを書いていきたいと思います。

CコンパイラMPW(ただで入手しよう!) 00/11/10
C言語の学習法(自分で調べろ!) 00/11/10
フォント設定(Osaka−等幅にしてくれ!) 00/11/10
C++に挑戦する(オブジェクト指向がどーした) 00/11/11
ゲームライブラリ製作中(ygs2k互換だよ) 00/11/29
高速転送ルーチンを作る(256色専用) 00/12/09
ソースファイルを分割せよ(原因不明なバグを抹殺する) 00/12/10
見切り発車してみる(pugoGL2001αリリース!) 01/2/4
ゲームの高速化(これが基本!) 01/2/5
世界一シンプルなMPWの使い方講座(書き方がな!) 01/2/17
覚えるべきCの文法(これで最低限なりよ!) 01/2/18
QuickTimeで高速化!?(デマに踊らされるな!) 01/3/11
最速。それは、夢。(目指せ人間オプティマイザ) 01/3/14
PixMap転送ルーチン(自前で作るこれ基本) 01/3/24
pugoGL開発中止か?(pugoGLα2リリース!) 01/4/3
おひさです(pugoGLα3リリース!) 02/8/13


CコンパイラMPW(ただで入手しよう!) 00/11/10

MPWはAppleが開発しているC/C++用のコンパイラです。Pascalやアセンブラも使えるようです。入手方法や使用法については▼彊良さんのホームページに詳しい説明があります。20MB以上もダウンロードしなければならないのが問題ですが、CodeWarriorを買うことを考えれば我慢できる電話代でしょう。

どうせただだから機能が少ないんじゃないか?とか、使いにくいんじゃないの?と思うかも知れません。確かに、MPWをダウンロードしたけど使ってない人もいます。"Hello,world!"と表示するプログラムを作るだけでもわけの分からない事がたくさんあるでしょう。しかし、慣れればこんな事は簡単です。本当に難しい部分はプログラムを作る事であり、その作業はCodeWarriorで作る時と同じです。

もうひとつの懸念事項である機能に関してはCarbon化が可能なのでMacOS Xに対応したアプリケーションも作れますし、コンパイラは当然PowerPCネイティヴだし、作成したアプリケーションの動作速度も十分な速度が出ています。CodeWarrior用のプログラムも少し手直しすれば動かす事ができます。有名な3DゲームのMarathonはMPWで作られています。

というわけで、MPWは無償だし、使えるコンパイラなのでダウンロードしてみてください。英語版のソフトですが英語力はほとんど要りません。真面目にReadMeファイルを読まなくても使えます。(僕は読んでません)MPWのコマンドがありますが覚える必要はありません。ほとんどメニューを選ぶだけで使えます。

何か分からなかったら、とりあえず調べる事です。検索エンジン(例えば▼google▼infoseek)を使ってMPWに関するホームページを全て調べてみればいいでしょう。どうせ数は少ないのです。

他にどんな開発用ソフトがあるかというと、すでに挙げたCodeWarriorや、HyperCard、REALbasic、Director、SuperCard、FutureBasic・・・結構ありますが、プログラム経験のない初心者ならHyperCard、HyperCardじゃ遅いけどCは難しくて手が出ない人にはREALbasic、REALbasicではコンピュータの裏側をいじれないからイヤ!って人はCodeWarriorでしょうか。これらは体験版やLite版が雑誌についていたりダウンロードできるので興味のある方は探してみて下さい。

Directorは機能のわりに使いやすいんだろうけど高額なのが問題。SuperCardはHyperCardをカラー化したようなもので、HyperCardがカラーになればいいと思っている人向け。FutureBasicは結構高速なゲームも作れそうですが、REALbasicより難しそうなので、いっその事C言語でやったほうがいいと思います。

CodeWarriorではJavaも扱えます。Javaは難しいわりにかなり遅いのでJavaで仕事を探したい人や(まだまだ需要が増えます)どうしてもWindowsやLinuxでも動かせるようにしたい人以外にはお勧めしません。

他にもAppleScriptやPerlがありますが、これらは使用目的が狭いので使うべき人はもう使っているでしょう。

う〜ん、Macで扱える開発ソフトや言語って結構あったんですね・・・。


C言語の学習法(自分で調べろ!) 00/11/10

僕はMPWやC/C++を使いこなせるわけではないので、C言語の解説はしません。C言語について知りたい方は検索エンジンで検索するなり参考書を買うなりしましょう。

僕は学校でC言語の初歩を習いましたが、C言語でどうやって絵を出すのだろう?と疑問に思っていました。C言語の参考書には文字を書く命令はありますが絵を描く命令はありません。実際の所、絵を描いたり音を出したりウィンドウを作ったりするのはOSによって異なります。MacではToolBoxと呼ばれている命令群を使います。

C言語やToolBoxについて調べるには▼K仲川のホームページ▼Mac素人プログラマの会が有用です。しかし、Web上の情報をかき集めても情報が足りません。僕は結局本を買いました。漢字Talk7時代の本ですがMacintoshアプリケーションプログラミング上巻、下巻(新居雅行著、ディー・アート、1995年刊、各3689円税抜き)の評判がなかなか良かったのでとりあえず上巻のみ買いました。

個人的には他の人が書いたプログラムを見る事が上達への近道だと思います。今井卓治さんのホームページが閉鎖してしまったので残念です(01/2/17:▼HKTさんのページでENDUREのソースが公開されてます。絶対入手するべし。)が、まきがめで有名な▼高橋健さんがもうすぐソースコードを公開するようですのでそちらに期待します。(01/2/17:もう公開されてます)ソースコードを公開している方があまりいらっしゃらないのが残念です。HyperCardスタックだとプログラム見放題ですからね。でも、ある人が公開したフリーウェアのコードが他の人のシェアウェアに化けたりとか、あるらしいですね・・・。


フォント設定(Osaka−等幅にしてくれ!) 00/11/10

MPWでは標準ではプログラムをMonacoの9ポイントで表示します。これでは日本語で書かれたコメントが読めません。EditメニューのFormat...で毎回変えていたのですが、変える事ができるはずだと思い、MPWの初期設定ファイルを探してみました。すると、MPWフォルダ内のStartUpファイルの中に次のような箇所を発見。

#	{Font} - Default Font for new windows.
			Set Font MPW
			Export Font

#	{FontSize} - Default font size for new windows.
			Set FontSize 9
			Export FontSize

これを次のように書き換えてみました。

#	{Font} - Default Font for new windows.
			Set Font 'Osaka−等幅'
			Export Font

#	{FontSize} - Default font size for new windows.
			Set FontSize 12
			Export FontSize

'Osaka−等幅'の部分は文字化けしますが、気にせずMPWを再起動し、Cのファイルを読込ませると見事に日本語が表示されました。'Osaka−等幅'の”−”は全角である事に注意して下さい。フォントはOsaka−等幅以外でもいいのですが、等幅フォントを指定しましょう。


C++に挑戦する(オブジェクト指向がどーした) 00/11/11

夏休みにC++入門書を読んだり、最近Effective C++(C++の効果的な使い方)を読んだりしているので、C++でいきなりプログラム組んでも問題ないかな〜と思っていたのですが、、、プログラムの書きはじめから全然分かりません。

C言語ならまずmain関数から実行されます。C++なら何から実行されるかというと、やはりmain関数です。こんな事に気付くのに30分くらいかかった・・・。C++だからといって混乱してしまった・・・。クラス化にしても、さっぱり分からず。いろいろ資料を調べてむりやりプログラムを作ってエラー連発。インスタンスって何なの!?とか、コンストラクタは何をするのだ!?(初期化をするのだ)といった感じでもう、初めてAppleScriptを使ってすぐやめた頃を思い出すなぁ。

混乱しつつも、何とかアプリケーションを作る事に成功。出来上がったものは、キーボードを押すと終了するアプリケーションです。え?何か機能はないのかって?起動すると、メニューバーに何も表示されず、ウィンドウも開きません。何かキーを押すと終了します。それだけ。これをベースにいろいろ組み立てる土台のプログラムです。

C++で組んだのでオブジェクト指向してるかといえば、そんなことはなく、ただ難解になっただけの可能性が高いです。下にC++で作ったプログラムのmain関数のみを示しますが、

int	main(){
CMain incm;		//単なるインスタンス
	incm.InitToolBox();		//ToolBoxの初期化
	while(!g_bEndEventLoop){incm.EventLoop();}		//イベントループ
	incm.Quit();		//終了処理
}

これをC言語で書けば

int	main(){
	InitToolBox();		//ToolBoxの初期化
	while(!g_bEndEventLoop){EventLoop();}		//イベントループ
	Quit();		//終了処理
}

インスタンスなんてものがなくなってスッキリしました。C++だとなんだか無駄に行数が増えて長ったらしいプログラムになるような・・・。世の中にはプログラムを書いた行数が給料に直結するという不思議な会社もあるそうですからそのような所にいるプログラマはC++を使うと給料が増えそうです。(^^;

まぁ、こんな小さなプログラムじゃC++のありがたみは分からないか。大規模プログラムになってこそ真の力を発揮するんでしょうね。う〜ん、それじゃじわじわとこのプログラムを大規模にしますかね〜。数万行のプログラムを書かないと就職も出来ないしね。就職の面接で、これまで書いたCのプログラムで一番長いものは?と聞かれ、1000行くらいかなぁと答えたら、見事に落とされました。1000行じゃ少なすぎるわなぁ。K-0 Fighters2000なんてHyperTalkで20000行もあるのに・・・。コピー&ペーストしまくったからだけどね。


ゲームライブラリ製作中(ygs2k互換だよ) 00/11/29

ゲームのプログラム中で(x,y)の位置にキャラクタを描画しようとする場合、いきなりCopyBitsなどのToolBox関数(MacOSで用意されている命令群)をゲームのメインプログラムの中に入れたりする事はまずありません。そんな事をするとプログラムはメチャクチャになってしまいます。たいてい、自分で作った描画専用の関数を呼び出し、その関数の中でToolBox関数を使用します。ToolBoxをつつむクレラップのようなものです。ToolBox関数はゲームだけでなくワープロやユーティリティなど様々なプログラムで使えるよういろいろな条件を指定できます。しかし、ToolBox関数を使うたびごとにたくさんの指定をするのは時間の無駄です。自作のゲームで使う条件は毎回同じなのです。自分で作った関数を呼び出せば条件の指定はその関数中ですればいいので1回だけですみますし、改良したい時やバグがあった時の修正も楽です。

とはいえ、その関数を上手く作るのもめんどくさいのです。こういんもんは偉〜い方が作られた関数セットを利用すればラクチンです。現在、MacのC言語用ゲームライブラリとしてM2GEやN Game Libraryなどがあります。これらを使えば高速なゲームが簡単にできます。でも、両方ともシェアウェアなのでお金のない僕は使いません。特に、N Game Libraryの使用条件として同一目的のライブラリを配付してはならないらしく、そのためMacのゲーム界に損害を与えているとも考えられ個人的に嫌いなので使いたくありません。M2GEは68k版はフリーウェアにしたり開発環境がなくてもスクリプト実行機能を使ってゲームを作れるようにするなど非常に良心的ですが、配付形態がコンパイル済コードのため自由度が少なそうです。

それなら、僕がフリーウェアでソースコードも公開!のゲームライブラリを作ってみようじゃないかと思ったのが3週間前の事。そこで気付いたのですが、WindowsにはyaneuraoGameScript2000(略してygs2k)というゲーム開発環境があり、それ用のプログラムが簡単に移植できたらおもしろいです。ygs2kを製作したやねうらお氏のホームページは▼ここです。ygs2k互換にしたのは、関数の名前と仕様を考えるのがめんどくさかったからという理由もありますが・・・。

現在、僕の開発しているライブラリ(pugoGL2001とでもしようかな)では機能が少なくスピードに不満があるものの、ygs2k用のゲーム2つが動いています。ygs2kではDirectXを使用して高速化をはかっているのに対し、pugoGL2001ではAppleの開発したDirectXに似ているGameSprocketsを使っていないので遅いのだと思い、DrawSprocketに対応させる事も考えています。ただし、その場合MacOS XではGameSprocketsがないのが問題です。

DrawSprocketの前に、マウス入力とファイル入出力を作らなあかんけど。


高速転送ルーチンを作る(256色専用) 00/12/09

アップルが赤字に転落したとか新聞に載ってましたけど、そんな事くらいでいちいち騒ぐのもおかしな事ですな。アップルが黒字になった時に驚けよ。いいかげんiMacじゃ引っ張れ無くなってきているしジョブスもそろそろやめさせられる頃かなぁ。そしたらアップルの買収騒ぎも起こって、リストラもして、、、はぁ。いつもの事だな。

前回、DrawSprocketに対応させるかもなどと書いたのですが、MacOS Xでのサポートを考えると無謀ですし、DrawSprocketそのものがあまり速くないようです。WindowsのDirectXでは内部バッファをVRAM上に置く事でかなりの高速化をしているのですが、DrawSprocketではオプションでVRAMに置くように設定してもPowerMac7xxx系で、しかもVRAMを増設していないと効果がないようです。ほとんどのMacではVRAMに内部バッファが置かれない・・・そんな馬鹿な。(2001/3/11:今思えばバッファは置かれるけどフリップできないということが書かれていたような気がする。)

転送ルーチンを自前で作るのも勉強になるし、まぁいいか。実際の所かなり苦労して半泣きになりながら作り上げたのですが。

256色転送ルーチンを作って高速になったのは良いのですが、その結果pugoGLが256色専用になってしまいました。G3だと32000色でもk高速に動作するはずなのですが、僕の環境でチェックしようがないですし256色専用でもゲームは作れます。HyperCardの白黒画面に比べれば圧倒的な表現力があると考えましょう。

ところで、開発中に訳の分からないエラーが出ました。MPWでコンパイルすると、
File ":MrCFE:Source:Compiler:blockopt.c"; line 216 # Internal error compiling Init
blockopt.cなんてファイルは作ってません。MPWが勝手に作ったファイルでエラーが起きたとか言われてもどうしようもないっちゅうねん!どうやってデバッグしよう・・・?


ソースファイルを分割せよ(原因不明なバグを抹殺する) 00/12/10

前回の最後に書いた謎のバグ、考えていても分からないので4400行もあったプログラムを複数のファイル(その数25個・・・やりすぎか?)に分割して原因がどの部分にあるのか調べようと思いました。その結果・・・コンパイル成功!いや、ちょっと待て、バグの原因は一体どこやってん?まさか、プログラムの行数が多すぎたら出るバグなのか!?いや、たぶん分割する時にプログラムをちょこっといじったのでその辺りでしょう。よく分からんけど。

ファイルの分割はC++なのでクラスごとに行いました。機能ごとにクラスを分けているのでどのファイルに何を書いているか分かりやすいです。以前Cでファイルを分けた時はどこに何を書いたか分からず探すのに時間がかかりました。C++の効果が出てきましたね。

※クラスとは:関数と変数をまとめて扱う仕組みです。いや、実際にはもっといろいろ出来るんですが、無理に継承というものを使おうとして訳が分からなくなったのでやめました。継承を使うべきではないところで使うと余計ややこしくなるのであった。


見切り発車してみる(pugoGL2001αリリース!) 01/2/4

彊良さんのページを見てMPWの新たなテクニックを発見しました。コマンドラインから命令するときにOptionキー押しながらEnterキーを押せばダイアログが出てきて、いろいろ設定できます。

最近はC++を全然いじってないんですよ。意外にも、Windowsに移った今もHyperCardがまだメインの開発環境なのです。新しいMacを買ったほうが良かったのか!?

とりあえず、LUTIA2001a2[2001/7/3Update](50KB)pugoGL2001(90KB)をダウンロードしてください。

感想、バグレポート待ってます。


ゲームの高速化(これが基本!) 01/2/5

せっかく公開したpugoGLですが、これからバージョンアップする気がないんですよ。ま〜別にこれからこんなの使ってゲーム作ろうという奴はダメでしょう。MacOS Xに対応しているものを使わなきゃ。じゃあ、pugoGLはただのゴミなのか・・・。いや、多少これについて語らせてくれ!

ちゅうわけで、今回はpugoGLのグラフィック部分のルーチンについて解説します。使い方ではありません。どうやって高速化しているかです。もう、僕は高速化しか頭にない単細胞野郎なんやろうか?

何枚かの画像ファイル(リソースでも同じ)があって、これらを順次入れ替えて高速にアニメーション表示したい場合、まず全ての表示したい画像ファイルをメモリ上のオフスクリーンバッファ(いわゆるGWorldです)に読み込みます。そして、そのバッファから裏画面に描画します。裏画面とはオフスクリーンバッファの一種ですが、描画するウィンドウと色深度、縦横のサイズ、カラーテーブルが同じようにしておきます。裏画面に描画したら、今度はその裏画面をウィンドウに描画します。以下、バッファ→裏画面→ウィンドウの繰り返しです。

ここで、全てのオフスクリーンバッファを同一の色深度にしておくと、CopyBits等での転送が速くなります。しかし、それでも低速マシンではきついです。実はオフスクリーンバッファ間の転送ではCopyBitsよりも自前で転送ルーチンを書いたほうが速いのです。なぜか裏画面→ウィンドウ間の転送ではCopyBitsのほうが速いんですけどね。最近の機種ではどうか知りませんが、少なくとも新しい機種なら何も考えなくても速いので古い機種で高速化する事を考えるべきでしょう。

そして、問題の自前転送ルーチンは・・・。解説するのが面倒なのでソースコードを調べましょう。pugoGLのpugoPlane.cppにあります。キーワードは、256色専用、unsigned longを使った32バイト転送、シフト命令を用いて除算命令を省く、どこぞのページに載っていた高速マスク付き転送ルーチン、4バイトにアラインするためにシフト、ループ内での処理を極力ループ外へ持っていく、ビット演算も重要です。

まだ高速化できるぞ!という情報がありましたらぜひ教えて下さいね。それから、ソースはパクって構いません。できればパクった報告お願いします。パクったぜ!と。


世界一シンプルなMPWの使い方講座(書き方がな!) 01/2/17

僕が迷えるMPWユーザーを増やしてしまったのではないかと責任を感じ、ちょっと解説などしてみようかなと思いました。MPW-GM版をダウンロードした人は読みましょう。

1.MPW Shellを起動する
2.FileメニューからNew...を選び、test.cという名前のファイルを作る
3.次のプログラムを打ち込む。全て半角で!

int main(){
    printf("Hello\n");
    return 0;
}
4.BuildメニューからCreate Build Commands...を選び、SIOWとPowerPCに(68Kユーザなら68Kに)チェックをつける。Program Nameも入れる。そして、Source Files...ボタンを押して、test.cをAddする。これでCreateMakeボタンを押す。
5.BuildメニューのBuild...を選ぶ。入っているプログラム名がさっきのProgram Nameである事を確認してOKボタンを押す。すると、ウィンドウがWorkSheetに切り替わり、コンパイルが始まる。
6.コンパイルが終わるとFinderにアプリケーションが出来上がっているので、ダブルクリックする。
7.ウィンドウにHelloと表示されているはず。

これがアプリケーション製作の流れです。
C言語の入門書に入っているサンプル類はこの方法でコンパイルします。

別に必要無い所をいじっても壊れないので、いろいろやってみましょう。MPWを使うのは難しくないです。小学生でも使えるでしょ?


覚えるべきCの文法(これで最低限なりよ!) 01/2/18

前回でMPWを使ったコンパイル方法は理解できたはずなので、次はプログラムの作り方を解説します。ただし、これまでにHyperCardでちょっと凝ったゲームを作ったことがあるレベルの人が対象です。

それでは、次のHyperTalkスクリプトをCのプログラムで表してみましょう。同じウィンドウを二つ開いて、対比させるとわかりやすいかと思います。

on openCard
  --変数に値を代入する
  put 1 into a
  --条件分岐
  if a=1 then
    put 2 into a
  end if
  --指定回の繰り返し
  repeat with i=1 to 10
    if i=5 then exit repeat
  end repeat
  --条件が満たされるまで繰り返し
  repeat while a>=0
    put a-1 into a
  end repeat
  --関数の呼び出し
  put func(3) into a
  --コマンドの呼び出し
  func2 a
end openCard

function func b
  return 2*b
end func

on func2 b
  put 2*b into c
end func2

画面表示など一切なしで変数を扱うことしかやっていませんが、そのへんをCで書くとややこしくなるから書いてません。上のスクリプトをCプログラムに移植すると、次のようになります。

//関数のプロトタイプ宣言
int func(int b);
void func2(int b);

int main(){
  //変数の宣言
  int a,i;
  //変数に値を代入する
  a = 1;
  //条件分岐
  if (a==1){
    a = 2;
  }
  //指定回の繰り返し
  for (i=0; i<10; i++){
    if (i==4) break;
  }
  //条件が満たされるまで繰り返し
  while (a>=0){
    a = a-1;
  }
  //関数の呼び出し
  a = func(3);
  //コマンドの呼び出し
  func2(a);

  return 0;
}

int func(int b){
  return 2*b;
}

void func2(int b){
  int c;
  c = 2*b;
}

多少余計な要素が増えてますが、まぁ、大して変わらんと思います。順番に説明していきましょう。

関数のプロトタイプ宣言:関数を使う場合ははじめのほうに、こんなもんが必要ということ。詳しい説明は省略。

int main() :メイン関数と呼ばれている。C言語のプログラムのスタートポイント。HyperTalkで対応するものがないのでopenCardにした。

{......} :Cではこの括弧の中身がひとまとまりで扱われる。HyperTalkではon〜end、repeat〜end repeatなどに相当する。

変数の宣言 nt a,i; :intってのは整数を表す。この文で整数型のaとiという名前の変数を作っている。HyperTalkではすべて文字列型と考えることができたけど、C言語の変数では整数型、浮動少数型とかいろいろあるので宣言が必要なんよ。

セミコロン ;←これ :HyperTalkでは改行までがひとつの文やけど、Cではセミコロンまでがひとつの文。付け忘れてエラーを出した経験が100回くらいあるかも。

変数に値を代入 a = 1; :見てのとおり、「=」を使えば右辺の値を左辺に代入できる。HyperTalkと代入するほうとされるほうの順番が逆なので注意。

条件分岐 if (a==1) :「==」ってなんやねん!?いや、これが「等しい」という意味の演算子なのよ。初心者のうちは「=」にしてバグが多発するので要注意やね。

指定回の繰り返し for (i=0; i<10; i++) :HyperTalkのrepeat with文とほぼ同じ。ただ、C言語では何事も0から始めるのが自然なので0から始めてます。

繰り返しからの抜け出し break; :exit repeatとおなじやね。

条件が満たされるまで繰り返し while (a>=0) :repeat whileと同じ。

関数の呼び出し a = func(3); :HyperTalkの関数呼び出しを知っていれば何も難しくはないと思う。

コマンドの呼び出し func2(a); :実はC言語では関数とコマンドの違いはない。別にa = func(3);と書かず、func(3);でも動く。

関数の定義 int func(int b) :整数型の返り値を持つfuncという名の関数で、その引数は整数型のbである。まー、そんなとこ。

返り値 return 2*b; :HyperTalkと変わらん。

ここまで理解できましたか?理解できなければ無視してください。(無視でええんか?)ここらへんで演算子の表でも作りましょうかね。

<算術演算子>
負符号:-1 -a
 乗算:3*a
 除算:a/2
 余り:a%4
足し算:a+1
引き算:a-1

<比較演算子>
  大なり:a>3
   以上:a>=3
  小なり:a<3
   以下:a<=3
  等しい:a==3
等しくない:a!=3

<論理演算子> NOT:! AND:&& OR:||

論理演算子の使い方について説明します。やはり、HyperTalkとCのプログラムを比べます。

if a is not 1 then
  --処理
end if
repeat while a>=1 and b<=2
  --処理
end repeat
repeat until a>0
  --処理
end repeat
if(a!=1){
  //処理
}
while( (a>=1) && (a<=2) ){
  //処理
}
while( !(a<0) ){
  //処理
}

だいたいこんなもんで普通のゲームのアルゴリズムは組めるでしょう。次はデータを入れる配列を調べましょう。

HyperTalkで凝ったゲームを作るにはchar,item,lineを使うことが重要です。Cではこれらの代わりに配列を使うことになります。HyperTalkとCで大きく異なるのはCでは配列に入れるもののサイズと配列の長さが固定だということです。

例えば、int型変数を10個並べた配列を使うプログラムは次のようになります。

int main(){
  int i;
  int a[10];

  for(i=0; i<10; i++){
    a[i]=0;
  }

  a[0] = a[1]+a[2];
  if (a[3]==a[4]){
    a[5] = a[5]+1;
  }
}

int a[10];のところが宣言です。これで10個のint型のデータが入る入れ物ができたわけです。詳しく言うと、a[0],a[1],a[2],a[3],a[4],a[5],a[6],a[7],a[8],a[9]の10個です。a[10]はありませんね。0から始まっているのだから当然です。

んー、これでC言語の初歩の半分は分かったといっても過言ではないでしょう。(これでまだ、初歩の半分なんか?)この位の知識を持っていればpugoGLを使ってゲームを作れそうな気もします。


QuickTimeで高速化!?(デマに踊らされるな!) 01/3/11

高速化ネタを探してWebを徘徊していると、何やら面白そうな事が、、、。▼Appleの流氷通信に 「QuickTimeで行う高速で柔軟な描画」とあり、内容はCopyBitsでオフスクリーンバッファを画面に転送するよりQuickTimeのDecompressImageのほうが速いと書いてあります。プログラムを見ると、簡単です。これくらい楽勝で実装できます。

で、さっそくpugoGLに組み込んで試してみました。結果は、、、かなり遅くなりました。これまで20FPS出ていたものが5FPSしか出ません。なんでやねん。・・・そういやQuickTimeって32000色に最適化されてるって話なので、オフスクリーン、画面の色数ともに32000色のプログラムを作って実行、、、CopyBitsは10FPS、QuickTimeは4FPS。あかんやん。

というわけで、何の意味もなかったのですが、FutureBasicのメールマガジンのログにはQuickTimeのアクセラレーションが効く機種なら高速化するようだと書いてあります。逆に、CopyBitsのアクセラレーションが効く機種もあるみたいです。だから、どっちが高速かをゲーム起動時に調べて、高速なほうを選ぶようにしたほうがいいという事です。でも、いいや。めんどいので画面転送はCopyBits様に任せます。

話は変わって、PentiumではFPUを使ってデータ転送すれば速いらしいです。整数ではlong型の32ビットまで計算できるわけですが、浮動小数ではdouble型の64ビットの計算ができるので、データバスが64ビット幅あれば64ビットで転送できるだろうってな原理です。というわけで実装。・・・体感スピード変わらず。ボツか?しかし、FPSをカウントするとわずかに速くなっている様だ。しかし、こんな方法使って大丈夫なんやろか?(01/3/14:どうやら速くなっていない様なので、この方法はボツにしました。)(01/3/20:僕のMacが古いから効かなかっただけのような気がする。測定プログラムを作って試してもらう必要があります。)


最速。それは、夢。(目指せ人間オプティマイザ) 01/3/14

最近またpugoGLをいじってます。高速化をしようと思いつく限りの方法を試した結果、高速化しませんでした。現状でも十分なスピードが出ているのですが、フルスクリーンで毎秒60フレームのゲームは厳しいです。ちゅうか、毎秒20フレームしか出ません。まぁ、僕のマシンが遅いからなのですが、、、。しかし、ソフマップで買取価格500円はないんとちゃうか?500円て、吉野家の牛丼と同じレベルかいな、、、。

それはさておき、高速化が無理なら機能を増やそうと考えて、VRAM直接転送ルーチンを書き、スキップライン表示を実現しました。きじばとさんのゲームで画面拡大表示して暗いモードにした状態といえばわかりやすいでしょうか?(長くてわかりにくい気がする)これで擬似フルスクリーン表示が可能になりました。これで、320×240の画面で毎秒60フレームが、、、実現しませんでした。40フレーム。240×240の画面なら何とかなりそうですね。小さいですが。

これに気を良くして、今度は拡大縮小ルーチンと回転ルーチンを書きました。これで2Dゲームに必要な機能は一通り揃いました。この二つのルーチンはもうちょっと最適化しないといけないのですが、G3マシンをターゲットにすれば相当無茶なことをしない限りスピードは十分なはずです。しかし、いくら最速を追及してもマシンの性能が上がればどれも同じに見えちゃうんですよね。はぁ、、、。


PixMap転送ルーチン(自前で作るこれ基本) 01/3/24

<ゲームの高速化>

C/C++で作っているゲームを高速化したいなら、どうすれば良いのでしょう?闇雲にプログラムを手直ししても、速度はほとんど変わりません。一番処理時間を食っている部分さえ改善すれば、全体的な処理速度を上げることができます。ゲームにおいては、グラフィックとサウンドで全処理の90%くらいを占めているといわれます。ここではグラフィックの高速化について解説します。

Macでゲームを作る場合、GWorldのお世話になります。GWorldを使えばメモリ上に仮想的なスクリーンを作れます。この仮想スクリーンに対してグラフィックを描き、それをウィンドウに転送してチラツキの無い画面処理を実現します。しかし、仮想スクリーンに対してグラフィックを描くときに使う命令「CopyBits]がどうも遅いのです。最近の機種ではどうか知りませんが、僕のMacでは遅いので何とかする必要があります。

高速化するにしても、方法はいろいろあります。DrawSprocketを使うなり、シェアウェア/フリーウェアのゲームライブラリを使うなりすればお手軽です。しかし、それではここで解説する必要がありません。独自のプログラムを組んで高速化に挑戦してみましょう。それに、独自のプログラムなら機能が無くて困ることがありません。自分で作れば良いのですから。・・・まぁ、作れるかどうかは別にして。

================================Break Time/================================

なぜ、仮想スクリーンに対してCopyBitsで転送すると遅いのでしょうか?ウィンドウに対して転送するときにはかなりのスピードが出ています。これはグラフィックチップによるアクセラレーションが働いていると思われます。CopyBitsは画面描画によく使われる命令なのでアクセラレーションがあれば体感速度が大きく向上します。それでは、なぜ仮想スクリーンに対してはアクセラレーションが効かないのでしょうか?グラフィックチップはVRAMへのアクセスは高速ですが、メインメモリに対するアクセスは低速です。だから、仮想スクリーンに対する描画は遅いのでしょう。あるいは、仮想スクリーンに対してはCPUが描画処理をしているのかもしれません。

================================/Break Time================================

<PixMapの中身>

メモリ上の(GWorldの)画像データはPixMapポインタの示す位置にあります。PixMapには次のようにデータが入っています。

 0,0,1,1,0,0,1,1,*,*,*,*,0,1,1,0,0,1,1,0,*,*,*,*,1,1,0,0,1,1,0,0,*,*,*,*,1,0,0,1,1,0,0,1,*,*,*,*

256カラー限定で解説するので、コンマでバイト単位に区切ってあると考えて下さい。0は白色、1は黒色、*はゴミデータです。左上の画素データから順番に並んでいます。このデータは8×4のサイズのデータですね。

 0,0,1,1,0,0,1,1,*,*,*,*
 0,1,1,0,0,1,1,0,*,*,*,*
 1,1,0,0,1,1,0,0,*,*,*,*
 1,0,0,1,1,0,0,1,*,*,*,*

PixMapの開始点のアドレスと1ライン当りのバイト数は必ず4の倍数になっているはずです。ゴミデータは4の倍数にあわせるためのものだと思います。(にしては、横幅を4の倍数にあわせるようにしていてもゴミデータがあるけど)

<画素データの参照>

この並び方から考えて、左上の点がのアドレスと1ライン当りのバイト数さえ分かればPixMapを自由に扱えそうです。その求め方は...

  int myLineSize;
  myLineSize = (0x3fff & (*myGWorld->portPixMap)->rowBytes);

  unsigned char *myPixMap;
  myPixMap = (unsigned char*) (*myGWorld->portPixMap)->baseAddr;

0x3fffってのが妙な気もしますが、0xと頭につくと16進数です。0x3fffは2進数で表すと0011 1111 1111 1111になるので、これとAND演算する事によって上位2ビットをクリアしている事になります。(unsigned char*)型にしているのは、符合ありの型の時は右シフト演算の時に最上位ビットが1だと「これはマイナスを意味する1だからそのまま残しておくにゃ」などと偉い偉いコンパイラ君が必要のない1を残して表示がおかしくなるので、その対策です。

画素データを参照する場合、例えば、一番右上の画素データはmyPixMap[myLineSize-1]で求められるわけです。いや、求められないよ、そこはゴミデータだ。ピクチャの横幅を求めるには、、、

  Rect myRect;
  myRect = myGWorld->portRect;

これでmyRect.rightが横幅な訳です。一番右上の画素データはmyPixMapess[myRect.right-1]でいいわけです。それでは、(x,y)=(2,3)の位置の画素データはmyPixMapess[3*myLineSize+2]になります。ん?どうしてmyRect.rightでは-1が必要で(x,y)=(2,3)では-1がいらないかって?それは(x,y)=(0,0)の位置が基準だから、(x,y)=(2,3)というのは横に3番目、縦に4番目の位置の画素だからなのです。

<PixMapの転送>

それでは、本題のPixMapの転送について。転送元の大きさは転送先の大きさよりも小さい事を前提にします。src:source(転送元)、dst:destination(転送先)の意味です。PixMap全体を転送するプログラムは次のようになります。

  int x,y;
  for(y=0; y<srcRect.bottom; y++){
    for(x=0; x<srcRect.right; x++){
      dstPixMap[y*dstLineSize+x] = srcPixMap[y*srcLineSize+x];
    }
  }

簡単ですね。for文を2重にして全ての画素をスキャンしているだけです。必ずxのループを内側にしましょう。PixMapのデータの並び方を考えれば、当然横方向にスキャンしていくべきです。縦方向にスキャンするとキャッシュや最適化の関係で遅くなる可能性があります。

このプログラムでは1回ループを回るたびに2回の乗算が必要です。乗算は加算や減算に比べて格段に遅いのでこいつをループ外に持っていきましょう。なに?コンパイラが最適化してくれるぅ?この例なら最適化してくれそうだけど、複雑になると最適化してくれないので簡単な所から練習をしておくべきなのです。

この例では毎回掛け算をしなくても、1ループごとにアドレスを1足せばいいのです。そして、yのループ1回ごとにアドレスをLineSizeだけ足せば、掛け算無しのプログラムになります。

  int x,y;
  for(y=0; y<srcRect.bottom; y++){
    for(x=0; x<srcRect.right; x++){
      dstPixMap[x] = srcPixMap[x];
    }
    srcPixMap += srcLineSize;
    dstPixMap += dstLineSize;
  }
 

ほらどうだ!これは速そうです。CopyBitsより断然速いはず!と思って調べてみると速くなかったりします。な、なぜ・・・?

================================Break Time/================================

乗算は遅いと書きましたが、一体どれくらい遅いのでしょうか?PowerPCはRISC型プロセッサですから、基本的に1命令は1クロックで実行できます。加算減算、論理演算、シフト演算などの演算系や、1次キャッシュ→レジスタ間の転送は1クロックで終わると考えています。(調べていませんが。)しかし、乗算は1クロックでは終わりません。乗算は複数の命令に分解されて実行されているんだと思います。僕の感覚では10命令(=10クロック)程度ではないかと思います。除算はさらに遅くて、20クロックくらい掛かると思われます。余りを求めるのも同程度でしょう。(誰か、詳しい人教えてくれんかなぁ。)

他に遅くなる原因は、キャッシュされていないメモリの参照や転送、関数呼び出しがあります。ループ内で乗算や除算を使ったり、関数を呼び出したりすることは避けたいものです。

Pentiumでは分岐予測失敗時のコストが高いのでif文を使うのは避けたほうがいいようです。しかし、PowerPCではどうかは知りません。・・・知らない事ばっかりだなぁ。最適化の参考にしているページのほとんどがWindows系だもんな・・・。

================================/Break Time================================

<4バイトまとめて>

高速化に関するページに行くと、4バイト同時に転送するだとか、FPUを使って8バイト同時に転送するだとか書かれています。そこで、このプログラムを見直してみると・・・unsined char*って書いてあります。char型は1バイト。そうです、1バイト単位で転送しています!とすると、4バイト単位で転送すれば4倍の転送速度になりそうです。(注:その他諸々のオーバーヘッドがあるので4倍にはなりません)

  //1ライン当りのバイト数
  int srcLineSize,dstLineSize;
  srcLineSize = (0x3fff & (*srcGWorld->portPixMap)->rowBytes);
  dstLineSize = (0x3fff & (*dstGWorld->portPixMap)->rowBytes);

  //PixMap開始点のアドレス
  unsigned long *srcPixMap,*dstPixMap;
  srcPixMap = (unsigned long*) (*srcGWorld->portPixMap)->baseAddr;
  dstPixMap = (unsigned long*) (*dstGWorld->portPixMap)->baseAddr;

  //ピクチャの矩形サイズ
  Rect srcRect;
  srcRect = srcGWorld->portRect;

  //転送ルーチン
  int x,y;
  for(y=0; y<srcRect.bottom; y++){
    for(x=0; x<srcRect.right/4; x++){
      dstPixMap[x] = srcPixMap[x];
    }
    srcPixMap += srcLineSize/4;
    dstPixMap += dstLineSize/4;
  }

4で割っている部分は最適化時に右シフトになるので(a/4 == a>>2)遅くはありません。これで一応4バイト単位で転送するのですが、開始点はPixMapの性質上必ず4で割り切れる事になっているので問題ありませんが、右端が4で割りきれない時、すなわちピクチャのサイズが4の倍数でない時にはsrcRect.right/4で端数が切り捨てられるので転送されない部分がでてきます。

<マスク演算を使おう>

こんな時にはマスク付き転送を使います。CopyMaskじゃありません。ちょっとした論理演算テクニックです。AND,OR演算を使います。1ビットの場合のAND演算はこうなります。

 0&0→0
 1&0→0
 0&1→0
 1&1→1
8ビットでやってみましょう。
 01010101&00001111→00000101
 11001100&11110000→11000000
&の左側を転送元の画像、&の右側をマスクと考えると、マスクが1の部分だけ残る訳です。

しかし、ここではマスクの部分を残すのではなく、マスクの部分だけを転送したいのです。マスクの部分だけを転送する有名な公式があります。

 dst = (src&mask)|(dst&~mask);

縦棒(OR)の左側はさっきやりましたね。右側は何なのでしょう?「~」はNOT演算なので、マスクを反転しています。それと転送先をマスク演算して・・・。分かりやすいように表してみましょう。

 ([ABCD]&0011)|([abcd]&~0011)

[ABCD][abcd]の各アルファベットは2進数で、これで4ビットの2進数を表しています。これを計算すると、

 =([ABCD]&0011)|([abcd]&1100)
 =[00CD]|([abcd]&1100)
 =[00CD]|[ab00]
 =[abCD]

なるほど、マスクビットが1の部分には転送元のデータが、0の部分には転送先のデータが入る訳です。それを転送先に代入すればマスクが1の部分だけ転送される訳です。いやぁ、素晴らしい。(注:pugoGLではXOR演算を駆使して高速化を図っています)

それで結局転送ルーチンがどうなるかといえば、こんなややこしい事になります。

  //転送ルーチン
  int x,y;
  for(y=0; y<srcRect.bottom; y++){
    for(x=0; x<srcRect.right/4; x++){
      dstPixMap[x] = srcPixMap[x];
    }
    switch(srcRect.right%4){
      case 0:
        break;
      case 1:
        dstPixMap[x] = (srcPixMap[x]&0xff000000)|(dstPixMap[x]&~0xff000000);
        break;
      case 2:
        dstPixMap[x] = (srcPixMap[x]&0xffff0000)|(dstPixMap[x]&~0xffff0000);
        break;
      case 3:
        dstPixMap[x] = (srcPixMap[x]&0xffffff00)|(dstPixMap[x]&~0xffffff00);
        break;
    }
    srcPixMap += srcLineSize/4;
    dstPixMap += dstLineSize/4;
  }

8ビット単位ですから、(0xff==255)マスクが長くなってしまって見にくいです。もう少しスマートに書けば、こうなります。

  //転送ルーチン
  int x,y;
  for(y=0; y<srcRect.bottom; y++){
    for(x=0; x<srcRect.right/4; x++){
      dstPixMap[x] = srcPixMap[x];
    }
    switch(srcRect.right%4){
      case 0:
        mask = 0x00000000;
        break;
      case 1:
        mask = 0xff000000;
        break;
      case 2:
        mask = 0xffff0000;
        break;
      case 3:
        mask = 0xffffff00;
        break;
    }
    dstPixMap[x] = (srcPixMap[x]&mask)|(dstPixMap[x]&~mask);
    srcPixMap += srcLineSize/4;
    dstPixMap += dstLineSize/4;
  }

================================Break Time/================================

G3など存在しない、Macが遅かった頃には高速シューティングゲームを作るには自前でPixMapを転送する必要がありました。しかし現在ではMacも高速になったため、たいていの2Dゲームは自前の転送ルーチンを作っていません。だから、僕がここで書いている事も無意味なのでしょうか?確かに、僕が書いているプログラムは役に立たないかも知れませんが、これはPixMapを高速転送する「基本」のプログラムなのです。これを発展させて様々な画像処理ルーチンを自前で書けるようになります。

拡大縮小、回転、モーフィング、半透明、エフェクト・・・あらゆる機能があり最高の速度が出せるライブラリができる事は無いでしょう。もちろん、現在でも十分な機能があって速度的にも問題の無いライブラリはあります。CopyBitsで十分な場合もありますし、OpenGLならかなり強力です。しかし、やはりどんなことでも完璧に出来ることはありません。足りないものは自分で作るしかありません。また、自分で作るとプログラムの勉強にもなります。それに、画像処理プログラムって面白いです。結果が見た目で分かりますからね。

================================/Break Time================================

<クリッピング>

今のプログラムでは転送先の位置が左上に固定されているのでこれを自由に移動させるようにしなければなりません。しかし、そのためにはクリッピング処理、srcとdstのアラインメント調整が必要です。

アラインメント調整は難関なので、まずはクリッピング処理について解説します。転送元と転送先の矩形サイズは同じで、矩形の位置はどこでもOKな場合、転送先のPixMap領域をはみだして転送すると、バグります。srcRect:転送元矩形、dstRect:転送先矩形、clipRect:クリップ矩形とします。

  //クリッピング
  if(dstRect.left < 0){ //左
    srcRect.left += -dstRect.left;
    dstRect.left = 0;
  }
  if(dstRect.right > clipRect.right){ //右
    srcRect.right -= dstRect.right - clipRect.right;
    dstRect.right = clipRect.right;
  }
  if(dstRect.top < 0){ //上
    srcRect.top += -dstRect.top;
    dstRect.top = 0;
  }
  if(dstRect.bottom > clipRect.bottom){ //下
    srcRect.bottom -= dstRect.bottom - clipRect.bottom;
    dstRect.bottom = clipRect.bottom;
  }

転送先矩形がクリップ矩形をはみだしていれば、クリップ矩形に合わせる訳です。その際に、転送先矩形のサイズの変化分と同じだけ転送元のサイズを変化させればクリップ処理の後でも転送元と転送先の矩形サイズは同じになります。こうすると後の処理が楽なんですね。

<データがずれているときには>

CPUとメモリの間のデータ転送はバスによって行われます。それはどんなバスかと聞かれれば、40人乗りのワンマンバスが入っていると答えておくと総理大臣になれるかも知れません。はてさて、このバスの幅は32bitです。最近のは64bit幅になっているような気もしますが、よく知りません。

32bit幅のバスなら一度に32bitのデータを転送できます。だからlong型を使えば高速に転送できる訳です。しかし、(x,y)=(0,0)の位置のデータを(x',y')=(1,0)の位置に転送するには一筋縄ではいきません。

 転送元データ:   |0,0,1,0|0,0,2,1|0,1,3,1|0,1,3,0|
                ↓
 転送先データ: |0,0,0,1|0,0,0,2|1,0,1,3|1,0,1,3|

このように1バイト単位でのずれが生じてしまうからです。これを解決するにはシフト演算でデータをずらすしかありません。

 unsigned long data;
 data = 0x12345678;
 data = data >> 8;

16進数ですので、0x12345678の8桁で4バイトを表します。このプログラムを実行するとdataには0x00123456が入ります。0x78はどこかへ消えてしまい、上位バイトには0x00が入る訳です。

 unsigned long in1,in2,out;
 in1 = 0x11223344;
 in2 = 0x55667788;
 out = (in1 << 8) | (in2 >> 24);

このプログラムでoutには0x22334455が入ります。これでずらす事に成功しました。これを実際のルーチンに適用したいところなのですが、pugoGLのルーチンとほぼ同じになるのでそちらを御覧下さい。

<透過付き転送>

最も単純な透過付き転送は、if文で透過すべきデータかどうか判定するものです。0を透過色とすると、こうなります。

  //1バイト単位の透過転送ルーチン
  int x,y;
  for(y=0; y<srcRect.bottom; y++){
    for(x=0; x<srcRect.right; x++){
      if(srcPixMap[y*srcLineSize+x] != 0){
        dstPixMap[y*dstLineSize+x] = srcPixMap[y*srcLineSize+x];
      }
    }
  }

しかし、毎回if文で判定すると1画素ずつしか転送出来ないため遅くなるので事前にマスクを作っておき転送時に論理演算する方法がよく用いられます。

  //マスク付き転送ルーチン
  int x,y;
  for(y=0; y<srcRect.bottom; y++){
    for(x=0; x<srcRect.right/4; x++){
      dstPixMap[y*dstLineSize+x] = (srcPixMap[y*srcLineSize+x]&maskPixMap[y*srcLineSize+x]) | (dstPixMap[y*srcLineSize+x]&~maskPixMap[y*srcLineSize+x]);
    }
  }

<終わりに>

PixMap高速転送ルーチンの解説はこれで終わりです。これだけの説明では分かりにくい所もあるでしょうが、説明を書くのも面倒なのです。決して、ここに書いた事が全てだとか、究極だとか思わないで下さい。さらなる上級テクニックは存在します。Web上をくまなく探せばさらなる高速化&機能アップへのヒントが見つかるでしょう。

なお、プログラムのインデントをタブではなく、全角スペースで表現しているので、コピーして使うとエラーが発生します。また、掲載したプログラムはエラーチェックしていません。ちゃんと自分でプログラムを打ちましょう。

この文章は凍る雨さんにメールで送ったものに加筆修正したものです。▼凍る雨さんのページにはゲーム製作に役立つサンプルがいくつかあるので、参考にするのも良いと思います。


pugoGL開発中止か?(pugoGLα2リリース!) 01/4/3

拡大縮小や回転機能、VRAM直接転送ルーチンを搭載し、バグも少しつぶしたpugoGLアルファ2を公開します。

とりあえず、p-shoot(60KB)pugoGLα2(90KB)をダウンロードしてください。

しかし、もう少し見やすいコードにできれば良かったな。今更一から作りなおす気はないけど。


おひさです(pugoGLα3リリース!) 01/4/3

最新版のpugoGLアルファ3を公開します。(05/03/23ちょっと修正してアルファ4にしました)

pugoGLα4(130KB)をダウンロードしてください。

makeファイルをつけたので、ファイル名だけ置き換えれば楽にmakeできると思います。

pugoGLベータ1.1(335KB)もあるでよ。


トップへ