フェーズ 3 星を流す

step0:星空を飛ぶ宇宙船

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

ゴール

画面いっぱいに星が散らばり、上から下へ流れ続けます。 速い星は明るく、遅い星は暗く描くことで奥行きが生まれます。 画面の下に出た星は上に戻って再利用されるので、星は無限に流れ続けます。

このフェーズが step0 の山場です ここで初めて update に中身が入り、画面が動きます。 新しい概念がまとめて 3 つ出てくるので、2 つのステップに分けて進めます。 ステップ A で星を止めたまま表示し、動くのを確認してからステップ B で流します。

1. 考え方

「動く」とは、座標を毎フレーム少しずつ変えること

アニメーションに特別な仕組みはありません。1 フレームごとに y 座標を少しだけ増やし、 その位置に描き直す。これを 1 秒間に 30 回繰り返すと、人の目には滑らかに動いて見えます。

y = 10 y = 12 y = 14 y = 16 +2 +2 +2 1 秒間に 30 回これを繰り返す = 動いて見える
座標を変えるのは update、描くのは draw。役割は分けたままです

星 1 個が覚えておくことは 3 つ

星ごとに x 座標・y 座標・落ちる速さ の 3 つが必要です。速さを星ごとに変えることで 奥行きが出ます。これを [x, y, 速さ] という 3 要素のリストで表し、 そのリストを さらにリストに入れて 全部の星をまとめて持ちます。

stars = 星ぜんぶを入れたリスト [0] [ 24 , 8 , 1.5 ] [1] [ 117 , 45 , 0.4 ] [2] [ 60 , 92 , 0.9 ] … 全部で 40 個 star[0] star[1] star[2] x 座標 y 座標 速さ 1 行 = 星 1 個 リストの中に リストが入っている = 二次元リスト
stars[2][1] と書くと「3 個目の星の y 座標」を指します

画面の下に出たら、上に戻して使い回す

星が下に流れ切ったら、その星を捨てて新しく作る必要はありません。 y を 0 に戻し、x をランダムに引き直せばそのまま新しい星として再利用できます。 星の個数は最初の 40 個から一生変わらず、それでも無限に流れ続けます。

画面(高さ 120) y が 120 以上になった y = 0 に戻す x は引き直す 星の数は ずっと 40 個
作っては捨てるより速く、書くコードも短くなります。ゲームでよく使う考え方です

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

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 回繰り返す

3. ステップ A:星を作って、止めたまま表示する

まず動かさずに、星が画面に散らばるところまで作ります。 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
ここまで確認できたらステップ B へ 星が出ない・エラーが出る場合は、先に下の「うまくいかないときは」を確認してください。

4. ステップ B:星を流す

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 は完了です。

5. コードの解説

リストの中のリスト(二次元リスト)

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.randintrandom.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_color

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

return はその場で関数を終える命令です。speed が 1.5 なら 最初の ifCOLOR_WHITE を返して終わるので、 以降の行は実行されません。だから elifelse を書かなくても 正しく振り分けられます。

これまでの関数(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に置きます。

6. 試してみよう

やってみること確かめたいこと
STAR_COUNT200 にする星が濃くなる。数を定数にしておいた利点が分かる
STAR_SPEED_MAX6.0 にするワープしているような速さになる
updatestar[1] = 0 を消す星が下に落ちきったきり戻らず、画面が空になる(=再利用の意味)
star[1] += star[2]star[0] += star[2] に変える星が右へ流れる。x と y の役割を確認できる
draw で星を描く for を、文字を描く 2 行の後ろに移す星が文字の手前に描かれ、文字が読みにくくなる(=描画順)

確認できたら元に戻しておいてください。

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

症状原因と対処
星が 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 つだけ
次のフェーズ フェーズ 4 では、リソースエディタで宇宙船のドット絵を描きます。 あわせて、増えてきた変数を整理するためにクラスを導入します。