step0:星空を飛ぶ宇宙船
画面いっぱいに星が散らばり、上から下へ流れ続けます。 速い星は明るく、遅い星は暗く描くことで奥行きが生まれます。 画面の下に出た星は上に戻って再利用されるので、星は無限に流れ続けます。
update に中身が入り、画面が動きます。
新しい概念がまとめて 3 つ出てくるので、2 つのステップに分けて進めます。
ステップ A で星を止めたまま表示し、動くのを確認してからステップ B で流します。
アニメーションに特別な仕組みはありません。1 フレームごとに y 座標を少しだけ増やし、 その位置に描き直す。これを 1 秒間に 30 回繰り返すと、人の目には滑らかに動いて見えます。
update、描くのは draw。役割は分けたままです
星ごとに x 座標・y 座標・落ちる速さ の 3 つが必要です。速さを星ごとに変えることで
奥行きが出ます。これを [x, y, 速さ] という 3 要素のリストで表し、
そのリストを さらにリストに入れて 全部の星をまとめて持ちます。
stars[2][1] と書くと「3 個目の星の y 座標」を指します星が下に流れ切ったら、その星を捨てて新しく作る必要はありません。 y を 0 に戻し、x をランダムに引き直せばそのまま新しい星として再利用できます。 星の個数は最初の 40 個から一生変わらず、それでも無限に流れ続けます。
| API・機能 | 意味 |
|---|---|
pyxel.pset(x, y, col) | (x, y) に色 col の点を 1 ドット打つ |
import random | 乱数を扱う Python の標準モジュール |
random.randint(a, b) | a 以上 b 以下の整数をランダムに返す(b を含む) |
random.uniform(a, b) | a 以上 b 以下の小数をランダムに返す |
リスト.append(値) | リストの末尾に要素を 1 つ追加する |
range(n) | 0 から n-1 までを順に取り出す。for と組で n 回繰り返す |
まず動かさずに、星が画面に散らばるところまで作ります。
star_voyager.py を次のように書き換えてください。追加した行に印を付けています。
# star_voyager.py
# step0 フェーズ3:星を流す(ステップA:星を表示する)
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 # ← 追加:星の速さの上限
stars = [] # ← 追加:星をぜんぶ入れるリスト
def make_stars(): # ← 追加:ここから
"""星を STAR_COUNT 個ぶん作って 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)
stars.append([x, y, speed])
def star_color(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(y, s, col):
"""文字列 s を、画面の横中央に来るように描く"""
x = (SCREEN_WIDTH - len(s) * pyxel.FONT_WIDTH) // 2
pyxel.text(x, y, s, col)
def update():
"""毎フレームの更新処理。まだ何もしない"""
pass
def draw():
"""毎フレームの描画処理"""
pyxel.cls(0)
for star in stars: # ← 追加:星を描く
pyxel.pset(star[0], star[1], star_color(star[2]))
text_center(20, TITLE, pyxel.COLOR_YELLOW)
text_center(34, GUIDE, pyxel.COLOR_GRAY)
pyxel.init(SCREEN_WIDTH, SCREEN_HEIGHT, title="Star Voyager")
make_stars() # ← 追加:星を用意してから
pyxel.run(update, draw)
python step0/star_voyager.py
update だけを次のように書き換えます。他の場所はそのままです。
def update():
"""毎フレームの更新処理。星を下へ動かす"""
for star in stars:
star[1] += star[2] # y に速さを足す
if star[1] >= SCREEN_HEIGHT: # 画面の下に出たら
star[0] = random.randint(0, SCREEN_WIDTH - 1) # x を引き直して
star[1] = 0 # いちばん上に戻す
先頭のコメントも ステップB:星を流す に直しておくと分かりやすくなります。
4 つとも確認できたらフェーズ 3 は完了です。
stars.append([x, y, speed])
[x, y, speed] というリストを 1 つ作って、それを stars に追加しています。
結果として stars は「リストを要素に持つリスト」になります。
| 書き方 | 指すもの |
|---|---|
stars | 星ぜんぶ(40 要素のリスト) |
stars[0] | 1 個目の星([x, y, 速さ] の 3 要素リスト) |
stars[0][1] | 1 個目の星の y 座標 |
star[2](for の中) | 今見ている星の 速さ |
star[1] より star.y と書けたほうが分かりやすい、と感じたはずです。
そのための仕組みがクラスで、次のフェーズ 4 で導入します。
いまの「番号で覚えるのが面倒」という感覚が、クラスを学ぶ動機になります。
for star in stars: の中で書き換えると、元のリストが変わるfor star in stars:
star[1] += star[2] # ← これで stars の中身が本当に変わる
star はリストの要素のコピーではなく、同じものを指す別名です。
だから star[1] を書き換えると stars の中身が変わります。
numbers = [1, 2, 3]
for n in numbers:
n += 10 # numbers は [1, 2, 3] のまま変わらない
n には値そのものが入るため、n を書き換えても元のリストには影響しません。
一方リストや、次に学ぶクラスのオブジェクトは中身を指す名札が渡るので、書き換えが元に反映されます。
Python でつまずきやすい区別なので、頭の片隅に置いておいてください。
stars = [] # 関数の外で作った
def make_stars():
stars.append(...) # 関数の中から追加できる(global 宣言は不要)
関数の外にある変数は、関数の中から読むことはできます。
append は「stars という箱の中身をいじる」操作なので、そのまま実行できます。
一方、stars = [] のように入れ直そうとすると別の話になります。
これは「stars という名前を新しいリストに付け替える」操作で、
関数の中で書くと関数の中だけの別の変数になってしまい、外側は変わりません。
その場合は global stars という宣言が必要です。
append だけなので global は不要です
この「外の変数をどう扱うか」という悩みも、フェーズ 4 でクラスにまとめると自然に解消します。
random.randint と random.uniform の違い| 関数 | 返る値 | 両端 | 今回の使いどころ |
|---|---|---|---|
random.randint(0, 159) | 整数 | 159 を含む | x 座標(ドットは整数のマス目) |
random.uniform(0.3, 1.8) | 小数 | 0.3 と 1.8 を含む | 速さ(細かく散らしたい) |
SCREEN_WIDTH - 1 の -1 を忘れないでください
画面幅が 160 のとき、使える x 座標は 0 〜 159 です。
randint(0, 160) と書くと、たまに画面外の 160 が出てしまいます。
randint は右端を含むことに注意してください。
star_colordef star_color(speed):
if speed > 1.2:
return pyxel.COLOR_WHITE
if speed > 0.7:
return pyxel.COLOR_GRAY
return pyxel.COLOR_DARK_BLUE
return はその場で関数を終える命令です。speed が 1.5 なら
最初の if で COLOR_WHITE を返して終わるので、
以降の行は実行されません。だから elif や else を書かなくても
正しく振り分けられます。
これまでの関数(text_center など)は「何かをして終わり」でしたが、
この関数は値を返します。返った値は pyxel.pset(..., star_color(star[2])) のように
そのまま別の関数に渡せます。
速さが 1.5 のような小数なので、star[1] も
10.5 12.0 …と小数になっていきます。
pyxel.pset は小数を受け取ると小数点以下を切り捨てて描くので、そのままで問題ありません。
むしろ小数のまま持つほうが正確です。もし整数に丸めてから足していくと、
0.3 のような遅い星は毎回 0 に切り捨てられ、永遠に動かなくなります。
make_stars() を呼ぶ位置pyxel.init(...)
make_stars() # 星を用意してから
pyxel.run(update, draw)
pyxel.run はそこで止まるので、それより後ろに書いた行は実行されません。
星の準備は必ず run の前に置きます。
| やってみること | 確かめたいこと |
|---|---|
STAR_COUNT を 200 にする | 星が濃くなる。数を定数にしておいた利点が分かる |
STAR_SPEED_MAX を 6.0 にする | ワープしているような速さになる |
update の star[1] = 0 を消す | 星が下に落ちきったきり戻らず、画面が空になる(=再利用の意味) |
star[1] += star[2] を star[0] += star[2] に変える | 星が右へ流れる。x と y の役割を確認できる |
draw で星を描く for を、文字を描く 2 行の後ろに移す | 星が文字の手前に描かれ、文字が読みにくくなる(=描画順) |
確認できたら元に戻しておいてください。
| 症状 | 原因と対処 |
|---|---|
| 星が 1 つも表示されない | make_stars() の呼び出しが抜けているか、pyxel.run(...) より後ろにある |
NameError: name 'random' is not defined |
ファイル先頭の import random が抜けている |
| 星が動かない(ステップ B 後) | update の中の pass が残っている。書き換えて消す |
| 一瞬で星が全部消える | if star[1] >= SCREEN_HEIGHT: の中で star[1] = 0 ではなく
star[1] == 0 と書いている(= が代入、== は比較) |
| 星が一直線に並んで落ちてくる | x を引き直す行が抜けている。star[0] = random.randint(...) を確認 |
IndexError: list index out of range |
star[3] のように存在しない番号を指している。要素は [0] [1] [2] の 3 つだけ |