p98lib リファレンス

使い方に戻る アプリを開く

1. p98lib とは / IDE での始め方

p98lib は、PC-98(386以上)向けにゲームのようなグラフィック プログラムを書くための、最小限のCライブラリです。画面をグラフィックモードに 切り替える・矩形や絵を描く・キー入力を読む・ちらつかずに絵を動かす、といった 「よくやること」をひとまとめにしています。

このページは include/p98.h(ヘッダファイル)を読まなくても p98lib を使えるように、「何ができるか」から使い方のコードを見せる形で まとめたリファレンスです。関数の正式な一覧は最後の 「12. 関数の総覧」にあります。

IDE での使い方

  1. 左のエクスプローラーのサンプル一覧から、p98lib/ で始まる もの(hello.c / walk.c / walk2.c)を 開きます。
  2. 他のサンプルと同じように、エディタ下の実行ボタン(▷)を 押すだけでそのまま動きます。

自分で新しくプログラムを書いて p98lib を使いたいときは、ファイルの どこかに #include "p98.h" と書くだけで構いません (慣例として先頭に書きますが、行頭に空白があってもファイルの1行目でなくても 構いません)。それだけで、ビルドの内部処理が自動的に huge model(p98lib が 必要とするメモリモデル)とライブラリのリンクに切り替わります。特別な設定は要りません。

動く最小のプログラム

画面を初期化し、色5(緑+青)の矩形を1つ描いて表示し、しばらく待ってから 終了する、最小のプログラムです(サンプルの hello.c と同じ内容)。

#include "p98.h"

int main(void) {
    int i;

    p98_init();
    p98_clear(0);
    p98_fill_rect(100, 50, 200, 80, 5);
    p98_flip();

    for (i = 0; i < 60; i++) {
        p98_wait_vsync();
    }

    p98_quit();
    return 0;
}

ここで出てきた p98_init()・p98_clear()・ p98_fill_rect()・p98_flip()・ p98_wait_vsync()・p98_quit() の意味は、 次の章から順番に説明します。

2. 画面を出す・消す

関数説明
int p98_init(void); 画面を 640x400・16色 のグラフィックモードに切り替える。 描画・キー入力を行う p98_* 関数は p98_init() の 後に呼ぶこと。ただし p98_set_hide_text_on_init() だけは例外で、p98_init() より前に呼ぶ (次の行参照)。戻り値は成功時0(現状は常に成功する)。
void p98_quit(void); p98_init() で変更した状態(画面モード・INT 23hベクタ)を 元へ戻す。パレットは、呼び出し前の実際のパレットを読み出して 復元するのではなく、p98_init() と同じ既定値16色へ 書き戻す(詳しくは次の「3. 色とパレット」参照)。テキスト画面に ついては、カーソルの表示だけは必ず元(表示)に戻すが、 p98_init() で消したテキストの中身(文字コード面)は 元に戻さない。プログラムの最後に必ず呼ぶこと。 p98_init() を呼んでいない状態で呼んではいけない。
void p98_set_hide_text_on_init(int enable); p98_init() が起動時にテキスト画面を消す・カーソルを隠す 動作をするかどうかを設定する(既定は有効=消す)。 p98_init() より前に呼ぶこと。 テキストとグラフィックを重ねて使いたい場合に 0 を渡す。
p98_init() を呼ぶと、画面は 640x400 の16色モードになり、 パレットは次の章で説明する「既定パレット」から始まります。

3. 色とパレット

p98lib の描画関数(p98_clear() や p98_fill_rect() など)が受け取る「色」は、0〜15 の色番号です。これは RGBの値そのものではなく、4本のビット面のON/OFFの組み合わせを 表す番号です。

色番号の各ビットの意味は次のとおりです。

ビット意味
bit0(値1)青
bit1(値2)赤
bit2(値4)緑
bit3(値8)輝度(明るさ)

例えば 5(2進数で 0101)は bit0(青)+bit2(緑) が 立っているので「緑+青=シアン」になります。10(1010)は bit1(赤)+bit3(輝度) なので「輝度+赤=明るい赤」です。慣れないうちは、 欲しい色を「赤・緑・青のどれを混ぜるか」+「輝度を足すか」で考えると 組み立てやすいです。

16色の既定パレット

p98_init() を呼ぶと、パレットは次の16色から始まります (8番だけ規則から外れて中間グレーです)。

番号色見本意味の例
0rgb(0,0,0)黒
1rgb(0,0,119)青
2rgb(119,0,0)赤
3rgb(119,0,119)赤+青=マゼンタ
4rgb(0,119,0)緑
5rgb(0,119,119)緑+青=シアン
6rgb(119,119,0)赤+緑=黄
7rgb(119,119,119)赤+緑+青=灰色
8rgb(68,68,68)中間グレー(規則の外側の特別枠)
9rgb(0,0,255)明るい青
10rgb(255,0,0)明るい赤
11rgb(255,0,255)明るいマゼンタ
12rgb(0,255,0)明るい緑
13rgb(0,255,255)明るいシアン
14rgb(255,255,0)明るい黄
15rgb(255,255,255)白

パレットを変える

関数説明
void p98_set_palette(int index, int r, int g, int b); パレット番号 index(0〜15)が実際に表す色を、 r/g/b(それぞれ0〜15の 濃さ)に設定する。以後、その番号で塗った場所すべての見た目が変わる。
注意: p98_quit() は、p98_set_palette() で変更したパレットをプログラム開始前の実際の状態へ戻すわけではありません。 常に p98_init() と同じ既定値16色へ書き戻します。

4. 描く

関数説明
void p98_clear(int color); 描画ページ全体を color(0〜15)で塗りつぶす。
void p98_fill_rect(int x, int y, int w, int h, int color); 描画ページの矩形 (x, y, w, h) を color で塗る。 座標は左上原点、画面は640x400。
x/y が負だったり、矩形が画面の右端・下端を はみ出したりしても、画面内に収まる部分だけ自動でクリップして描かれるので、 呼び出し側で範囲チェックをしなくても安全です。完全に画面外の矩形は何も描きません。

5. 表示を切り替える(ダブルバッファ)

画面には「今表示しているページ」と「今描いているページ」の2枚があります。 もし画面を1枚しか持たず、そこへ直接絵を描いていくと、 描いている途中の絵がそのまま見えてしまい、ちらつきの原因に なります。p98lib では、見えない裏側のページに絵を描き終えてから 表と裏を入れ替えることで、これを防ぎます。

関数説明
void p98_wait_vsync(void); 次の垂直帰線(画面の描き直しの区切り)が始まるまで待つ。
void p98_flip(void); 表示ページと描画ページを入れ替える。以後の p98_clear()/p98_fill_rect() などは 新しい描画ページ(直前まで表示していなかった側)に書かれる。
unsigned long p98_frames(void); p98_init() 以降に p98_flip() を呼んだ回数。
注意: p98_flip() は「前のフレームで描いた内容」を 新しい描画ページへ引き継ぎません。2枚のページはそれぞれ別々の内容を持つだけです。 動く絵を描くときは、毎フレーム背景を描き直すか、動いた分だけ元に戻す処理を 自分で書く必要があります(後述の「8. 背景ページ+差分復帰」はこれを楽にする 方法です)。

毎フレームの定型は次のとおりです:描く → p98_wait_vsync() → p98_flip()。

#include "p98.h"

#define SC_ESC 0x00

int main(void) {
    int x = 0;
    int running = 1;

    p98_init();

    while (running) {
        p98_poll();
        if (p98_key_down(SC_ESC)) running = 0;

        p98_clear(0);
        p98_fill_rect(x, 180, 40, 40, 10);
        x = (x + 4) % 600;

        p98_wait_vsync();
        p98_flip();
    }

    p98_quit();
    return 0;
}

(p98_poll() と p98_key_down() は次の章で説明する キー入力の関数です。)

6. キー入力

関数説明
void p98_poll(void); このフレーム分の入力を取り込む。
int p98_key_down(int scancode); scancode を押している間ずっと真(1)。
int p98_key_pressed(int scancode); 前回の p98_poll() から今回までの間に、新たに 押された瞬間だけ真(1)。押しっぱなしにしても、真になるのは 最初の1回だけ。
int p98_key_getch(void); 文字入力を1文字取り出す(無ければ0。ブロックしない)。 SHIFT/CAPS/CTRL等の変換はBIOSまかせ。
つまずきどころ: p98_poll() を毎フレーム 呼ばないと、キー入力は一切取れません。 p98_key_down() / p98_key_pressed() は、直前に呼んだ p98_poll() の結果を見ているだけです。

p98_key_down(押されている間ずっと真)と p98_key_pressed(押した瞬間だけ真)は用途が違います。 例えば「押しっぱなしで歩き続ける」移動には p98_key_down を、 「押すたびに1回だけ色を変える」ような切り替えには p98_key_pressed を使います(samples/walk.c が この使い分けの実例です)。

scancode(実測済みの対応表)

キーscancode
UP0x3A
RIGHT0x3C
DOWN0x3D
LEFT0x3B
SPACE0x34
ESC0x00

上の6つは samples/walk.c で実際に使われ、動作確認済みの値です。 掲載した6つ以外のキーを使いたい場合は、PC-98のスキャンコード表 (WebNP2-wiki の Keyboard.md 等)を参照してください。

7. スプライト(絵を描く)

矩形の塗りつぶしだけでなく、あらかじめ用意したビットマップ(スプライト)を 画面へ描くこともできます。p98lib のスプライトは 4プレーン (青・赤・緑・輝度)+マスクという形式のデータを使います。

p98_sprite_t の中身

フィールド意味
int w;幅(ドット数、1以上)
int h;高さ(ドット数、1以上)
const unsigned char *planes[4]; [0]=青 [1]=赤 [2]=緑 [3]=輝度。それぞれのビットが立っている位置がその色面ONを表す。
const unsigned char *mask; 1のビットの位置だけ画面に描く(透明部分の指定)。

ビット順は MSBが左端のドット(p98_fill_rect と 同じ並び)で、1行は ceil(w/8) バイト、パディング無しで詰めて 並びます(1プレーン分の総バイト数は ceil(w/8)*h)。

マスクの考え方: mask のビットが1の位置だけ、 4プレーンの内容がそのまま画面へ書かれます。0の位置は4プレーンとも 背景をそのまま残し、何も上書きしません。つまり 「背景色をキーカラーにして透明を表す」方式ではなく、 マスク専用のビットマップで透明・不透明を指定する方式です。 色を変えたいときは、planes の中身をその場で書き換えるのではなく、 色ごとに別々の p98_sprite_t(同じ形のマスクを共有しつつ、 プレーンの組み合わせだけ変えたもの)を用意して切り替えます (samples/walk.c の SPR_WHITE/SPR_MAGENTA 等がその実例です)。
関数・型説明
void p98_draw_sprite(const p98_sprite_t *spr, int x, int y); スプライトの左上が (x, y) に来るように描く。xは 1ドット単位で自由。画面外へはみ出す部分は自動でクリップされる。 spr が NULL、または w<=0 || h<=0 なら何もしない。
p98_sprite_backend_t P98_SPRITE_CPU(既定。1バイト単位の読み書きで確実に 描く)と P98_SPRITE_EGC(EGCで一部を高速化。見た目の 結果はCPU版と常に一致する)の2種類。
void p98_set_sprite_backend(p98_sprite_backend_t backend);
p98_sprite_backend_t p98_get_sprite_backend(void);
以後の p98_draw_sprite() が使うバックエンドを切り替える/取得する。
void p98_draw_sprite_ex(const p98_sprite_t *spr, int x, int y, p98_sprite_backend_t backend); 現在の設定に関わらず、バックエンドを明示して描く。

8. 背景ページ+差分復帰(動くものを速くする方法)

「5. 表示を切り替える」で触れたとおり、p98_flip() 方式では、 動くキャラクタを描くたびに背景を自分で描き直す必要があります。背景が 軽ければ問題になりませんが、背景が重い(タイルを敷き詰めるなど)場合、 毎フレーム全部描き直すのは無駄です。背景ページ+差分復帰は、 背景を1回だけ裏の「背景ページ」に描いておき、動く部分だけ そこから復元してから新しい位置に描くことで、この無駄を減らす方法です。

つまずきどころ: この方式は p98_flip() による ダブルバッファリングとは併用できません(どちらも同じ ページ切り替えの仕組みを使うが、意味が違うため)。使うなら p98_init() の代わりに p98_init_bgpage() を使い、 以後は p98_flip() を呼ばないでください。
関数・型説明
int p98_init_bgpage(void); p98_init() と同じ初期化に加え、背景ページモードにする。
p98_render_mode_t p98_get_render_mode(void); 現在の描画モードを返す(P98_RENDER_FLIP が既定、 p98_init_bgpage() した場合のみ P98_RENDER_BGPAGE)。
void p98_set_draw_target(p98_draw_target_t target); 以後の p98_clear()/p98_fill_rect()/ p98_draw_sprite() の描画先を、P98_TARGET_SCREEN (画面ページ、既定)と P98_TARGET_BACKGROUND(背景ページ) で切り替える。P98_RENDER_BGPAGEモードでのみ意味を持つ。
void p98_draw_sprite_diff(const p98_sprite_t *spr, int x, int y); 前回この関数が描いた矩形を背景ページから画面ページへ復元してから、 spr を (x, y) へ描く。最初の呼び出しでは 復元をしない。P98_RENDER_FLIPモードでは p98_draw_sprite() と同じ動作(フォールバック)になる。
void p98_copy_bgpage_to_screen(void); 背景ページの内容を画面ページへ丸ごとコピーする。背景を作り直した ときに画面へ反映したい場合に使う。P98_RENDER_BGPAGEモード でないときは何もしない。

典型的な使い方は「1. 背景ページへ一度だけ背景を描く → 2. 画面ページへ p98_copy_bgpage_to_screen() でコピー → 3. 以後は毎フレーム p98_draw_sprite_diff() でキャラクタだけを描く」という流れです (samples/walk2.c がこの流れの実例です)。

同時に動かせるのは1体分(直前の1矩形)だけである点に注意してください。

9. VRAM常駐スプライト(さらに速くする方法)

スプライトをあらかじめVRAMの余りへ「常駐」させておき、EGC(グラフィック用の 専用回路)に本来の転送機能を使わせることで、CPUでの1バイトずつの読み書きより 速く描く経路です。

つまずきどころ: 置き場は P98_VRAM_STORE_SIZE (768バイト/プレーンしかありません)。使い切ると p98_vram_upload() は失敗(負の値を返す)ので、 戻り値を必ず確認して、失敗したらCPU経路(p98_draw_sprite() 系)へ落とす書き方にしてください(samples/walk2.c の draw_character() がこの書き方の実例です)。
定数・型説明
#define P98_VRAM_STORE_OFF 32000
#define P98_VRAM_STORE_SIZE 768
VRAM常駐置き場の先頭オフセットとサイズ(1プレーンあたり768バイト)。
p98_vram_sprite_t p98_vram_upload() の結果を保持する構造体 (幅・高さ・置き場の位置など。フィールドは内部実装向けで、 呼び出し側は中身を直接いじらず関数経由で使う)。
関数説明
void p98_vram_reset(void); VRAM置き場のバンプ割り当てを先頭へ戻す。個別解放はできないため、 起動時やシーン切り替え時にまとめてリセットする。
int p98_vram_upload(const p98_sprite_t *spr, p98_vram_sprite_t *out); スプライトをVRAMの置き場へアップロードする。戻り値: 0=成功、 -1=幅が16の倍数でない、-2=置き場の容量不足。
int p98_vram_reupload(p98_vram_sprite_t *vs, const p98_sprite_t *spr); 既にアップロード済みの領域へ、同じ幅・高さ・不透明設定の別の絵を 上書きアップロードする(アニメのコマ替え用。一致しなければ-1)。
int p98_vram_free_bytes(void); 置き場の残りバイト数(1プレーンあたり)。
void p98_draw_sprite_vram(const p98_vram_sprite_t *vs, int x, int y); vs をEGC経由で (x, y) へ描く。横方向に 画面端をはみ出す場合はCPU経路へ自動フォールバックする。
void p98_draw_sprite_vram_diff(const p98_vram_sprite_t *vs, int x, int y); p98_draw_sprite_diff() のVRAM版。作りは同じで描画部分だけEGC経由になる。

10. つまずきやすいところ(まとめ)

11. サンプルとの対応

IDE のサンプル一覧にある p98lib/hello.c / walk.c / walk2.c は、この順に難易度が 上がっていきます。まず hello.c を読み、次に walk.c、最後に walk2.c という順番がおすすめです。

サンプル使っている主な機能対応する章
p98lib/hello.c p98_init/p98_clear/p98_fill_rect/ p98_flip/p98_wait_vsync/p98_quit 1・2・3・4・5
p98lib/walk.c 上記に加えて p98_poll/p98_key_down/ p98_key_pressed、p98_draw_sprite (毎フレーム背景を描き直す方式) 5・6・7
p98lib/walk2.c p98_init_bgpage/p98_set_draw_target/ p98_draw_sprite_diff/p98_copy_bgpage_to_screen、 p98_vram_upload/p98_vram_reupload/ p98_draw_sprite_vram/p98_draw_sprite_vram_diff 8・9

12. 関数の総覧

include/p98.h にある公開API(関数・型・定数)の一覧です。 辞書的に引きたいときに使ってください。

画面の初期化・終了

API説明節
int p98_init(void);640x400・16色モードを開始する2
void p98_quit(void);状態を元に戻す2
void p98_set_hide_text_on_init(int enable);初期化時にテキスト画面を消すかどうか2

表示・描画

API説明節
void p98_wait_vsync(void);次の垂直帰線まで待つ5
void p98_flip(void);表示ページと描画ページを入れ替える5
unsigned long p98_frames(void);p98_flip()を呼んだ回数5
void p98_clear(int color);描画ページ全体を塗る4
void p98_fill_rect(int x, int y, int w, int h, int color);矩形を塗る(自動クリップ)4
void p98_set_palette(int index, int r, int g, int b);パレット番号の色を変える3

キーボード

API説明節
void p98_poll(void);このフレーム分の入力を取り込む6
int p98_key_down(int scancode);押している間ずっと真6
int p98_key_pressed(int scancode);押した瞬間だけ真6
int p98_key_getch(void);文字入力を1文字取り出す6

スプライト(CPU合成/EGC)

API説明節
p98_sprite_t(型)4プレーン+マスクのスプライトデータ7
void p98_draw_sprite(const p98_sprite_t *spr, int x, int y);スプライトを描く7
p98_sprite_backend_t(型。P98_SPRITE_CPU/P98_SPRITE_EGC)描画バックエンドの種類7
void p98_set_sprite_backend(p98_sprite_backend_t backend);既定のバックエンドを切り替える7
p98_sprite_backend_t p98_get_sprite_backend(void);現在のバックエンドを取得する7
void p98_draw_sprite_ex(const p98_sprite_t *spr, int x, int y, p98_sprite_backend_t backend);バックエンドを明示して描く7

背景ページ+差分復帰

API説明節
p98_render_mode_t(型。P98_RENDER_FLIP/P98_RENDER_BGPAGE)現在の描画モードの種類8
int p98_init_bgpage(void);背景ページ+差分復帰モードで初期化する8
p98_render_mode_t p98_get_render_mode(void);現在の描画モードを取得する8
p98_draw_target_t(型。P98_TARGET_SCREEN/P98_TARGET_BACKGROUND)描画先の種類8
void p98_set_draw_target(p98_draw_target_t target);描画先を切り替える8
void p98_draw_sprite_diff(const p98_sprite_t *spr, int x, int y);前回の矩形を復元してから描く8
void p98_copy_bgpage_to_screen(void);背景ページを画面ページへ丸ごとコピーする8

VRAM常駐スプライト(EGC転送)

API説明節
#define P98_VRAM_STORE_OFF 32000VRAM常駐置き場の先頭オフセット9
#define P98_VRAM_STORE_SIZE 768VRAM常駐置き場のサイズ(バイト/プレーン)9
p98_vram_sprite_t(型)VRAMへアップロード済みのスプライトを表す9
void p98_vram_reset(void);置き場の割り当てを先頭へ戻す9
int p98_vram_upload(const p98_sprite_t *spr, p98_vram_sprite_t *out);VRAMへアップロードする9
int p98_vram_reupload(p98_vram_sprite_t *vs, const p98_sprite_t *spr);既存領域へ別の絵を上書きアップロードする9
int p98_vram_free_bytes(void);置き場の残りバイト数9
void p98_draw_sprite_vram(const p98_vram_sprite_t *vs, int x, int y);EGC経由で描く9
void p98_draw_sprite_vram_diff(const p98_vram_sprite_t *vs, int x, int y);p98_draw_sprite_diff()のVRAM版9