フェーズ 2 男の子を矢印キーで動かす

位置を変数で覚えさせ、キー入力に合わせて毎フレーム動かす

トップページstep1 概要 / フェーズ 2

ゴール

フェーズ 1 で並べた 3 つの絵のうち、男の子だけを画面に残し、 矢印キーで上下左右に動かせるようにします。 画面の端まで行ったら、そこで止まって外へは出ないところまでを作ります。

フェーズ 1 の candy_hunt.py に書き足していく形です。新しいファイルは作りません。 update()pass が、いよいよ中身に変わります。

お化けとお菓子はいったん画面から外します フェーズ 1 では 3 つ並べて表示を確かめましたが、ここからは 1 つずつ役割を持たせていきます。 お菓子はフェーズ 3、お化けはフェーズ 5 で戻ってきます。 絵そのものは .pyxres に残っているので、消す必要はありません。

1. 考え方

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

Pyxel は pyxel.run(self.update, self.draw) を呼んだあと、 1 秒間に 30 回のペースで update()draw() をくり返し呼び続けます。 絵が動いて見えるのは、この 1 回ごとに描く座標をほんの少しずらしているからです。 パラパラ漫画と同じ仕組みです。

起動時(1 回だけ) pyxel.init() pyxel.load() 位置の変数を用意 pyxel.run() ここから先はくり返し update() 位置を計算する draw() その位置に絵を描く これを 1 秒間に 30 回
initload は起動時に 1 回だけ。updatedraw だけが毎フレーム呼ばれる
init() の役割でつまずいたら pyxel.init() が「入れ物を用意する関数」であることと、 load() を先に書くと Pyxel not initialized で落ちる理由は、 備忘録のinit() は何をしている?」にまとめてあります。

1-2. 位置は self に覚えさせる

男の子の位置は「いま画面のどこにいるか」というずっと持ち続ける情報です。 一方 update() は毎フレーム呼び出され、そのたびに中の処理が最初から実行されます。 つまり update() の中で

def update(self):
    boy_x = 76          # ← 毎フレーム 76 に戻ってしまう
    if pyxel.btn(pyxel.KEY_RIGHT):
        boy_x += 2

と書くと、右キーを押しても boy_x は 78 になった直後に捨てられ、 次のフレームでまた 76 から始まります。1 ドットも進みません。

そこで self.boy_x という形でインスタンス(App 自身)にくっつけて持たせます。 self. がついた変数は __init__ で一度作れば、 update() が何回呼ばれても値が残り続けます。

書き方作る場所寿命
boy_x = 76update() の中その 1 回の update() が終わるまで
self.boy_x = 76__init__ の中プログラムが終わるまで
step0 でも同じことをしていました 宇宙船の self.ship_x、ひよこの self.hiyoko_scale、 魚の self.fish_x ——「フレームをまたいで覚えておきたいもの」は、 どれも __init__self. をつけて用意していました。同じ考え方です。

1-3. update() で決めて、draw() で描く

キーを見て座標を変えるのは update()、その座標に絵を貼るのは draw() です。 この 2 つは役割を混ぜません。

やることやらないこと
update()キーを調べる、座標を計算する、当たり判定をする画面に描く
draw()画面を消す、絵や文字を描く座標を変える

draw() の中に self.boy_x += 2 と書いても、実は動いてしまいます。 それでも分けるのは、あとで「ゲームオーバー中は動かないが、絵は描き続ける」のような 場面が必ず出てくるからです。分けておけば update() の中身を呼ばないだけで実現できます。 フェーズ 6 と 7 で、この分け方が効いてきます。

1-4. 画面の外へ出さない

何もしないと、男の子は画面の外までどこまでも歩いて行って見えなくなります。 座標に上限と下限を設けて、はみ出したら端の値に押し戻します

ここで大事なのは、上限が 160 ではなく 152 になることです。 blt() に渡す座標は絵の左上の位置なので、 左上が 152 のとき絵の右端がちょうど 159(画面のいちばん右のドット)に来ます。

画面 160 × 120(図は 2 倍で表示) 0 152 160 だと 画面の外 self.boy_x が取れる範囲 152 = SCREEN_WIDTH - BOY_SIZE = 160 - 8
座標は絵の左上を指すので、右端の上限は画面幅から絵の幅を引いた値になる

縦も同じで、self.boy_y の範囲は 0 から SCREEN_HEIGHT - BOY_SIZE(= 120 − 8 = 112)までです。

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

機能役割
pyxel.btn(キー)そのキーが押されているあいだ True を返す
pyxel.KEY_LEFT / KEY_RIGHT / KEY_UP / KEY_DOWN矢印キーを表す番号
max(a, b)2 つのうち大きいほうを返す(下限を作るのに使う)
min(a, b)2 つのうち小さいほうを返す(上限を作るのに使う)

3. 手順

一度に全部書かず、3 段階に分けて書き足します。 途中で実行して、そのつど動きを確かめてください。

3-1. 定数と、位置の変数を用意する

まず、ファイルの上のほうにある定数に 2 行足します。

SCREEN_WIDTH = 160
SCREEN_HEIGHT = 120

BOY_SIZE = 8        # 男の子の絵の大きさ(縦横とも 8 ドット)
BOY_SPEED = 2       # 1 フレームで進むドット数

次に __init__pyxel.load()pyxel.run() のあいだに、 男の子の初期位置を 2 行足します。画面の中央に置きます。

    def __init__(self):
        """起動時の設定"""
        pyxel.init(SCREEN_WIDTH, SCREEN_HEIGHT, title="Candy Hunt")
        pyxel.load("candy_hunt.pyxres")

        self.boy_x = SCREEN_WIDTH // 2 - BOY_SIZE // 2      # ← 足す
        self.boy_y = SCREEN_HEIGHT // 2 - BOY_SIZE // 2     # ← 足す

        pyxel.run(self.update, self.draw)
なぜ - BOY_SIZE // 2 を引くのか SCREEN_WIDTH // 2(= 80)だけだと、絵の左上が中央に来るため、 見た目は右下に 4 ドットずれます。絵の大きさの半分を引くと、絵の真ん中が画面の中央に来ます。 // を使うのは、座標を整数のままにしておきたいからです (/// の使い分け)。

3-2. 矢印キーで動かす

update()pass を消して、移動処理を書きます。 移動そのものは move_boy() という別のメソッドに分けます。

    def update(self):
        """フレーム毎の更新処理"""
        self.move_boy()

    def move_boy(self):
        """矢印キーで男の子を動かす"""
        if pyxel.btn(pyxel.KEY_LEFT):
            self.boy_x -= BOY_SPEED
        if pyxel.btn(pyxel.KEY_RIGHT):
            self.boy_x += BOY_SPEED
        if pyxel.btn(pyxel.KEY_UP):
            self.boy_y -= BOY_SPEED
        if pyxel.btn(pyxel.KEY_DOWN):
            self.boy_y += BOY_SPEED

最後に draw() を書き換えます。3 行あった blt() を 1 つにして、 座標を self.boy_x / self.boy_y に差し替えます。

    def draw(self):
        """フレーム毎の描画処理"""
        pyxel.cls(pyxel.COLOR_BLACK)

        pyxel.blt(self.boy_x, self.boy_y, 0, 0, 0,
                  BOY_SIZE, BOY_SIZE, pyxel.COLOR_BLACK)

ここで一度実行してください。矢印キーで男の子が動けば成功です。 ただし端まで行くとそのまま画面の外へ出て、見えなくなってしまうはずです。 それを次で直します。

3-3. 画面の外へ出ないようにする

move_boy() の末尾に 2 行足します。 4 つの if がすべて終わったあと、最後にまとめて範囲内へ押し戻すのがコツです。

        # 画面の外へ出さない
        self.boy_x = max(0, min(self.boy_x, SCREEN_WIDTH - BOY_SIZE))
        self.boy_y = max(0, min(self.boy_y, SCREEN_HEIGHT - BOY_SIZE))

4. できあがりのコード

# candy_hunt.py
# step1 フェーズ2:男の子を矢印キーで動かす

import pyxel

SCREEN_WIDTH = 160
SCREEN_HEIGHT = 120

BOY_SIZE = 8        # 男の子の絵の大きさ(縦横とも 8 ドット)
BOY_SPEED = 2       # 1 フレームで進むドット数

class App:
    def __init__(self):
        """起動時の設定"""
        pyxel.init(SCREEN_WIDTH, SCREEN_HEIGHT, title="Candy Hunt")
        pyxel.load("candy_hunt.pyxres")

        self.boy_x = SCREEN_WIDTH // 2 - BOY_SIZE // 2
        self.boy_y = SCREEN_HEIGHT // 2 - BOY_SIZE // 2

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

    def update(self):
        """フレーム毎の更新処理"""
        self.move_boy()

    def move_boy(self):
        """矢印キーで男の子を動かす"""
        if pyxel.btn(pyxel.KEY_LEFT):
            self.boy_x -= BOY_SPEED
        if pyxel.btn(pyxel.KEY_RIGHT):
            self.boy_x += BOY_SPEED
        if pyxel.btn(pyxel.KEY_UP):
            self.boy_y -= BOY_SPEED
        if pyxel.btn(pyxel.KEY_DOWN):
            self.boy_y += BOY_SPEED

        # 画面の外へ出さない
        self.boy_x = max(0, min(self.boy_x, SCREEN_WIDTH - BOY_SIZE))
        self.boy_y = max(0, min(self.boy_y, SCREEN_HEIGHT - BOY_SIZE))

    def draw(self):
        """フレーム毎の描画処理"""
        pyxel.cls(pyxel.COLOR_BLACK)

        pyxel.blt(self.boy_x, self.boy_y, 0, 0, 0,
                  BOY_SIZE, BOY_SIZE, pyxel.COLOR_BLACK)

App()

5. コードの解説

btn()btnp() の違い

名前が似ていますが、True を返すタイミングがまったく違います。

関数True になるとき向いている用途
pyxel.btn(キー)押しているあいだ ずっと(毎フレーム)移動
pyxel.btnp(キー)押した瞬間の 1 フレームだけ決定、ジャンプ、画面の切り替え

移動に btnp() を使うと、キーを押しっぱなしにしても2 ドット進んで止まるだけになります。 逆にフェーズ 7 の「タイトル画面でキーを押してゲーム開始」には btnp() を使います。 btn() だと、押した勢いのままゲームオーバー画面まで一瞬で飛んでしまうからです。

if を 4 つ並べる。elif にはしない

4 つの if は、それぞれ独立して判定されます。 これを elif でつなぐと、どれか 1 つが成立した時点で残りは調べられません。 斜めに動けなくなります。

if を 4 つ並べる 斜めに進む 左の boy_x -= 2 上の boy_y -= 2 が両方効く elif でつなぐ 真横にしか進まない 左が成立した時点で、 上の判定は飛ばされる
どちらも「← と ↑ を同時に押した」ときの動き。同時に効かせたいので if を並べる
斜めは少しだけ速くなります ← と ↑ を同時に押すと横に 2・縦に 2 進むので、実際の移動距離は 2 ではなく 約 2.83 ドット(2 の 1.41 倍)になります。 今回のゲームでは気にならない差なので、このままにします。 遊んでみて「斜めだけ速いのが気になる」と感じたら、そのときに直せば十分です。

maxmin で挟んで範囲に収める

self.boy_x = max(0, min(self.boy_x, SCREEN_WIDTH - BOY_SIZE))

内側から順に読みます。

  1. min(self.boy_x, 152) … 152 を超えていたら 152 にする(右端で止める
  2. max(0, ...) … 0 を下回っていたら 0 にする(左端で止める
そのときの self.boy_xmin(x, 152) の結果max(0, ...) の結果
−2(左へ出た)−20
76(画面内)7676
154(右へ出た)152152

if で書くこともできますが 4 行かかります。 max / min なら 1 行で、しかも「この変数は 0 〜 152 に収まる」と 一目で読み取れます。step0 で魚を画面の端で折り返させたときと同じ書き方です。

# if で書くとこうなる(動きは同じ)
if self.boy_x < 0:
    self.boy_x = 0
if self.boy_x > SCREEN_WIDTH - BOY_SIZE:
    self.boy_x = SCREEN_WIDTH - BOY_SIZE
押し戻しは if 4 つの「あと」に書きます 先に押し戻してから移動すると、そのフレームでまた画面の外へ出た状態draw() に進みます。 「動かしきってから、最後に範囲へ収める」という順番が大事です。

なぜ move_boy() に分けたのか

いまは update() の中身が 1 行だけなので、分けた意味が薄く見えるかもしれません。 これは先を見越した準備です。フェーズが進むと update() はこうなります。

    def update(self):
        self.move_boy()          # フェーズ 2
        self.check_candy()       # フェーズ 3
        self.move_ghosts()       # フェーズ 5
        self.check_caught()      # フェーズ 6

こうしておくと update() を読むだけで「毎フレーム何が起きるか」の一覧になり、 中身を知りたいときだけ下の定義を見に行けばよくなります。 関数を分ける基準は備忘録のこちらにまとめてあります。

blt() の引数を 2 行に折り返している

pyxel.blt(self.boy_x, self.boy_y, 0, 0, 0,
          BOY_SIZE, BOY_SIZE, pyxel.COLOR_BLACK)

() の中は、途中で改行してかまいません。 Python は閉じカッコが来るまで 1 つの式として読み続けるからです。 行が長くなって読みにくいときに使える書き方で、2 行目のインデントは自由ですが、 1 行目のカッコの中身に頭を揃えるのが一般的です。1 行のまま書いても問題ありません。

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

症状見るところ
キーを押しても動かない update()self.move_boy() を書いたか確認します。 pass だけが残っていると、move_boy() は定義されていても呼ばれません
AttributeError: 'App' object has no attribute 'boy_x' __init__self.boy_x = ... を書き忘れているか、 pyxel.run() よりあとに書いています。run() はいちばん最後です
男の子が画面の中央にいない - BOY_SIZE // 2 を引いているか確認します。 引いていないと右下に 4 ドットずれます
斜めに動かない(← と ↑ の同時押しで横にしか進まない) elif になっています。4 つとも if にします
押しっぱなしでも 1 回しか進まない btnp() になっています。移動は btn() です
端で止まらず画面の外へ出る max / min の 2 行が move_boy() の中にあるか、 また 4 つの if より下にあるかを確認します
右端や下端で絵が少しはみ出す 上限が SCREEN_WIDTH / SCREEN_HEIGHT のままになっています。 - BOY_SIZE を引きます
動きが速すぎる/遅すぎる BOY_SPEED の数字を変えます。1・2・3 のうち好みのものを選んでください

7. 次のフェーズ

操作できるキャラクターができました。次はお菓子を 1 個置き、重なったら消して次の場所に出します。 この step の山場である当たり判定と、置き場所を決めるための random が登場します。 step1 概要の「土台 1:当たり判定は横と縦に分けて考える」を 先に読み返しておくと、すんなり入れるはずです。