フェーズ 4 スコアを表示する

集めた数を数える変数を用意し、画面に文字で描く

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

ゴール

お菓子を集めた数を数えて、画面の左上に SCORE 0 のように表示します。 書き足すのは全部で 4 行だけの、短いフェーズです。

新しく出てくるのは pyxel.text()f 文字列の 2 つです。

1. 考え方

1-1. 数えるための変数を 1 つ増やす

「いくつ集めたか」も、男の子の位置と同じでフレームをまたいで覚えておきたい情報です。 なので __init__self.score = 0 と用意します。

1-2. 増やす場所は「取った瞬間」の 1 か所だけ

フェーズ 3 で書いた check_candy()if の中が、 ちょうど「いま取った」瞬間です。ここに 1 行足すだけで数えられます。

update() move_boy() check_candy() if 重なっている: self.score += 1 self.place_candy() ここに 1 行足すだけ 「取った瞬間」は 1 フレームしかないので、ここで数えれば二重に数えることはない
お菓子を置きなおすのと同じ場所で、スコアも 1 増やす

1-3. 文字を描くのは pyxel.text()

pyxel.text(x, y, 文字列, 色番号)

blt() と同じで、xy文字の左上の位置です。 色は pyxel.COLOR_WHITE(= 7)のような色番号を渡します。

1-4. 数値はそのままでは描けない

self.score数値ですが、pyxel.text() に渡せるのは文字列です。 そこで、数値を文字列の中に埋め込みます。それが f 文字列です。

self.score = 3

f"SCORE {self.score}"      # → "SCORE 3" という文字列になる

文字列の前に f をつけると、{ } の中身がその場で計算されて 文字に置きかわりますf を書き忘れると、 {self.score} がそのまま文字として表示されてしまいます。

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

機能役割
pyxel.text(x, y, s, col)文字列 s(x, y) に色 col で描く
pyxel.COLOR_WHITE白(色番号 7)。7 と書いても同じ
f"... {変数} ..."文字列の中に変数の中身を埋め込む(f 文字列)
+= 1いまの値に 1 を足して入れなおす

3. 手順

3-1. スコアの変数を用意する

__init__ に 1 行足します。

        self.boy_x = SCREEN_WIDTH // 2 - BOY_SIZE // 2
        self.boy_y = SCREEN_HEIGHT // 2 - BOY_SIZE // 2
        self.score = 0                                      # ← 足す
        self.place_candy()

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

3-2. 取ったときに 1 増やす

check_candy() に 1 行足します。

    def check_candy(self):
        """お菓子に重なったら、スコアを増やして次の場所へ置きなおす"""
        if is_hit(self.boy_x, self.boy_y, BOY_SIZE,
                  self.candy_x, self.candy_y, CANDY_SIZE):
            self.score += 1                                 # ← 足す
            self.place_candy()

3-3. 画面に描く

draw()いちばん最後に 1 行足します。

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

        pyxel.blt(self.candy_x, self.candy_y, 0, 16, 0,
                  CANDY_SIZE, CANDY_SIZE, pyxel.COLOR_BLACK)    # お菓子
        pyxel.blt(self.boy_x, self.boy_y, 0, 0, 0,
                  BOY_SIZE, BOY_SIZE, pyxel.COLOR_BLACK)        # 男の子

        pyxel.text(4, 4, f"SCORE {self.score}", pyxel.COLOR_WHITE)   # ← 足す

実行して、左上に SCORE 0 が出て、お菓子を取るたびに数字が増えれば完成です。

1 行目のコメントも書きかえておきましょう # step1 フェーズ3:… のままになっているはずです。 毎フェーズの最後にここを直す習慣にしておくと、あとで見返したときに助かります。

4. できあがりのコード

変わったのは 4 行だけですが、全体を載せておきます。

# candy_hunt.py
# step1 フェーズ4:集めた数をスコアとして表示する

import random

import pyxel

SCREEN_WIDTH = 160
SCREEN_HEIGHT = 120

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

CANDY_SIZE = 8          # お菓子の絵の大きさ


def is_hit(x1, y1, size1, x2, y2, size2):
    """2 つの四角が重なっていれば True を返す"""
    return (x1 < x2 + size2 and x2 < x1 + size1 and
            y1 < y2 + size2 and y2 < y1 + size1)


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
        self.score = 0
        self.place_candy()

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

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

    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 place_candy(self):
        """男の子と重ならない場所へ、お菓子を置きなおす"""
        while True:
            self.candy_x = random.randint(0, SCREEN_WIDTH - CANDY_SIZE)
            self.candy_y = random.randint(0, SCREEN_HEIGHT - CANDY_SIZE)
            if not is_hit(self.boy_x, self.boy_y, BOY_SIZE,
                          self.candy_x, self.candy_y, CANDY_SIZE):
                break

    def check_candy(self):
        """お菓子に重なったら、スコアを増やして次の場所へ置きなおす"""
        if is_hit(self.boy_x, self.boy_y, BOY_SIZE,
                  self.candy_x, self.candy_y, CANDY_SIZE):
            self.score += 1
            self.place_candy()

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

        pyxel.blt(self.candy_x, self.candy_y, 0, 16, 0,
                  CANDY_SIZE, CANDY_SIZE, pyxel.COLOR_BLACK)    # お菓子
        pyxel.blt(self.boy_x, self.boy_y, 0, 0, 0,
                  BOY_SIZE, BOY_SIZE, pyxel.COLOR_BLACK)        # 男の子

        pyxel.text(4, 4, f"SCORE {self.score}", pyxel.COLOR_WHITE)

App()

5. コードの解説

f 文字列(f-string)

数値を文字列にまぜる方法はいくつかありますが、いまの Python では f 文字列がいちばん素直です。

書き方結果ひとこと
f"SCORE {self.score}"SCORE 3これを使う
"SCORE " + str(self.score)SCORE 3同じ結果。長くなる
"SCORE " + self.scoreTypeError。文字列と数値は + でつなげない
"SCORE {self.score}"SCORE {self.score}f を忘れた形。そのまま表示される
{ } の中には式が書けます 変数だけでなく、計算もそのまま書けます。
f"SCORE {self.score}"          # → SCORE 3
f"NEXT {5 - self.score % 5}"   # → 計算結果が入る
f"{self.score:3}"              # → 幅 3 で右ぞろえ("  3")
最後の :3 のような書き方は書式指定といって、 桁をそろえたいときに便利です。いまは使いませんが、頭の隅に置いておいてください。

self.score += 1 の意味

self.score += 1        # これは次と同じ意味
self.score = self.score + 1

「いまの値を読んで、1 足して、また入れなおす」という 3 段階を短く書いたものです。 -=(フェーズ 2 の self.boy_x -= BOY_SPEED)も同じ仲間です。

なぜ draw() ではなく check_candy() で増やすのか

draw() の中で self.score += 1 と書くと、 毎フレーム(1 秒に 30 ずつ)増えてしまいます。 フェーズ 2 で決めた「update() で決めて、draw() で描く」という 役割分担が、ここでも効いています。

「取った瞬間」は 1 フレームしかない 重なった次のフレームには place_candy() でお菓子が別の場所へ移っているので、 同じお菓子で 2 回数えることはありません。備忘録の 「1 回だけ処理する書き方」で扱った 「状態が変わった瞬間だけ処理する」のと似た形になっています。

文字はいちばん最後に描く

フェーズ 3 の「あとに描いたものが上に重なる」がここでも効きます。 pyxel.text()blt() よりに書くと、 お菓子や男の子がスコアの上を通ったときに文字が隠れてしまいます。

描く順番は 背景 → ゲームの中身 → 画面表示(スコアなど)が基本です。

ここで定数を作らなかった理由

これまで BOY_SPEEDCANDY_SIZE は定数にしてきましたが、 pyxel.text(4, 4, ...)4 はそのまま書いています。 基準は「同じ数字が 2 か所以上に出てくるか」です。

なんでも定数にすると、かえって「この名前は何だったか」と探す手間が増えます。 迷ったら、2 回目に同じ数字を書くときに定数へ格上げすると決めておけば十分です。

フォントの大きさ(実測)

Pyxel の標準フォントは1 文字 4 ドットの送りで、 実際に色が付くのは横 3 ドット、高さは大文字で 5 ドット、 gy のように下に伸びる文字を含めると 6 ドットです。

1 ドットを 14 倍に拡大したところ(pyxel.text(0, 0, "AB", 7) 文字 3 送り 4 高さ 5(下に伸びる文字を含めると 6) 文字列の幅(ドット) len(s) * 4 - 1 例:"SCORE 0" は 7 文字 → 27 ドット 中央にそろえるなら (160 - len(s) * 4) // 2
フォントの寸法。文字数から幅が計算できるので、中央ぞろえもできる(フェーズ 6 で使います)
標準フォントで日本語は出せません pyxel.text(4, 4, "スコア", 7) と書いても、何も描かれません (エラーにもなりません)。標準フォントは英数字と記号だけです。
日本語を出したい場合は、Pyxel に同梱されている日本語フォント (pyxel/examples/assets/umplus_j12r.bdf)を読み込む方法があります。
font = pyxel.Font("umplus_j12r.bdf")
pyxel.text(4, 4, "スコア 5", 7, font)
ただしフォントファイルを配布物に同梱する必要が出てくるので、 この step では英数字だけで作ります

6. もう一歩(やらなくても大丈夫)

お菓子や男の子がスコアの真下を通ると、白い文字が読みにくくなることがあります。 黒で 1 ドットずらして描いてから、白で本体を描くと、縁取りができて読みやすくなります。

        text = f"SCORE {self.score}"
        pyxel.text(5, 5, text, pyxel.COLOR_BLACK)      # 影
        pyxel.text(4, 4, text, pyxel.COLOR_WHITE)      # 本体

同じ文字列を 2 回書かずに済むよう、いったん text という変数に入れています。

いまの画面では効果が見えません 背景が黒(COLOR_BLACK)なので、黒い影は背景に溶けて見えません。 効果を確かめたいときは、一時的に pyxel.cls(pyxel.COLOR_NAVY)(濃紺)などに 変えてみてください。影が効いてくるのは明るい背景を敷いたときや、 文字の下を明るい絵が通るときです。 step1 は黒背景で進めるので、この飾りは入れなくても構いません

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

症状見るところ
SCORE {self.score} とそのまま表示される 文字列の前の f が抜けています
TypeError: can only concatenate str (not "int") to str "SCORE " + self.score と書いています。 f 文字列にするか、str(self.score) で文字列に変換します
AttributeError: 'App' object has no attribute 'score' __init__self.score = 0 を書き忘れているか、 pyxel.run() よりあとに書いています
スコアが猛烈な勢いで増える self.score += 1if の外、または draw() の中にあります。 check_candy()ifplace_candy() と同じ深さ)に置きます
スコアが増えない self.score += 1 ではなく self.score + 1 になっていませんか。 これは計算するだけで、結果をどこにも入れていません
文字が表示されない 色が背景と同じ黒(0)になっていないか確認します。 また、日本語を渡していると何も出ません
男の子がスコアの上に重なって文字が消える pyxel.text()blt() より前にあります。いちばん最後に描きます

8. 次のフェーズ

遊びの手ごたえが出てきました。次はいよいよお化けを 1 体出して、男の子を追いかけさせます。 「相手の座標と自分の座標を見くらべて、近づく向きへ動く」という、 追跡の基本の考え方が登場します。