【Python】ソーサリアンのようなタイトル表示を実現してみる
久しぶりの更新です。
昔、主にPC8801mk2SRなどの8ビットパソコン用に、「ソーサリアン」というRPGが発売されていました。
このゲームは、当時としてはとてもスケールの大きいものだったのですが、そのタイトル画面がシンプルなのに壮大で美しく、ゲームのイメージにピッタリのものでした。
今回、このソーサリアンのようなタイトル表示をやってみましたので、解説します。
言語はPython、モジュールはpyxelを使用します。
なお、実際に動いているようすは以下になります。
自作ゲーム開発。
— あぶり6800 (@aburi6800) 2021年6月12日
ジワるタイトル処理、何だか違和感あったので真面目にロジック組み直してみたヨ!
これでもういいだろう!(早く中身の製作進めれ)#python #pyxel #gamedev pic.twitter.com/AaIcdU7q6C
厳密に言うと、PC88版というよりは、メガドライブ版に近いのですが、まあ雰囲気が良ければヨシ、ということで…
事前準備
事前に以下を準備します。
- 元絵
- バッファ(元絵と同じサイズ、このバッファの内容を画面に表示する)
- 青色、緑色、赤色の各色要素のカラーリスト(後述)
処理説明
全体の処理としては、以下のようになります。
- 明度インデックスをゼロに初期化
- 以下4回ループ
(1) ループ回数によって処理開始位置を以下で設定する。
1ループ目:X,Y
2ループ目:X+1,Y+1
3ループ目:X+1,Y
4ループ目:X,Y+1
(2) オフセットX座標をゼロに初期化
(3) 以下、Y座標を1ドットおきに処理する。
a. 以下、X座標を8ドットおきに処理する。
(a) 元絵の処理対象座標+オフセットX座標のドットの色を取得
(b) 取得した色が青要素のカラーリストに含まれていれば、
青要素のカラーリストの明度インデックスの色でバッファにドットを描画
(c) 取得した色が緑要素のカラーリストに含まれていれば、
緑要素のカラーリストの明度インデックスの色でバッファにドットを描画
(d) 取得した色が赤要素のカラーリストに含まれていれば、
赤要素のカラーリストの明度インデックスの色でバッファにドットを描画
b. バッファの内容を画面に描画
(4) オフセットX座標を+2
(5) オフセットX座標が8以下なら(3).に戻る - 明度インデックスを+1
- 明度インデックスが5未満なら2.に戻る
このうち、(a)〜(d)が描画色に関わる処理で、だんだん明るい色に描画していくための部分(描画色設定)になります。
その他の処理は、1ドットおきにメッシュのパターンで描画する部分(バッファ描画)になります。
どちらか一方でもフェードインの効果は出せますが、今回はこの2つを組み合わせて、じわ〜っとフェードインしてくる効果を出しています。
以降、処理をバッファ描画と描画色設定に分けて解説します。
バッファ描画について
まずはバッファ描画のアルゴリズムから解説します。
イメージ的には、全体を2x2ドットで仕切り、それぞれの左上、右下、右上、左下の順にドットを埋めていく処理になっています。
ただし、全体を一度に描画していくと味気なかったため、横8ドット単位で同時に描画して、少しづつ右に描いていくようにしています。
最初は(X,Y)から横8ドット置き、縦2ドット置きに描画する
オフセットX座標を+2して、同様に描画を繰り返す
横8ドット分の処理が終わった状態
次に(X+1,Y+1)を起点に、同様に横2ドットおきに描画していく
横8ドット分の処理が終わった状態
次に(X+1,Y)を起点に、同様に横2ドットおきに描画していく
横8ドット分の処理が終わった状態
最後に(X,Y+1)を起点に、同様に横2ドットおきに描画していく
横8ドット分の処理が終わった状態
これで全部埋まりました。
ここまでの一連の処理を、この後説明する明度インデックスの回数分繰り返していきます。
なお、実際の実装では、毎フレーム処理すると速すぎるため、1フレームおきに実行しています。
描画色設定について
続いて描画色の設定についてです。
上記のバッファ描画でドットを描画する色について、段階的に明るくしていく処理になります。
これは、全体のループ回数から、予めリストに定義しておいた描画色を決定しています。
ループ回数が最初のほうが暗い色で青系に揃えておいて、明るくなるにつれて各色を発色させていくような定義にしています。
ここからはpyxelを前提にした話になりますが、カラーパレットは標準で以下のようになっています。
pyxelのカラーパレットは16色なので、MSXでも同様の処理を作ることができると思います。 また、PC8801mk2SRなど8色でも、アナログパレットで同様な処理を実現可能かと思 います。
このカラーパレットを元に、暗い色から明るい色への変化をカラーリストとして定義していきます。
また、カラーリストは一度に全色分定義すると処理が複雑になるので、赤要素、緑要素、青要素に分けて定義します。
赤要素のカラーリスト:
緑要素のカラーリスト:
青要素のカラーリスト:
赤要素のカラーリストに8(赤色)などが含まれていませんが、これは実際に使用した画像では未使用だったためです。
汎用的に使える処理にするには、赤要素のカラーリストを(1, 2, 8, 9, 10) などにしたり、要素数を増やす必要があると思います。
先のバッファ描画の中でドットを描画する際には、以下の処理を行います。
- 各色要素のカラーリストに、元絵から取得したドット色が含まれているかを調べる
- 含まれている場合はその色要素のカラーリストから、現在のカラーインデックス(ループ数により0~4となる)のカラーコードで描画する
その結果、最初は全てカラーコード1で描画され、次は全てカラーコード5、その次から徐々に各色の色が付くような描画となります。
ソース(抜粋)
これまで説明したアルゴリズムの実装コードを掲載します。
実際にゲームで使っているもので、あまり良いコードではないですが、以下が毎フレーム実行されています。
# タイトルロゴの色パターン # 各要素は赤系、緑系、青系の順で定義している TITLE_COLOR = ( (pyxel.COLOR_NAVY, pyxel.COLOR_DARKBLUE, pyxel.COLOR_ORANGE, pyxel.COLOR_YELLOW, pyxel.COLOR_YELLOW), (pyxel.COLOR_NAVY, pyxel.COLOR_DARKBLUE, pyxel.COLOR_GREEN, pyxel.COLOR_LIME, pyxel.COLOR_WHITE), (pyxel.COLOR_NAVY, pyxel.COLOR_DARKBLUE, pyxel.COLOR_CYAN, pyxel.COLOR_LIGHTBLUE, pyxel.COLOR_WHITE), ) # タイトルロゴのフェードイン処理用 # タイトルロゴの座標、大きさ TITLE_X = 0 TITLE_Y = 0 TITLE_W = 152 TITLE_H = 40 # バッファの書き出し座標 TITLE_BUFF_OFFSET_X = 0 TITLE_BUFF_OFFSET_Y = 208 # 処理中の色パターンインデックス title_color_idx = 0 # 元画像の取得元ピクセル座標 title_get_x = 0 title_get_y = 0 # タイリング表示用のループカウント、0~3で1ループ title_get_loop_cnt = 0 (中略) def update_title_fadein(self): ''' タイトルロゴフェードイン ''' if pyxel.frame_count % 2 == 0: # タイトルロゴのピクセルを走査し、バッファに描き込む for _y in range(self.TITLE_Y + self.title_get_y, self.TITLE_H + 1, 2): for _x in range(self.TITLE_X + self.title_get_x, self.TITLE_W + 7, 8): # イメージバンク0の指定した座標のピクセルの色を取得する _pick_color = pyxel.image(0).get(_x, _y) # 取得した色が現在のからーグループ以降に含まれているかを調べる # 青系 if self.TITLE_COLOR[2][self.title_color_idx:].count(_pick_color): pyxel.image(0).set(self.TITLE_BUFF_OFFSET_X + _x, self.TITLE_BUFF_OFFSET_Y + _y, self.TITLE_COLOR[2][self.title_color_idx]) # 緑系 elif self.TITLE_COLOR[1][self.title_color_idx:].count(_pick_color): pyxel.image(0).set(self.TITLE_BUFF_OFFSET_X + _x, self.TITLE_BUFF_OFFSET_Y + _y, self.TITLE_COLOR[1][self.title_color_idx]) # 赤系 elif self.TITLE_COLOR[0][self.title_color_idx:].count(_pick_color): pyxel.image(0).set(self.TITLE_BUFF_OFFSET_X + _x, self.TITLE_BUFF_OFFSET_Y + _y, self.TITLE_COLOR[0][self.title_color_idx]) # 横2ドット移動 self.title_get_x += 2 # 8ドット分処理したか if self.title_get_x > 8: # 8ドット分処理したら、次の処理に向けて準備する self.title_get_loop_cnt += 1 # ループカウント1の場合:x+1ドット、y+1ドット目から処理する if self.title_get_loop_cnt == 1: self.title_get_x = 1 self.title_get_y = 1 # ループカウント2の場合:x+1ドット、y+0ドット目から処理する if self.title_get_loop_cnt == 2: self.title_get_x = 1 self.title_get_y = 0 # ループカウント3の場合:x+0ドット、y+1ドット目から処理する if self.title_get_loop_cnt == 3: self.title_get_x = 0 self.title_get_y = 1 # ループカウント4の場合:ループカウントをリセット、次のカラーグループでx+0ドット、y+0ドット目から処理する if self.title_get_loop_cnt == 4: self.title_get_loop_cnt = 0 self.title_get_x = 0 self.title_get_y = 0 self.title_color_idx += 1 if self.title_color_idx >= len(self.TITLE_COLOR[0]): self.state = self.STATE_TITLE
さいごに
解説は以上となります、参考になれば幸いです。
説明が少々わかりにくい部分(特に描画色設定のあたり)があると思いますので、折を見て図などを追加できればと考えています。