step0:星空を飛ぶ宇宙船
このフェーズでやることは 2 つです。
step0/assets.pyxres に保存するApp というクラスにまとめ直す
星は pyxel.pset 1 行で描けました。1 ドットだからです。
では宇宙船はどうでしょう。これから描くものは 100 ドットほどあります。
pset を 100 行書くのは現実的ではありませんし、
1 ドット直すたびにコードを書き換えることになります。
そこで発想を変えます。絵は絵として別に作っておき、コードからは 「あの絵を、あの場所に貼れ」とだけ指示する。 Pyxel はこのために リソースエディタという専用のお絵かきツールを内蔵しています。
0・1・2 の 3 枚あり、
ゲームで使う絵をここに並べておきます。コードからは
「何番の台紙の、どの座標から、何ドットぶん」という指定で切り出して使います。
pyxel.blt(切り出して貼る)はフェーズ 5 で扱います。
いまのコードでは、星のリスト stars を関数の外に置いています。
これはフェーズ 3 までは何の問題もありませんでした。しかし、この先を考えてみてください。
ship_x と ship_y が増える関数の外に置く変数(グローバル変数)が増えていくと、 「この変数は、どの関数が書き換えているのか」が追えなくなります。 そして、もっと困る問題が待っています。
フェーズ 6 で宇宙船を右へ動かすとき、素直に書くとこうなります。ですがこれは動きません。
ship_x = 72 # 関数の外に置いた変数
def update():
ship_x = ship_x + 2 # ← UnboundLocalError で落ちる
Python は「関数の中で代入されている名前は、その関数だけのローカル変数だ」と判断します。
そのため ship_x + 2 を計算しようとした時点で、
まだ何も入っていないローカルの ship_x を読もうとしてエラーになります。
stars.append(...) は動いたのか
append は「stars という名前に別のものを代入する」のではなく、
「stars が指しているリストの中身を変える」操作だからです。
名前への代入がないので、Python はローカル変数だと判断しません。
詳しくはフェーズ 3 の解説で扱いました。ship_x を増やすには必ず代入が必要になります。
回避策として global ship_x と宣言する方法はありますが、
変数が増えるたびに書き足すことになり、結局は散らかります。そこで クラスの出番です。
App という箱を作り、その中にデータ(星のリスト)も
それを扱う関数も入れてしまいます。箱の中からは self. を付けるだけで
データに手が届き、代入も自由にできます。
| API・機能 | 意味 |
|---|---|
pyxel edit ファイル名 | リソースエディタを開く。ファイルが無ければ空の状態で開き、保存した時点で作られる |
.pyxres | 絵・タイルマップ・音・曲をまとめて 1 つに入れる、Pyxel のリソースファイル |
イメージバンク 0〜2 | 256×256 ドットの絵の置き場。3 枚ある |
class App: | クラス(データと関数をまとめた設計図)を定義する |
def __init__(self): | 箱が作られるときに 1 回だけ呼ばれる、準備専用の関数 |
self | 「作られた箱そのもの」を指す。クラスの中の関数では第 1 引数に必ず書く |
self.名前 = 値 | その箱が覚えておく変数(属性)を作る/書き換える |
App() | 設計図から箱を 1 つ作る。このとき __init__ が呼ばれる |
self.stars など)を属性、
設計図から実際に作られた箱 1 個をインスタンスと呼びます。
用語は後からで構いません。まずは「箱にまとめる」感覚をつかんでください。
ターミナルで次を実行します。
pyxel edit step0/assets.pyxres
assets.pyxres はまだ存在しませんが、エラーにはなりません。
空のリソースがエディタで開き、保存した時点でファイルが作られます。
Esc では閉じません
リソースエディタは Esc キーを無効にしています(描いている途中に誤って閉じないためです)。
ウィンドウ右上の × ボタンで閉じてください。
そして閉じる前に必ず Ctrl + S で保存すること。
保存していない変更は警告なしに消えます。
覚えておく点は 3 つだけです。
| 操作 | できること |
|---|---|
| キャンバスを左ドラッグ | 選んでいる色で描く |
| キャンバスを右クリック | その場所の色を吸い取る(スポイト) |
ドラッグ中に Shift | まっすぐ/正方形に補正される |
1〜8 | 色 0〜7 を選ぶ |
Shift + 1〜8 | 色 8〜15 を選ぶ |
P / S | ペン/選択ツール |
R / Shift + R | 四角(枠だけ/塗りつぶし) |
C / Shift + C | 円(枠だけ/塗りつぶし) |
B | バケツ(つながった同じ色を塗りつぶす) |
Ctrl + Z / Ctrl + Y | 元に戻す/やり直す |
Ctrl + S | 保存 |
Alt + ← / → | IMAGE / TILEMAP / SOUND / MUSIC を切り替える |
下が設計図です。左上の (0, 0) から 16×16 ドットの範囲に描きます。 方眼の太い線が 8 ドットごとの区切りです。
u、縦方向を v と呼びます(フェーズ 5 で使います)使う色は、背景の黒を入れて 7 色です。キーボードの数字キーで切り替えられます。
| 見本 | 色番号 | 名前 | 選ぶキー | 使う場所 |
|---|---|---|---|---|
0 | BLACK | 1 | 背景(そのまま/あとで透明にする) | |
5 | DARK_BLUE | 6 | 翼 | |
6 | LIGHT_BLUE | 7 | 機体 | |
7 | WHITE | 8 | 輪郭・機首 | |
8 | RED | Shift + 1 | 噴射の外側 | |
10 | YELLOW | Shift + 3 | 噴射の芯 | |
12 | CYAN | Shift + 5 | コックピット |
0(黒)のまま残す … フェーズ 5 で色 0 を「透明」として抜くためです。
背景を黒以外で塗ってしまうと、宇宙船のまわりに四角い枠が出ますCtrl + S を押すls step0
assets.pyxres が並んでいれば成功です。もう一度
pyxel edit step0/assets.pyxres を実行して、
描いた絵がちゃんと残っていることも確かめておきましょう。
.pyxres は中身がバイナリなので、VS Code で開いても読めません。それで正常です。
ここからはコードの作業です。動きは 1 ミリも変えません。 同じ動作のまま、置き場所だけを整えます。 このように「外から見た動きを変えずに、内側を整理する」書き換えを リファクタリングと呼びます。
star_voyager.py を下の内容にまるごと書き換えてください。
定数の部分(上から 15 行目まで)は今までと同じなので、そのまま残して構いません。
# star_voyager.py
# step0 フェーズ4:素材を描き、クラスにまとめる
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 # 星の速さの上限
class App:
"""このゲーム全体を受け持つ箱"""
def __init__(self):
"""箱が作られるときに 1 回だけ呼ばれる。ここで準備をすませる"""
pyxel.init(SCREEN_WIDTH, SCREEN_HEIGHT, title="Star Voyager")
self.stars = [] # 星をぜんぶ入れるリスト
self.make_stars()
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)
for star in self.stars:
pyxel.pset(star[0], star[1], self.star_color(star[2]))
self.text_center(20, TITLE, pyxel.COLOR_YELLOW)
self.text_center(34, GUIDE, pyxel.COLOR_GRAY)
App()
class App: の中へ入れる(インデントを 4 つ深くする)self を足すstars を self.stars に、
star_color(...) を self.star_color(...) に書き換えるpyxel.init / make_stars() / pyxel.run の 3 行を
__init__ の中へ移し、いちばん下は App() だけにするpython step0/star_voyager.py
Esc で閉じられる「何も変わらない」ことが確認できれば成功です。
class は設計図、App() は実物
class App: と書いた時点では、まだ何も動きません。これは設計図だからです。
いちばん下の App() で初めて、設計図から実物が 1 個作られます。
「たった 1 個しか作らないのに、なぜ設計図が要るのか」と思うかもしれません。 理由は変数と関数を 1 つの名前の下にまとめられるからで、 数を作るためではありません。step1 以降でお化けやお菓子を何十個も作るときには、 「設計図から実物をたくさん作る」という本来の使い方も出てきます。
__init__ は準備係
App() と書くと、Python は箱を作ったあと自動的に __init__ を呼びます。
自分で app.__init__() と書く必要はありません。
前後のアンダースコア 2 本は「Python が決めた特別な名前」という目印です。
今回の __init__ がやっているのは、フェーズ 3 でファイルの最後に書いていた 3 行そのままです。
| フェーズ 3(ファイルの末尾) | フェーズ 4(__init__ の中) |
|---|---|
pyxel.init(...) | pyxel.init(...) |
(stars = [] はファイル上部) | self.stars = [] |
make_stars() | self.make_stars() |
pyxel.run(update, draw) | pyxel.run(self.update, self.draw) |
self とは何者か
self は 「いま処理している、その箱そのもの」です。
不思議に見えるのは、呼ぶときには書かないのに、定義には書く点でしょう。
これは Python が自動で渡しているからです。
self という名前は決まりではなく慣習ですが、必ず self と書いてください。Python の世界の共通語ですself の書き忘れ
定義に self を書き忘れると
TypeError: make_stars() takes 0 positional arguments but 1 was given
というエラーになります。「0 個しか受け取らない関数に 1 個渡された」=
Python が渡した self の受け取り先がない、という意味です。
self.stars と、ただの stars の違い
self. を付け忘れて stars と書くと、それは
そのメソッドの中だけの、まったく別の変数になります。
書き換えても箱の中身は変わらないので、
「エラーは出ないのに星が動かない」という一番やっかいな症状になります。
クラスの中でデータを触るときは、必ず self. を付けると覚えてください。
pyxel.run(self.update, self.draw) にカッコが無い理由
self.update() と書くと「いま実行しろ」という意味になり、実行結果が渡ってしまいます。
カッコを付けない self.update は関数そのものを指します。
Pyxel に渡したいのは結果ではなく「毎フレーム呼ぶべき関数」なので、カッコは付けません。
これはフェーズ 1 の pyxel.run(update, draw) と同じ考え方で、
self. が付いただけです。
SCREEN_WIDTH や STAR_COUNT は一度決めたら変わらない値で、
箱ごとに違う値を持つ必要がありません。こういうものは外に置いたままで十分です。
「箱ごとに変わるもの」だけを self. に入れるのが目安です。
App() の 1 行だけで動くのか
App() → __init__ が呼ばれる → その中の pyxel.run(...) に到達する、
という流れです。pyxel.run はそこで止まってゲームを回し続けるので、
App() の 1 行が実質的にゲーム全体の実行になります。
| やってみること | 確かめたいこと |
|---|---|
make_stars の定義から self を消す |
TypeError: ... takes 0 positional arguments but 1 was given が出る |
update の中の self.stars を stars にする |
NameError: name 'stars' is not defined が出る(self. は省略できない) |
いちばん下の App() を App に変える |
何も起きずに終了する。カッコが「作れ」の合図だと分かる |
pyxel.run(self.update, self.draw) を
pyxel.run(self.update(), self.draw()) にする |
エラーになる。カッコの有無で意味が変わることを確認できる |
__init__ の中の self.make_stars() を消す |
星が 1 つも出ない。準備の呼び出しが要ることが分かる |
確認できたら元に戻しておいてください。
| 症状 | 原因と対処 |
|---|---|
Directory for 'step0/assets.pyxres' does not exist |
step0 フォルダのある場所(リポジトリのルート)から実行していない。
pwd で現在地を確認する |
エディタが Esc で閉じない |
仕様です。ウィンドウ右上の × で閉じてください |
| 閉じたら描いた絵が消えていた | Ctrl + S での保存を忘れている。保存前は閉じても警告が出ません |
| キャンバスに描いても反応しない | 選んでいる色が背景と同じ 0(黒)になっている。数字キーで別の色を選ぶ |
TypeError: ... takes 0 positional arguments but 1 was given |
メソッドの定義に self を書き忘れている |
NameError: name 'stars' is not defined |
self.stars と書くべきところが stars のままになっている |
NameError: name 'star_color' is not defined |
draw の中が self.star_color(...) になっていない。
同じクラスの中でも self. は必要です |
IndentationError が出る |
クラスの中に入れた関数のインデントが揃っていない。
def はすべてスペース 4 つの位置に揃える |
| 実行しても何も起きずに終わる | いちばん下の App() が抜けているか、App とカッコ無しで書いている |
| 星が出ない(エラーは無し) | __init__ の中の self.make_stars() が抜けている。
または pyxel.run(...) より後ろに書いている |
pyxel.load で読み込み、
pyxel.blt で画面に表示します。ようやく見た目が変わります。