フェーズ 5 宇宙船を画面に出す

step0:星空を飛ぶ宇宙船

トップページstep0 概要 / フェーズ 5

ゴール

フェーズ 4 で描いた宇宙船を、実際に画面へ表示します。 やることは 2 つだけです。

今度は見た目が変わります フェーズ 4 は「何も変わらないことが正解」でしたが、今回は違います。 流れる星の中央に、自分で描いた宇宙船が浮かびます。 step0 でいちばん達成感のあるフェーズです。

1. 考え方

1-1. 「読み込む」と「貼る」は別の仕事

絵を画面に出すには、2 つの段階を踏みます。

段階やることいつ
読み込む
pyxel.load
.pyxres ファイルの中身を、Pyxel のイメージバンク(絵の置き場)へ運び込む 起動時に 1 回だけ
貼る
pyxel.blt
イメージバンクから絵を切り出して、画面の指定した位置にコピーする 毎フレーム(1 秒に 30 回)

ここを取り違えると動きません。ファイルを読むのは重い処理なので、 毎フレームやってはいけません。逆に、画面は毎フレーム cls で消されるので、 貼るほうは毎回やらないと宇宙船は一瞬で消えます。

「読み込む」は引っ越しのイメージ .pyxres はハードディスクの中のファイルです。そのままでは Pyxel は絵を使えません。 pyxel.load は、その中身をメモリ上のイメージバンクへ運び込む作業です。 一度運び込んでしまえば、あとは何度でも高速に使い回せます。

1-2. blt に渡す 8 つの数字

pyxel.blt は引数が 8 個あります。多く見えますが、 「どこへ」「どこから」「どれだけ」「何を抜くか」の 4 セットに分かれているだけです。

pyxel.blt(x, y, img, u, v, w, h, colkey)
引数意味今回の値
x, y画面のどこへ貼るか(左上のかど)72, 80
imgどのイメージバンクから取るか(0〜2)0
u, v台紙のどこから切り出すか0, 0
w, h何ドットぶん切り出すか16, 16
colkey塗らない色(透過色)。省略可0
切り出す側:イメージバンク 0(左上を拡大) 貼る側:画面(160 x 120 ドット) (u, v) = (0, 0) w = 16 h = 16 blt 切り出して貼る x = 72 y = 80 pyxel.blt(72, 80, 0, 0, 0, 16, 16, 0)
左が絵の置き場(イメージバンク 0)、右が画面。uv台紙の中の座標、xy画面の中の座標です
用語:uv 台紙側の横・縦を表す座標です。画面側の xy と区別するために、 わざと別の文字を使う習わしになっています。u が横、v が縦です。 今回の宇宙船は台紙の左上に描いたので u = 0v = 0 です。
座標はすべて「左上のかど」を指します x = 72y = 80宇宙船の中心ではなく左上のかどの位置です。 16 ドットの絵を画面の横中央に置きたいときは、 (160 - 16) / 2 = 72 のように絵の幅を引いてから半分にします。

1-3. 黒を「塗らない色」に指定する

リソースエディタで宇宙船を描いたとき、周りは黒(色番号 0)のままでした。 このまま貼ると、16 × 16 の黒い四角ごと画面に貼られてしまい、 宇宙船の後ろを流れるはずの星が四角く隠れます。

そこで blt の 8 番目の引数 colkey(カラーキー=透過色)に 0 を渡します。すると Pyxel は色番号 0 のドットだけ塗らずに飛ばすようになります。

colkey を書かないと 黒い四角ごと貼られ、中の星が消える colkey に 0 を渡すと 黒が抜けて、星がそのまま見える
宇宙船のまわりを 7 倍に拡大したところ。破線が貼り付ける 16 × 16 の範囲です。左では範囲内の星がすべて消えていますが、右では機体の黒いすきま(機首の両脇、噴射口のあいだ)から星が見えています
透過は「消す」ではなく「描かない」 透明にしているのではありません。その色のドットだけ処理をスキップしているだけです。 だから下に描いてあったもの(星)がそのまま残って見えます。 スプライトを扱うゲームでは、この「1 色を犠牲にして抜き色にする」やり方が定番です。

1-4. 描いた順に、上へ重なる

draw の中では、先に書いた命令ほど奥後に書いた命令ほど手前になります。 紙に絵の具を重ね塗りするのと同じで、あとから塗ったものが上にきます。

1. cls(0) 画面を黒で消す 2. pset 星を描く 3. blt 宇宙船を描く 4. text STAR VOYAGER PRESS ESC TO QUIT 文字を描く
毎フレーム、この 4 段階を左から順にやり直しています。順番を入れ替えると重なりも変わります

だから draw「奥のものから順に」書きます。 星 → 宇宙船 → 文字、の順です。 もし宇宙船を星より先に描くと、宇宙船の上を星が通り過ぎてしまいます。

2. このフェーズで使う API

API意味
pyxel.load(ファイル名) .pyxres を読み込み、絵・音をイメージバンクなどへ入れる。 pyxel.initあとに書く
pyxel.blt(x, y, img, u, v, w, h, colkey) イメージバンク img(u, v) から w × h ドットを 切り出し、画面の (x, y) へ貼る。colkey の色は塗らない
pyxel.COLOR_BLACK 色番号 0(黒)を表す定数。0 と書いても同じ
colkey は省略できます pyxel.blt(x, y, 0, 0, 0, 16, 16) のように 7 個で呼ぶこともできます。 その場合は透過なし(全ドットをそのまま貼る)になります。 「省略できる引数」がある関数は Pyxel に多いので、覚えておいてください。

3. コードを書き換える

変更は 3 か所だけです。まず変更点を確認してから、下の全文と見比べてください。

3-1. 定数を足す(STAR_SPEED_MAX の下)

SHIP_U = 0              # 台紙のどこから切り出すか(横)
SHIP_V = 0              # 台紙のどこから切り出すか(縦)
SHIP_WIDTH = 16         # 宇宙船の幅
SHIP_HEIGHT = 16        # 宇宙船の高さ

3-2. __init__ に読み込みと初期位置を足す

        pyxel.init(SCREEN_WIDTH, SCREEN_HEIGHT, title="Star Voyager")
        pyxel.load("assets.pyxres")     # ← 追加:絵と音のファイルを読み込む

        self.stars = []
        self.make_stars()

        # ↓ 追加:宇宙船の位置を覚えておく
        self.ship_x = (SCREEN_WIDTH - SHIP_WIDTH) // 2   # 横は画面の中央
        self.ship_y = 80                                 # 縦は下のほう

        pyxel.run(self.update, self.draw)

3-3. drawblt を足す

        # 2. 宇宙船(星より手前)      ← 星のループと文字のあいだに追加
        pyxel.blt(
            self.ship_x, self.ship_y,   # 画面のどこに貼るか
            0,                          # どのイメージバンクから取るか
            SHIP_U, SHIP_V,             # 台紙のどこから切り出すか
            SHIP_WIDTH, SHIP_HEIGHT,    # 何ドットぶん切り出すか
            pyxel.COLOR_BLACK,          # この色は塗らない(透過色)
        )

3-4. 書き換え後の全文

# star_voyager.py
# step0 フェーズ5:宇宙船を画面に出す

import random

import pyxel

SCREEN_WIDTH = 160
SCREEN_HEIGHT = 120

TITLE = "STAR VOYAGER"
GUIDE = "PRESS ESC TO QUIT"

STAR_COUNT = 40         # 星の数
STAR_SPEED_MIN = 0.3    # 星の速さの下限(ドット/フレーム)
STAR_SPEED_MAX = 1.8    # 星の速さの上限

SHIP_U = 0              # 台紙のどこから切り出すか(横)
SHIP_V = 0              # 台紙のどこから切り出すか(縦)
SHIP_WIDTH = 16         # 宇宙船の幅
SHIP_HEIGHT = 16        # 宇宙船の高さ


class App:
    """このゲーム全体を受け持つ箱"""

    def __init__(self):
        """箱が作られるときに 1 回だけ呼ばれる。ここで準備をすませる"""
        pyxel.init(SCREEN_WIDTH, SCREEN_HEIGHT, title="Star Voyager")
        pyxel.load("assets.pyxres")     # 絵と音のファイルを読み込む

        self.stars = []                 # 星をぜんぶ入れるリスト
        self.make_stars()

        self.ship_x = (SCREEN_WIDTH - SHIP_WIDTH) // 2   # 横は画面の中央
        self.ship_y = 80                                 # 縦は下のほう

        pyxel.run(self.update, self.draw)

    def make_stars(self):
        """星を STAR_COUNT 個ぶん作って self.stars に入れる"""
        for i in range(STAR_COUNT):
            x = random.randint(0, SCREEN_WIDTH - 1)
            y = random.randint(0, SCREEN_HEIGHT - 1)
            speed = random.uniform(STAR_SPEED_MIN, STAR_SPEED_MAX)
            self.stars.append([x, y, speed])

    def star_color(self, speed):
        """速い星ほど明るい色にして、奥行きを表す"""
        if speed > 1.2:
            return pyxel.COLOR_WHITE
        if speed > 0.7:
            return pyxel.COLOR_GRAY
        return pyxel.COLOR_DARK_BLUE

    def text_center(self, y, s, col):
        """文字列 s を、画面の横中央に来るように描く"""
        x = (SCREEN_WIDTH - len(s) * pyxel.FONT_WIDTH) // 2
        pyxel.text(x, y, s, col)

    def update(self):
        """毎フレームの更新処理。星を下へ動かす"""
        for star in self.stars:
            star[1] += star[2]                                  # y に速さを足す

            if star[1] >= SCREEN_HEIGHT:                        # 画面の下に出たら
                star[0] = random.randint(0, SCREEN_WIDTH - 1)   # x を引き直して
                star[1] = 0                                     # いちばん上に戻す

    def draw(self):
        """毎フレームの描画処理。奥のものから順に描く"""
        pyxel.cls(0)

        # 1. 星(いちばん奥)
        for star in self.stars:
            pyxel.pset(star[0], star[1], self.star_color(star[2]))

        # 2. 宇宙船(星より手前)
        pyxel.blt(
            self.ship_x, self.ship_y,   # 画面のどこに貼るか
            0,                          # どのイメージバンクから取るか
            SHIP_U, SHIP_V,             # 台紙のどこから切り出すか
            SHIP_WIDTH, SHIP_HEIGHT,    # 何ドットぶん切り出すか
            pyxel.COLOR_BLACK,          # この色は塗らない(透過色)
        )

        # 3. 文字(いちばん手前)
        self.text_center(20, TITLE, pyxel.COLOR_YELLOW)
        self.text_center(34, GUIDE, pyxel.COLOR_GRAY)


App()

実行して確認

python step0/star_voyager.py

4. コードの解説

pyxel.loadpyxel.init のあとに書く

順番が逆だと動きません。pyxel.init が「Pyxel の置き場所(イメージバンクなど)を用意する」 命令だからです。置き場所が無いところに荷物は運び込めません。

pyxel.init(...)              # 1. 置き場所を用意する
pyxel.load("assets.pyxres")  # 2. そこへ運び込む

"assets.pyxres" はどこを探しにいくのか

ここは間違えやすいところです。pyxel.load に渡す相対パスは、 実行したスクリプト(star_voyager.py)が置いてあるフォルダを基準に探されます。 いま自分がいるフォルダ(カレントディレクトリ)は関係ありません。

learning_pyxel/          ← ここでコマンドを打っても
└── step0/
    ├── star_voyager.py  ← 基準になるのはこのファイルの場所
    └── assets.pyxres    ← なので "assets.pyxres" で見つかる

つまり python step0/star_voyager.py と打っても、 step0 に移動してから python star_voyager.py と打っても、 どちらも pyxel.load("assets.pyxres") で読めます。 "step0/assets.pyxres" とは書きません。

これは Python の open とは違う挙動です 普通の open("assets.pyxres")カレントディレクトリ基準で探すため、 リポジトリのルートから実行すると見つかりません。 pyxel.load は「ゲームのファイルはスクリプトと一緒に置くもの」という前提で、 親切に振る舞ってくれています。

なぜ self.ship_x なのか。ただの ship_x ではだめか

今回は画面の中央に置いたきりで動かさないので、実は定数でも足ります。 それでも self. を付けてインスタンスの属性にしたのには理由があります。

次のフェーズ 6 で、この値をキー入力で書き換えるからです。

置き場所性質向いているもの
SHIP_WIDTH = 16
(クラスの外の定数)
ずっと変わらない 絵のサイズ、画面サイズ、色
self.ship_x
(インスタンスの属性)
ゲーム中に変化する
update で書き換え、draw で読む
位置、速度、得点、残機

self. が付いた変数は updatedraw の両方から見えて、 しかもフレームをまたいで値が残ります。これがクラスを使う最大の理由です。 「変わるものは self. に、変わらないものは定数に」と覚えてください。

引数を縦に並べて書いた理由

pyxel.blt(self.ship_x, self.ship_y, 0, SHIP_U, SHIP_V, SHIP_WIDTH, SHIP_HEIGHT, 0) と 1 行で書いてもまったく同じ意味です。ただ、数字が 8 個並ぶと何番目が何なのか読めません

Python ではカッコの中は自由に改行できるので、意味のまとまりごとに改行して コメントを付けられます。行末に \ などは不要です。 最後の引数のうしろにコンマ(,)が付いているのも文法違反ではなく、 あとで引数を足しやすくするための書き方です。

pyxel.COLOR_BLACK0 は同じもの

pyxel.COLOR_BLACK の中身はただの 0 です。 それでも名前のほうを書くのは、読んだときに意味が分かるからです。 pyxel.blt(..., 0) だと最後の 0 が 「バンク番号? 座標? 色?」と迷いますが、 pyxel.COLOR_BLACK なら一目で色だと分かります。

マジックナンバー コードに直接書かれた、意味の分からない数値のことをマジックナンバーと呼びます。 名前を付けて定数にすると読みやすくなり、あとで変更するときも 1 か所で済みます。 SHIP_WIDTH = 16 と定数にしたのも同じ理由です。

5. 試してみよう

1 つずつ変えて実行し、変えたら元に戻して次へ進んでください。

やってみること確かめたいこと
最後の pyxel.COLOR_BLACK, の行を消す
SHIP_HEIGHT, のコンマも消す)
宇宙船が黒い四角ごと貼られる。透過色の役目がはっきり分かる
pyxel.COLOR_BLACKpyxel.COLOR_WHITE に変える 白いドット(コックピットの支柱)だけが抜けて星が透ける
SHIP_WIDTH8 にする 宇宙船の左半分だけが表示される。「切り出す大きさ」の意味が分かる
SHIP_U8 にする 切り出す位置が右へずれて、右半分+台紙の余白が貼られる
blt の行を、星のループよりに移動する 星が宇宙船の上を通るようになる。描画順=重なり順を確認できる
self.ship_y110 にする 宇宙船が画面の下からはみ出す。はみ出しても落ちない(エラーにならない)
pyxel.load(...) の行を消す 宇宙船が消える。読み込まないとイメージバンクは空のまま

6. うまくいかないときは

症状原因と対処
宇宙船が表示されない(エラーなし) pyxel.load を書き忘れているか、 bltpyxel.run より後ろに書いている。 または draw の中ではなく __init__blt を書いている
宇宙船が黒い四角付きで表示される 8 番目の引数 colkey を渡していない
宇宙船のところだけ真っ黒な穴があく リソースエディタで背景を黒(0)以外の色で塗りつぶしている。 エディタで背景を 0 に戻す
OSError / ファイルが見つからない系のエラー assets.pyxresstar_voyager.py同じ step0 フォルダに 無い。"step0/assets.pyxres" と書いてしまっている場合も同じエラーになる
TypeError: blt() missing ... argument 引数の数が足りない。x, y, img, u, v, w, h7 個は必須
変な絵(台紙の別の場所)が貼られる SHIP_USHIP_V0 になっていない。 または宇宙船を台紙の左上以外に描いている
星が宇宙船の上を通る blt が星のループより前にある。後ろへ移動する
絵が更新されない(前の絵のまま) リソースエディタで Ctrl + S による保存を忘れている
次のフェーズ フェーズ 6 では pyxel.btn でキー入力を受け取り、 矢印キーで宇宙船を動かします。 今回作った self.ship_xself.ship_yupdate の中で書き換えるだけです。画面の外へ出ないようにする処理も学びます。