タートルグラフィックスで学ぶ Python のキホン 第 12 章
教材コード
第 12 章

ゲームをつくる

キーボード,マウス,当たり判定.ここまでの全部を使って,遊べるものを作ります.

ここまでのプログラムは,どれもこちらから一方的に命令するものでした. 実行すれば決まった絵が出て,決まったところで終わります. 今回は,人の操作に反応するプログラムを作ります. キーを押すと自機が動き,敵に当たると点が入り,失敗が続くと終わる — ゲームです.

新しく覚えることは多くありません. 「キーが押されたら,この関数を呼んでください」と登録する命令と, 「2 つのかめが近いかどうか」を調べる命令.この 2 つだけです. あとは第 10 章の ontimerdef,第 5 章の if, 第 3 章の変数 — これまで習ったものの組み合わせです. defglobal は,第 10 章と同じく 決まり文句のままで作れます(仕組みは第 13 章でくわしく習います).

この章で作るもの.最後に作るのは,次のような画面のシューティングゲームです. で下の自機を動かし,上から落ちてくる玉を受け止めます. 取れば左上のスコアが増え,3 回にがすとゲームオーバーです. 12.7 節で,これを5 段階に分けて少しずつ作ります.

図 12.1 この章で作るゲーム.左右キーで自機を動かし,落ちてくる玉を受け止める(掲載のプログラムを動かしたもの).
この章でできるようになること
  • ゲームの骨組み(準備 → 毎フレームの処理 → 終了判定)を自分で書ける
  • onkeypress でキーボードから,onscreenclickonclick でマウスから操作を受け取れる
  • ontimer でゲームループを作れる(while True は使えないことがわかる)
  • distance や座標の範囲で当たり判定を書ける
  • スコアを画面に出し,条件を満たしたらゲームを終われる
  • 自分で考えた小さなゲームを,最後まで組み立てられる
今回の進め方 この章の図はその場で動きます. 載せているプログラムをそのまま動かして録画したものなので, 絵と本文の説明がずれることはありません. 自分で操作して遊ぶには, 「▶ 演習環境で開く」から右側で実行してください.
  • キーが効かないときは,一度タートル画面をクリックしてください.コード欄に文字カーソルがあると,キーはコード欄に取られます.
  • 止めたいときは「■ 停止」を押します.押さないと動き続けます.
  • 速さの設定は,今回のように ontimer で動かすプログラムには効きません.速さを変えたいときは ontimerミリ秒の数を変えます.

12.1 ゲームの骨組み

どんなゲームも,中身は次の 3 つに分かれます. シューティングも,パズルも,レースゲームも,大きさが違うだけで形は同じです.

  1. 準備 — 自機や敵を作り,位置と色を決める.得点を 0 にする.1 回だけ行う.
  2. 毎フレームの処理 — 少し動かす,入力を見る,当たったか調べる,得点を書き直す.1 秒に何十回も繰り返す.
  3. 終了判定 — 続けるか,終わるかを決める.終わるなら繰り返しをやめる.
1. 準備(1 回だけ)2. 毎フレームの処理3. 終了判定おわりontimer でもう一度
図 12.2 ゲームの骨組み.2 と 3 をくり返し,終了条件を満たしたら 3 で抜ける.

12.1.1 なぜ while True: が使えないのか

紙の教科書に載っているゲームのプログラムは,たいてい次のような形をしています. 「ずっと繰り返す」という意味の while True: です.

ほかの教科書によくある書き方
while True:        # ← このページでは使えません
    # 少し待つ
    # 少し動かす
    # 画面を描き直す
ブラウザでは「待ち続ける」ことができない この教材はブラウザの中で Python を動かしています.ブラウザは, 1 つのプログラムが動いている間,画面の描き直しもキー入力の受け取りもできません. そのため while True: で待ち続けると,絵は出ず,キーも効かず, ページ全体が固まってしまいます. この教材では,しばらく待っても終わらないプログラムは 実行中断 という表示を出して自動的に止めます.

12.1.2 ontimer でループを作る(第 10 章の復習)

かわりに使うのが ontimer(関数, ミリ秒) です. 「指定したミリ秒だけ経ったら,この関数を 1 回呼んでください」と ブラウザにお願いする命令です.お願いしたらプログラムはいったん終わります. ブラウザは空いた時間で画面を描き直し,キー入力を受け取り,時間が来たら関数を呼びます.

1 回しか呼ばれないので,呼ばれた関数の最後で,もう一度 ontimer を書く のがコツです.これで「少し動く → 少し待つ → また少し動く」が続きます.

game_loop.py
# ontimer でループを作る(第 10 章の復習)

from turtle import *

setup(620, 460)

t = Turtle()
t.shape("turtle")
t.color("seagreen")
t.pu()
t.goto(-280, 0)

# 1 コマぶんの処理
def tick():
    t.fd(6)
    if t.xcor() > 280:
        t.goto(-280, 0)
    ontimer(tick, 30)      # ← 次のコマを予約する(これが「くり返し」)

ontimer(tick, 30)          # ← 最初の 1 回

done()
ミリ秒のめやす ontimer(tick, 30)30 はミリ秒です. 1 秒 = 1000 ミリ秒なので,1 秒に約 33 回呼ばれます. 数を小さくすると速く,大きくするとゆっくりになります. 30 前後にしておくと,たいていのゲームでちょうどよい速さになります.
かっこを付けない ontimer(tick, 30)tickかっこが付いていない ことに注意してください.tick() と書くと「いま呼べ」という意味になり, tick と書くと「この関数そのもの」を指します. ここで渡したいのは関数そのものです.

12.2 キーボードで動かす

12.2.1 onkeypress と listen

キーボードも同じ考え方です.「このキーが押されたら,この関数を呼んでください」と 登録しておきます.登録する命令が onkeypress(関数, キーの名前) です.

書き方意味
onkeypress(go_left, "Left") キーが押されたら go_left() を呼ぶ
onkey(stop_it, "Left") キーが離されたstop_it() を呼ぶ
listen()「キーボードを聞く準備をしてください」の合図.登録をぜんぶ済ませたあとに 1 回書く
key_move.py
# 矢印キーでかめを動かす

from turtle import *

setup(620, 460)

t = Turtle()
t.shape("turtle")
t.color("royalblue")
t.pu()

def go_left():
    t.setx(t.xcor() - 20)

def go_right():
    t.setx(t.xcor() + 20)

def go_up():
    t.sety(t.ycor() + 20)

def go_down():
    t.sety(t.ycor() - 20)

# キーと関数を結び付ける
onkeypress(go_left, "Left")
onkeypress(go_right, "Right")
onkeypress(go_up, "Up")
onkeypress(go_down, "Down")
listen()

done()
← キーで左へ→ キーで右へ1 回押すと 20 だけ動く
図 12.3 キーを 1 回押すと,かめが 20 だけ動く.

setx(x) は x 座標だけを変える命令,sety(y) は y 座標だけを変える命令です. t.xcor() で「いまの x 座標」が取れるので, t.setx(t.xcor() - 20) は「いまより 20 だけ左へ」という意味になります.

12.2.2 使えるキーの名前

キーの名前は文字列で書きます.大文字・小文字の区別があるので気をつけてください.

キー書き方備考
↑ ↓ ← →"Up" "Down" "Left" "Right"先頭だけ大文字
スペース"space"すべて小文字.弾を撃つのによく使う
ふつうの文字"a" "z" "1"小文字で書く."A" は Shift を押したときの扱いになる
Enter"Return"
Esc"Escape"ゲームを止めるのに使える
キーは名前で書く キーを番号で指定する必要はありません. "Left" のように名前で書けば,そのまま通じます.

12.2.3 押しっぱなしになめらかに動かす

12.2.1 のプログラムは,キーを押しっぱなしにしても 「トン … トトトトト」とぎこちなく動きます. これはキーボードの「リピート」の仕組みのせいで,最初の 1 回と 2 回目の間に間が空くからです.

なめらかに動かす定石は,キーで直接動かさないことです. キーが押されたら「いま右向きに進みたい」という気持ちだけを変数に覚えさせ, 実際に動かすのは ontimer の中でやります. 離されたら気持ちを 0 に戻します.キーが離されたことは onkey で受け取れます.

key_smooth.py
# 押しっぱなしでなめらかに動かす

from turtle import *

setup(620, 460)

t = Turtle()
t.shape("turtle")
t.color("royalblue")
t.pu()

dx = 0      # 横方向の速さ
dy = 0      # たて方向の速さ

def press_left():
    global dx
    dx = -6

def press_right():
    global dx
    dx = 6

def press_up():
    global dy
    dy = 6

def press_down():
    global dy
    dy = -6

def release_x():
    global dx
    dx = 0

def release_y():
    global dy
    dy = 0

def tick():
    t.goto(t.xcor() + dx, t.ycor() + dy)
    ontimer(tick, 25)

onkeypress(press_left, "Left")
onkeypress(press_right, "Right")
onkeypress(press_up, "Up")
onkeypress(press_down, "Down")

onkey(release_x, "Left")
onkey(release_x, "Right")
onkey(release_y, "Up")
onkey(release_y, "Down")

listen()
ontimer(tick, 25)

done()
global を忘れない 関数の中で関数の外の変数を書き換えるときは, 先頭に global 変数名 と書きます.これを忘れると, 関数の中だけの別の変数ができてしまい,外の dx は 0 のままです. 読むだけなら global は要りませんtick の中の dx がそうです).書き換えるときだけ必要です. 第 10 章と同じ決まり文句です. なぜ関数の中と外で変数が別ものになるのかは,第 13 章でくわしく習います.
同時押しもできる この書き方なら同時押しにも対応できます. ← と ↑ を同時に押せば dx = -6dy = 6 の両方が立ち, ななめに動きます.
虫食い虫食い 12-A スペースキーで色を変える

矢印キーの左右で動き,スペースキーを押すと色が変わるプログラムです. ____ を埋めて完成させてください. スペースキーの名前は表のとおりです.関数を渡すときはかっこを付けません

fill10_key.py
from turtle import *

setup(620, 460)

t = Turtle()
t.shape("circle")
t.color("royalblue")
t.pu()

def go_left():
    t.setx(t.xcor() - 20)

def go_right():
    t.setx(t.xcor() + 20)

def change_color():
    t.color("orange")

onkeypress(go_left, "____")
onkeypress(____, "Right")
onkeypress(change_color, "____")
____()

done()

12.3 マウスを使う

12.3.1 クリックした場所を受け取る

onscreenclick(関数) と登録しておくと,画面がクリックされたときに その関数が呼ばれます.このとき,クリックされた場所の座標が 2 つ渡ってきます. 受け取る側は,def のかっこの中に 名前を 2 つ書いておきます. こう書いておくと,呼ばれたときにその名前へ値が入ってきます. かっこの中に並べるこの名前を引数といいます. いまは決まり文句で,仕組みは第 13 章でくわしく習います.

受け取り方の形
def jump(x, y):        # ← x と y の 2 つを受け取る約束
    t.goto(x, y)

onscreenclick(jump)    # ← ここでもかっこは付けない
mouse_click.py
# クリックした所へかめが飛ぶ

from turtle import *

setup(620, 460)

t = Turtle()
t.shape("circle")
t.color("tomato")
t.pu()

def jump(x, y):
    t.goto(x, y)
    print("クリック", round(x), round(y))

onscreenclick(jump)

done()
ヒント 渡ってくる座標はタートルの座標です.画面の中央が (0, 0), 右が x の正の向き,上が y の正の向きになっています. 620 × 460 の画面なら,x は約 −310 〜 310,y は約 −230 〜 230 です. コンソールに出る数を見ながら,あちこちクリックして確かめてください.
ドラッグは扱いません 「いまマウスがどこにいるか」をいつでも聞くことはできません. クリックされたときだけ座標が届く方式です. ドラッグのかわりになる書き方は 12.3.3 節で扱います.

12.3.2 かめ自身をクリックする

画面全体ではなく,そのかめが押されたときだけ反応させたいこともあります. もぐらたたきや,ボタンのような使い方です.そのときは かめ.onclick(関数) と,かめに直接登録します.

turtle_click.py
# 押されたかめの色が変わる(もぐらたたき風)

from turtle import *

setup(620, 460)

a = Turtle()
a.shape("circle")
a.color("orange")
a.pu()
a.goto(-90, 0)

b = Turtle()
b.shape("square")
b.color("mediumpurple")
b.pu()
b.goto(90, 0)

def hit_a(x, y):
    a.color("gray")
    print("まるが押された")

def hit_b(x, y):
    b.color("gray")
    print("しかくが押された")

a.onclick(hit_a)
b.onclick(hit_b)

done()
押せる範囲はせまい かめをクリックしたと判定される範囲は,かめの中心から半径 14 のあたりです. shapesize で大きく見せても,押せる範囲は変わりません. 小さくて押しにくいときは,onscreenclick で座標を受け取り, かめ.distance(x, y) < 30 のように自分で判定してください (12.4 節でやります).
虫食い虫食い 12-B クリックした所に点を打つ

クリックするたびに,その場所に色つきの点を打つプログラムです. ____ を埋めてください. onscreenclick が呼ぶ関数は,引数を 2 つ受け取る約束でした.

fill10_click.py
from turtle import *

setup(620, 460)

pen = Turtle()
pen.ht()
pen.pu()
pen.color("mediumseagreen")

def put(____, ____):
    pen.goto(x, y)
    pen.dot(24)

onscreenclick(____)

done()

12.3.3 ドラッグのかわり:つかんで,置く

マウスで絵をドラッグして動かす—— 「ボタンを押したままマウスを動かす」という操作を考えてみます. ところがこの教材のランタイムは, クリックされた瞬間しか受け取れません. 「いまボタンが押されているか」も「いまマウスがどこにいるか」も分からないので, ドラッグはそのままでは作れません.

そこで「1 回目のクリックでつかむ → 2 回目のクリックで置く」方式に置き換えます. 操作は 2 回のクリックに分かれますが, できることはドラッグと同じ(好きなものを好きな所へ動かす)です.

やり方は,つかんでいる最中かどうかを変数に覚えさせるだけです. この変数をフラグ(旗)と呼びます.12.6 節でも同じ考え方が出てきます.

grab_and_place.py
# ドラッグのかわり:1 回目のクリックでつかみ,2 回目のクリックで置く

from turtle import *

setup(620, 460)

box = Turtle()
box.shape("square")
box.color("mediumpurple")
box.shapesize(2, 2)
box.pu()
box.goto(-120, 0)

msg = Turtle()
msg.ht()
msg.pu()
msg.color("dimgray")
msg.goto(0, 160)

holding = False          # つかんでいる最中かどうか(フラグ)

def show(text):
    msg.clear()
    msg.write(text, align="center", font=("Arial", 16, "normal"))

def click(x, y):
    global holding
    if holding:
        box.goto(x, y)               # 2 回目:ここに置く
        box.color("mediumpurple")
        holding = False
        show("置きました.つかむには四角をクリック")
    elif box.distance(x, y) < 30:    # 四角の近くが押された
        box.color("crimson")         # つかんだ印に色を変える
        holding = True
        show("つかみました.置きたい所をクリック")

show("四角をクリックしてつかむ")
onscreenclick(click)

done()
1 回目のクリックでつかみ,2 回目で置くもとの場所つかんでいる(赤)
図 12.4 つかんでいる間は色を変えておくと,いまどちらの状態かが目で分かる.
同じ操作でも,状態で意味が変わる holdingTrueFalse かで, 同じクリックの意味が変わるのがこの書き方のポイントです. if の順番にも意味があります. 先に「つかんでいるか」を調べるので,つかんでいる間はどこを押しても「置く」になります.

12.4 当たり判定

ゲームらしさの正体は当たり判定です. 弾が敵に当たった,自機が壁にぶつかった,アイテムを拾った — すべて 「2 つのものが近いかどうか」を調べているだけです.

12.4.1 距離で判定する

t.distance(u) と書くと,かめ t とかめ u中心どうしの距離が返ってきます. これがある値より小さければ「当たった」とみなします.

円で当たりを見る
if t.distance(u) < 30:
    print("当たった!")
tut.distance(u)r
図 12.5 距離が r より小さければ「当たった」とみなす.

判定に使う数(上の図の r)は,見た目の大きさに合わせて自分で決めます. 小さくすると「かすっても当たらない」きびしいゲームに, 大きくすると「近づけば当たる」やさしいゲームになります. 20〜30 くらいから試して,遊びながら調整してください.

hit_distance.py
# クリックで丸を動かし,四角に近づくと色が変わる

from turtle import *

setup(620, 460)

a = Turtle()
a.shape("circle")
a.color("royalblue")
a.pu()
a.goto(-150, -80)

b = Turtle()
b.shape("square")
b.color("seagreen")
b.pu()
b.goto(100, 40)

def move(x, y):
    a.goto(x, y)
    if a.distance(b) < 30:
        b.color("crimson")
        print("当たった! 距離", round(a.distance(b)))
    else:
        b.color("seagreen")
        print("まだ遠い.距離", round(a.distance(b)))

onscreenclick(move)

done()
distance が 30 より小さいと当たり当たった(赤)まだ遠い
図 12.6 上のプログラムを動かしたところ.丸を近づけると四角が赤くなる.
座標との当たり判定 t.distance(x, y) のように座標を直接わたすこともできます. onscreenclick で受け取った座標がかめに当たっているかを調べるときに使います.

12.4.2 座標の範囲で判定する

四角い的や,床・ゴールのような広い範囲を判定したいときは, 距離より「x がいくつからいくつの間で,かつ y がいくつからいくつの間」と書くほうが自然です. 2 つの条件を and でつなぎます.

範囲で当たりを見る
x = t.xcor()
y = t.ycor()

if -100 < x and x < 100 and 50 < y and y < 150:
    print("四角いゾーンの中にいます")
ヒント Python では -100 < x < 100まとめて書くこともできます. 数学の書き方そのままです.上の ifif -100 < x < 100 and 50 < y < 150: と書けます.
-100 < x < 10050 < y < 150中(当たり)外(はずれ)
図 12.7 x と y の範囲の両方に入っていれば「中」.

12.4.3 画面の端の判定と跳ね返り

画面からはみ出さないようにする,あるいは端でボールを跳ね返す — これも当たり判定です. 620 × 460 の画面なら,画面の端そのものは x が ±310,y が ±230 です. かめには大きさがあるので,少し内側の ±300・±215 あたりで折り返すと ちょうどよく見えます.

跳ね返らせる方法はとても簡単で, 進む向きの符号を反対にするだけです. 右へ 7 ずつ進んでいた(dx = 7)なら,dx = -dx とすれば 左へ 7 ずつ進むようになります.

ここで dx = -dxx = -300x = 300
図 12.8 右の壁に当たったら x の向きだけ反対にする.y の向きはそのまま.
bounce.py
# 画面の端ではね返るボール

from turtle import *

setup(620, 460)
bgcolor("#101820")

ball = Turtle()
ball.shape("circle")
ball.color("gold")
ball.pu()

dx = 7      # 横に進む量
dy = 5      # たてに進む量

def tick():
    global dx, dy
    ball.goto(ball.xcor() + dx, ball.ycor() + dy)

    # 左右の壁
    if ball.xcor() > 300 or ball.xcor() < -300:
        dx = -dx

    # 上下の壁
    if ball.ycor() > 215 or ball.ycor() < -215:
        dy = -dy

    ontimer(tick, 25)

ontimer(tick, 25)

done()
dy = -dydx = -dxdy = -dy
図 12.9 上のプログラムを動かしたところ.ボールの通った跡を線で描き足してある.実際には線は残らない.
ヒント or は「どちらか一方でも成り立てば」という意味です. ball.xcor() > 300 or ball.xcor() < -300 は 「右の壁を越えたか,左の壁を越えたか」を 1 行で調べています.
虫食い虫食い 12-C 画面からはみ出さないようにする

左右のキーで動かせますが,このままでは画面の外まで行ってしまいます. x が −290 より小さくならず,290 より大きくならないようにしてください. ____ を埋めましょう.

ヒント if の後は「行きすぎていたら,行きすぎない位置に戻す」と考えます. 左へ行きすぎたら x を -290 に,右へ行きすぎたら x を 290 にします.
fill10_wall.py
from turtle import *

setup(620, 460)

t = Turtle()
t.shape("turtle")
t.color("royalblue")
t.pu()

def go_left():
    t.setx(t.xcor() - 20)
    if t.xcor() < ____:
        t.setx(____)

def go_right():
    t.setx(t.xcor() + 20)
    if t.xcor() ____ 290:
        t.setx(290)

onkeypress(go_left, "Left")
onkeypress(go_right, "Right")
listen()

done()

12.5 スコアを表示する

点数を画面に出すには,スコア専用のかめを 1 匹用意します. このかめは絵を描かず,文字を書くためだけに使います. 姿は ht() で消し,ペンは pu() で上げておきます.

点が増えたら,clear() で前の文字を消して, write() で新しい文字を書きます.この 2 行がセットです.

スコア板の作り方
board = Turtle()
board.ht()                 # 姿を消す
board.pu()                 # 線を引かない
board.goto(-290, 190)      # 左上に置く

def show_score():
    board.clear()                                   # 前の文字を消す
    board.write("スコア " + str(score),
                font=("Arial", 18, "normal"))       # 新しく書く
数を文字にする str() write に渡せるのは文字列です. score は数なので,str(score) で文字列に直してから + でつなぎます."スコア " + str(3)"スコア 3" になります.
scoreboard.py
# スコア板:スペースキーを押すと点が増える

from turtle import *

setup(620, 460)

board = Turtle()
board.ht()
board.pu()
board.color("navy")
board.goto(-290, 190)

score = 0

def show_score():
    board.clear()
    board.write("スコア " + str(score), font=("Arial", 20, "normal"))

def plus():
    global score
    score = score + 1
    show_score()

show_score()
onkeypress(plus, "space")
listen()

done()
スコア 12← ここを clear() してから write() で書き直す
図 12.10 スコア板は画面の左上に置くことが多い.黄色いわくは説明のために描き足したもの.
clear() は画面ぜんぶを消す この教材の clear() は,画面に描かれた線と文字をすべて消します. かめ 1 匹ぶんだけを消すのではありません. ですからゲームでは,自機も敵も pu() で動かして線を残さない ようにしておきます.そうすれば,画面に残っている絵はスコアの文字だけになり, clear() しても困りません.
虫食い虫食い 12-D 残り時間を表示する

スペースキーを押すたびに,残りが 1 ずつ減っていく表示を作ります. ____ を埋めてください. 書き換えの 2 行(消す・書く)と,global を忘れずに.

fill10_score.py
from turtle import *

setup(620, 460)

board = Turtle()
board.ht()
board.pu()
board.color("crimson")
board.goto(-290, 190)

rest = 10

def show_rest():
    board.____()
    board.write("のこり " + ____(rest), font=("Arial", 20, "normal"))

def minus():
    ____ rest
    rest = rest - 1
    show_rest()

show_rest()
onkeypress(minus, "space")
listen()

done()

12.6 ゲームオーバーのつくり方

ゲームを止めるとは,次の ontimer を予約しないことです. 予約しなければ,それ以上 tick は呼ばれず,動きが止まります.

終わり方の定石
def tick():
    global miss

    # …ここで敵を動かしたり当たり判定をしたり…

    if miss >= 3:                        # 終わりの条件
        board.goto(-90, 0)
        board.write("ゲームオーバー", font=("Arial", 26, "bold"))
        return                           # ← 予約せずに関数を抜ける

    ontimer(tick, 30)                    # ← 続けるときだけ予約する

return は「この関数をここで終わりにする」という命令です. return より下は実行されないので,ontimer の予約も行われません. return にはもう 1 つ「計算した答えを持ち帰る」という大事な働きがあり, そちらは第 14 章でくわしく習います. ここでは「関数をここで打ち切る」という使い方だけを, 決まり文句として覚えてください.

もうひとつ,フラグ変数を使う書き方もあります. 「遊んでいる最中かどうか」を playing という変数に覚えさせ, キーの関数でもそれを見るようにします. こうすると,ゲームオーバーのあとにキーを押しても自機が動きません.

フラグ変数で止める
playing = True        # True なら遊んでいる最中

def go_left():
    if playing:                     # 遊んでいる最中だけ動く
        player.setx(player.xcor() - 20)

def tick():
    global playing
    # …
    if miss >= 3:
        playing = False             # 旗を下ろす
        return
    ontimer(tick, 30)
True と False True(真)と False(偽)は, 「はい・いいえ」を表す Python の値です.先頭が大文字であることに注意してください. if playing:if playing == True: と同じ意味ですが, 前者のほうがよく使われます.

12.7 シューティングゲームを 5 段階で作る

いよいよゲームを作ります.いきなり全部を書こうとしてはいけません. 動くものを少しずつ大きく育てていくのが,ゲーム作りの正しい進め方です. ここでは 5 段階に分けます.1 段階ごとに必ず実行して, 思ったとおりに動くことを確かめてから次へ進んでください. ⑤ まで進めば完成です.⑥ は,弾を撃てるようにする発展編です.

段階できること使うもの
自機を左右に動かすonkeypresslisten
敵が上から落ちてくるontimerrandom
当たったら敵が出直すdistance
取った数をスコアに出すスコア用のかめ,clearwrite
3 回取りそこねたら終わりフラグ変数,return
(発展)弾を撃って敵に当てるフラグ変数,htst

12.7.1 ① 自機を動かす

まず自機だけ.画面の下のほうに置いて,左右のキーで動かします. はみ出さないように maxmin で止めています. max(-290, x) は「−290 と x の大きいほう」という意味で, x が −290 より小さくなったら −290 になります.

game1_player.py
# シューティング ① 自機を動かす

from turtle import *

setup(620, 460)
bgcolor("#0d1117")

# --- 自機 ---
player = Turtle()
player.shape("triangle")
player.color("cyan")
player.pu()
player.goto(0, -170)

def go_left():
    player.setx(max(-290, player.xcor() - 20))

def go_right():
    player.setx(min(290, player.xcor() + 20))

onkeypress(go_left, "Left")
onkeypress(go_right, "Right")
listen()

done()
図 12.11 ① の画面.左右キーで自機だけが動く.

12.7.2 ② 敵を落とす

敵を 1 匹追加し,ontimer で少しずつ下げます. 下まで行ったら,また上のどこかに戻します. 横の位置は random.randint(-270, 270) で毎回変えると, それだけでゲームらしくなります.

game2_enemy.py
# シューティング ② 敵が落ちてくる

from turtle import *
import random

setup(620, 460)
bgcolor("#0d1117")

# --- 自機 ---
player = Turtle()
player.shape("triangle")
player.color("cyan")
player.pu()
player.goto(0, -170)

# --- 敵 ---
enemy = Turtle()
enemy.shape("circle")
enemy.color("orange")
enemy.pu()
enemy.goto(random.randint(-270, 270), 200)

def new_enemy():
    enemy.goto(random.randint(-270, 270), 200)

def go_left():
    player.setx(max(-290, player.xcor() - 20))

def go_right():
    player.setx(min(290, player.xcor() + 20))

def tick():
    enemy.sety(enemy.ycor() - 7)      # 敵を下げる
    if enemy.ycor() < -210:           # 下まで落ちたら
        new_enemy()                   # 上から出直す
    ontimer(tick, 30)

onkeypress(go_left, "Left")
onkeypress(go_right, "Right")
listen()
ontimer(tick, 30)

done()
図 12.12 ② の画面.玉が上から落ちてくる.まだ当たり判定は無い.

12.7.3 ③ 当たり判定を入れる

tick の中で,自機と敵の距離を調べます. 近ければ「取った」ことにして,敵を上から出直させます. ここでゲームとして成立します.必ず実行して,取れることを確かめてください.

game3_hit.py
# シューティング ③ 当たり判定

from turtle import *
import random

setup(620, 460)
bgcolor("#0d1117")

player = Turtle()
player.shape("triangle")
player.color("cyan")
player.pu()
player.goto(0, -170)

enemy = Turtle()
enemy.shape("circle")
enemy.color("orange")
enemy.pu()
enemy.goto(random.randint(-270, 270), 200)

def new_enemy():
    enemy.goto(random.randint(-270, 270), 200)

def go_left():
    player.setx(max(-290, player.xcor() - 20))

def go_right():
    player.setx(min(290, player.xcor() + 20))

def tick():
    enemy.sety(enemy.ycor() - 7)

    if player.distance(enemy) < 26:      # 取った
        print("取った!")
        new_enemy()
    elif enemy.ycor() < -210:            # 取りそこねた
        print("にがした…")
        new_enemy()

    ontimer(tick, 30)

onkeypress(go_left, "Left")
onkeypress(go_right, "Right")
listen()
ontimer(tick, 30)

done()
図 12.13 ③ の画面.玉に当たると,次の玉が上から出直す.
ヒント ifelif にしているのは, 「取った」と「にがした」が同時に起きないようにするためです. 両方 if で書くと,画面の下のほうで取ったときに 2 回ぶん処理されてしまうことがあります.

12.7.4 ④ スコアを付ける

スコア用のかめを足し,取った数を数えます. tick の中で score を書き換えるので global score が要ります.

game4_score.py
# シューティング ④ スコア

from turtle import *
import random

setup(620, 460)
bgcolor("#0d1117")

player = Turtle()
player.shape("triangle")
player.color("cyan")
player.pu()
player.goto(0, -170)

enemy = Turtle()
enemy.shape("circle")
enemy.color("orange")
enemy.pu()
enemy.goto(random.randint(-270, 270), 200)

# --- スコア板 ---
board = Turtle()
board.ht()
board.pu()
board.color("white")
board.goto(-290, 190)

score = 0
miss = 0

def show_score():
    board.clear()
    board.write("スコア " + str(score) + "  ミス " + str(miss),
                font=("Arial", 18, "normal"))

def new_enemy():
    enemy.goto(random.randint(-270, 270), 200)

def go_left():
    player.setx(max(-290, player.xcor() - 20))

def go_right():
    player.setx(min(290, player.xcor() + 20))

def tick():
    global score, miss

    enemy.sety(enemy.ycor() - 7)

    if player.distance(enemy) < 26:
        score = score + 1
        new_enemy()
    elif enemy.ycor() < -210:
        miss = miss + 1
        new_enemy()

    show_score()
    ontimer(tick, 30)

show_score()
onkeypress(go_left, "Left")
onkeypress(go_right, "Right")
listen()
ontimer(tick, 30)

done()
図 12.14 ④ の画面.取ればスコアが,にがせばミスが増える.

12.7.5 ⑤ ゲームオーバーを付ける

最後に終わりを付けます.ミスが 3 回たまったら, 大きな文字で「ゲームオーバー」と出して return で止めます. フラグ playing を下ろしておくと,そのあとキーを押しても自機は動きません. これで完成です.

game5_complete.py
# シューティング ⑤ 完成版
# 遊び方:← → で動かし,落ちてくる玉を取る.3 回にがすと終わり.

from turtle import *
import random

setup(620, 460)
bgcolor("#0d1117")

player = Turtle()
player.shape("triangle")
player.color("cyan")
player.pu()
player.goto(0, -170)

enemy = Turtle()
enemy.shape("circle")
enemy.color("orange")
enemy.pu()
enemy.goto(random.randint(-270, 270), 200)

board = Turtle()
board.ht()
board.pu()
board.color("white")
board.goto(-290, 190)

score = 0
miss = 0
playing = True

def show_score():
    board.clear()
    board.write("スコア " + str(score) + "  ミス " + str(miss),
                font=("Arial", 18, "normal"))

def new_enemy():
    enemy.goto(random.randint(-270, 270), 200)

def go_left():
    if playing:
        player.setx(max(-290, player.xcor() - 20))

def go_right():
    if playing:
        player.setx(min(290, player.xcor() + 20))

def tick():
    global score, miss, playing

    enemy.sety(enemy.ycor() - 7)

    if player.distance(enemy) < 26:
        score = score + 1
        new_enemy()
    elif enemy.ycor() < -210:
        miss = miss + 1
        new_enemy()

    show_score()

    if miss >= 3:                 # 終わりの条件
        playing = False
        board.goto(-120, 0)
        board.color("crimson")
        board.write("ゲームオーバー", font=("Arial", 28, "bold"))
        return                    # 予約せずに抜ける → 止まる

    ontimer(tick, 30)

show_score()
onkeypress(go_left, "Left")
onkeypress(go_right, "Right")
listen()
ontimer(tick, 30)

done()
図 12.15 ⑤ の画面.わざと取らずにいると,ミス 3 で止まる.
ここからが本番 ここから先は,自分の好きなように変えていけます. 敵をもう 1 匹増やす,速さを少しずつ上げる,弾を撃てるようにする, shapecolor を変えて見た目を作り込む — 1 つ変えるたびに実行することを守れば,必ず動くものができます.

12.7.6 もう一歩:弾を撃てるようにする

ここまでの 5 段階は,落ちてくる敵を受け止めるゲームでした. 6 段階目として,敵が横に動き,自機から弾を撃って当てる シューティングも作ってみましょう.新しく覚えることはありません. いままで使ったものの組み合わせです.

  • 敵は横に動くenemy.setx(enemy.xcor() + 4).右端まで行ったら左端から出直す
  • 弾は 1 発だけ — 飛んでいる最中かどうかを flying というフラグに覚えさせ,飛んでいないときだけ撃てるようにする
  • 弾は上へ飛ぶbullet.sety(bullet.ycor() + 18)
  • 当たり判定は弾と敵の間bullet.distance(enemy) < 26
  • 使わない弾は消しておくbullet.ht() で姿を消し,撃つときに bullet.st() で出す
game6_shoot.py
# シューティング ⑥ 弾を撃つ
# 遊び方:← → で動かし,スペースキーで弾を撃つ.横に動く敵に当てる.

from turtle import *
import random

setup(620, 460)
bgcolor("#0d1117")

player = Turtle()
player.shape("triangle")
player.color("cyan")
player.pu()
player.goto(0, -170)

enemy = Turtle()
enemy.shape("circle")
enemy.color("orange")
enemy.pu()
enemy.goto(-250, 150)

bullet = Turtle()
bullet.shape("circle")
bullet.color("gold")
bullet.shapesize(0.5, 0.5)
bullet.pu()
bullet.ht()                   # 撃つまでは見えない

board = Turtle()
board.ht()
board.pu()
board.color("white")
board.goto(-290, 190)

score = 0
flying = False                # 弾が飛んでいる最中かどうか

def show_score():
    board.clear()
    board.write("スコア " + str(score), font=("Arial", 18, "normal"))

def go_left():
    player.setx(max(-290, player.xcor() - 20))

def go_right():
    player.setx(min(290, player.xcor() + 20))

def shoot():
    global flying
    if not flying:                                  # 1 発ずつしか撃てない
        bullet.goto(player.xcor(), player.ycor() + 20)
        bullet.st()
        flying = True

def tick():
    global score, flying

    # 敵は右へ.右端まで行ったら左端から出直す
    enemy.setx(enemy.xcor() + 4)
    if enemy.xcor() > 300:
        enemy.goto(-300, random.randint(60, 190))

    # 弾は上へ飛ぶ
    if flying:
        bullet.sety(bullet.ycor() + 18)

        if bullet.distance(enemy) < 26:             # 当たった
            score = score + 1
            flying = False
            bullet.ht()
            enemy.goto(-300, random.randint(60, 190))
        elif bullet.ycor() > 230:                   # 画面の外へ出た
            flying = False
            bullet.ht()

    show_score()
    ontimer(tick, 30)

show_score()
onkeypress(go_left, "Left")
onkeypress(go_right, "Right")
onkeypress(shoot, "space")
listen()
ontimer(tick, 30)

done()
図 12.16 ⑥ の画面.スペースキーで弾を撃つ.
なぜフラグが要るのか flying のようなフラグを使わずに書くと, スペースキーを押しっぱなしにしたときに弾が毎コマ出直してしまい, 弾がその場に止まって見えます. 「いま飛んでいるか」を覚えておくことが大事です.
音は扱いません この教材のランタイムは音に対応していないので,音は扱いません. そのぶん,色・大きさ・動きで表現を工夫してください.

12.8 ゲームで役に立つ文法

ゲームを作っていると出てくる,ここまでで説明しそびれた書き方を 4 つ紹介します. どれも短いものです.読めるようになっておけば十分です.

12.8.1 break — 繰り返しを途中でやめる

for の繰り返しの途中で break を実行すると, その場で繰り返しから抜けます.「見つかったらもう探さなくてよい」ときに使います.

break
for i in range(10):
    if i == 4:
        break          # ここで for から抜ける
    print(i)           # 0 1 2 3 だけが出る
break は控えめに break は「入口ひとつ・出口ひとつ」というプログラムの作法をくずすので, 使いすぎないほうがよいと言われます. playing のようなフラグ変数で制御すれば, break を使わずに同じことが書けます(12.6 節でやったとおりです).

12.8.2 continue — この章だけ飛ばす

continue は,繰り返しを抜けるのではなく その章の残りを飛ばして次の回へ進みます.

continue
for i in range(6):
    if i % 2 == 0:
        continue       # 偶数のときは下を飛ばす
    print(i)           # 1 3 5 だけが出る

12.8.3 not — 真偽をひっくり返す

not は「〜でなければ」という意味です. not TrueFalsenot FalseTrue になります.

not
playing = False

if not playing:                 # 「遊んでいる最中でなければ」
    print("ゲームは止まっています")
ヒント not playingplaying == False と同じ意味です. どちらでもかまいませんが,not のほうが短く,英語の文として読めます.

12.8.4 and と or — 条件を組み合わせる

条件は組み合わせられます. and は「両方とも成り立つ」, or は「どちらか一方でも成り立つ」です.

書き方意味ゲームでの使いどころ
A and BA も B も成り立つとき「四角いゾーンの中」「自機が生きていて,かつ当たった」
A or BA か B の少なくとも一方が成り立つとき「左の壁か右の壁に当たった」
not AA が成り立たないとき「ゲーム中でなければ操作を受け付けない」
logic.py
# break / continue / not / and / or をまとめて試す

# break:4 になったら繰り返しをやめる
for i in range(10):
    if i == 4:
        break
    print("break の例", i)

print("---")

# continue:偶数の回は飛ばす
for i in range(6):
    if i % 2 == 0:
        continue
    print("continue の例", i)

print("---")

# not:真偽をひっくり返す
playing = False
if not playing:
    print("ゲームは止まっています")

print("---")

# and と or
x = 120
y = 40
if x > 100 and y > 30:
    print("and:2 つとも成り立つ")
if x > 500 or y > 30:
    print("or:どちらか一方でも成り立つ")
虫食い虫食い 12-E ゲーム中だけ動かす

ゲームが止まっているときは自機が動かないようにします. ____ を埋めてください. スペースキーで止める/再開する仕組みです. 「止まっていれば再開,動いていれば停止」は not で書けます.

fill10_flag.py
from turtle import *

setup(620, 460)

t = Turtle()
t.shape("turtle")
t.color("royalblue")
t.pu()

playing = True

def go_left():
    if ____:                 # 遊んでいる最中だけ動く
        t.setx(t.xcor() - 20)

def go_right():
    if playing:
        t.setx(t.xcor() + 20)

def toggle():
    ____ playing
    playing = ____ playing   # True と False を入れかえる
    print("playing =", playing)

onkeypress(go_left, "Left")
onkeypress(go_right, "Right")
onkeypress(toggle, "____")
listen()

done()

12.9 この章で出てきた命令

命令意味
onkeypress(関数, "Left")そのキーが押されたときに関数を呼ぶ.かっこを付けずに関数名だけ書く
onkey(関数, "Left")そのキーが離されたときに関数を呼ぶ
listen()キーボードを聞く準備をする.登録のあとに 1 回書く
onscreenclick(関数)画面がクリックされたら関数を呼ぶ.関数は (x, y) を受け取る
かめ.onclick(関数)そのかめがクリックされたら関数を呼ぶ.押せる範囲は中心から半径 14 ほど
ontimer(関数, ミリ秒)指定した時間のあとに関数を 1 回呼ぶ.関数の最後で書き直すとループになる
かめ.distance(相手)相手のかめまでの距離.< 30 などで当たり判定に使う
かめ.distance(x, y)座標までの距離
かめ.setx(x) / かめ.sety(y)x 座標だけ/y 座標だけを変える
かめ.xcor() / かめ.ycor()いまの x 座標/y 座標
かめ.clear()描いた線と文字を消す(画面ぜんぶが対象)
かめ.write(文字, font=(…))その位置に文字を書く
かめ.shape("circle")姿を変える.classic arrow square triangle circle turtle blank
bgcolor(色)画面の背景色を変える
setup(横, たて)画面の大きさを決める
random.randint(a, b)a 以上 b 以下の整数をでたらめに 1 つ.import random が要る
global 変数名関数の中から,外の変数を書き換えるときに書く
return関数をその場で終わりにする
not / and / or条件をひっくり返す/両方/どちらか
break / continue繰り返しを抜ける/その章を飛ばす
この教材でできないこと
  • while True: は使えません.ループは ontimer で作ります.
  • 画像を貼り付けた見た目は使えません.shapecolorshapesize で作ります.
  • 音は出せません.
  • マウスの「いまの位置」やドラッグは取れません.クリックされたときだけ座標が届きます.
  • 右クリック・ダブルクリックの区別もできません.クリックは 1 種類だけです.

演習

以下の演習には,出発点となるプログラムが入っています. 「▶ この演習を開く」で右側に読み込み,それを発展させて作品にしてください. できあがったら「⤓ 保存」でファイルに保存して,手元に残しておきましょう.

ヒント 今回の演習はすべて動き続けるプログラムです. 書いたら必ず実行し,止めたいときは「■ 停止」を押してください. キーが効かないときは,一度タートル画面をクリックしてください.
演習演習 12-1 プレイヤーを操作するプログラムをアレンジする

矢印キーで上下左右に動かせるプログラムを与えます. これをもとに,自分なりの工夫を加えてください.

  • 画面からはみ出さないようにする(12.4.3 節)
  • shapecolorshapesize で見た目を作る
  • スペースキーなど,矢印以外のキーにも働きを付ける
  • 動いた跡を線で残す(pd() にしてみる)と,お絵かきソフトになります
演習演習 12-2 つかんで置くプログラムをアレンジする

図形をマウスで動かすプログラムを与えます. このランタイムはドラッグに対応していない(ボタンを押したままの状態も, いまのマウスの位置も取れない)ので, 1 回目のクリックでつかみ,2 回目のクリックで置く方式にしてあります. できることはドラッグと同じです.これを発展させてください.

  • つかんでいる間は色を変える・大きくするなど,いまつかんでいることが分かる印を付ける
  • 置ける場所を決めておき,そこに置けたら得点が入るようにする(並べかえパズル)
  • 動かせるものを増やす(elif を 1 つ足すだけです)
  • ontimer で,つかんでいないものが勝手に動くようにする(もぐらたたきになります)
  • つかむ判定の 30 を変えて,つかみやすさを調整する
ヒント held にはかめそのものを入れています. 何もつかんでいないことを表すのに None(「なし」を表す Python の値)を使い, if held is not None: で「何かつかんでいるか」を調べています.
演習演習 12-3 得点板のプログラムをアレンジする

アイテムを拾うと点が増えるプログラムを与えます. 自機を動かしてアイテムに触れると,アイテムが別の場所に現れ,スコアが 1 増えます. これを発展させてください.

  • 制限時間を付ける(ontimer が呼ばれた回数を数える)
  • アイテムの種類を増やし,点の高いアイテムを作る
  • 取るとアイテムが速くなる/自機が遅くなる
  • スコアの表示を大きくする,色を変える,画面の中央に出す
演習演習 12-4 よけるゲームを作る

こんどは取るのではなくよけるゲームです. 落ちてくる岩に当たったら終わり,よけた数がスコアになります. 出発点を与えるので,完成させて発展させてください.

  • ____ の部分ではなく,まだ書かれていない当たり判定とゲームオーバーを自分で足します
  • 岩を 2 個・3 個に増やす
  • よけた数が増えるほど岩を速くする(speed_y を少しずつ大きくする)
  • 当たったときに色を変える,画面を赤くするなどの演出を付ける
演習演習 12-5 2 人で遊べるようにする

キーを分ければ,1 台のパソコンで 2 人遊べます. 左のプレイヤーは az,右のプレイヤーは で動かす「テニス」の出発点を与えます. ボールがラケットに当たると跳ね返り,後ろの壁に当たると相手の得点です.

  • 得点の表示を,左右それぞれに分けて出す
  • ボールが跳ね返るたびに少し速くする
  • 5 点先取で「勝ち」と出して止める
  • ラケットの長さ(shapesize と判定の数)を変えて難しさを調整する
自由演習 12-6 オリジナルゲームを作る

自由な発想で,自分のゲームを作ってください. 12.7 節の 5 段階をそのまま真似て,①から順に少しずつ育ててください. いきなり完成形を書こうとすると,まず動きません. 弾を撃つゲームにしたいときは,12.7.6 節の ⑥ が出発点になります.

作品の要件(次の 4 つを満たしていること)

  • 操作できること — キーボードかマウスで,遊ぶ人が何かを動かせる
  • 当たり判定があることdistance か座標の範囲で,ものとものが当たったかを判定している
  • スコアか終了があること — 点数を画面に出すか,条件を満たしたら終わる(両方あればなおよい)
  • コメントで題名と氏名を書くこと — 先頭に題名・氏名・日付と,遊び方を書く
ヒント 題材のヒント:よける/取る/当てる/逃げる/追いかける/積む/ もぐらたたき/リアクションの速さをはかる/迷路を抜ける/2 人対戦. 凝ったものでなくてかまいません.最後まで動くことのほうが大事です.
動かなくなったら 行き詰まったら,いちばん最後に足した 1 行を消してください. そこまでは動いていたはずです. それでも直らないときは,コンソールのエラーの行番号を見てください.

まとめ

  • ゲームは準備 → 毎フレームの処理 → 終了判定の 3 つでできている.
  • ブラウザでは while True: で待てない.ループは ontimer(関数, ミリ秒)関数の最後で書き直して作る.
  • キーは onkeypress(関数, "Left") で登録し,最後に listen().なめらかに動かしたいときは,キーで速さの変数だけ変えて,動かすのは ontimer の中でやる.
  • マウスは onscreenclick(関数) で座標を受け取る.かめ自身を押させたいときは かめ.onclick(関数).ドラッグは使えないので,つかむ → 置くの 2 回クリックに置き換える.
  • 当たり判定は distance による円か,x と y の範囲.端で跳ね返らせるには進む量の符号を反対にする.
  • スコアは専用のかめを 1 匹用意し,clear()write() で書き直す.
  • 止めるときは,次の ontimer予約しないreturnフラグ変数を使う.
  • 関数の中から外の変数を書き換えるときは global が要る(第 10 章からの決まり文句.仕組みは第 13 章).
  • 作るときは少しずつ.1 つ足したら必ず実行

ここまでの 3 回で,defglobal決まり文句として使い続けてきました. tickgo_leftdraw_board — 気がつけば,ずいぶんたくさんの「名前の付いた中身」を書いています.

次の回では,その def の正体を習います. かっこの中に名前を並べておくと何ができるのか(引数), なぜ関数の中と外で変数が別ものになるのか(そして global は何をしているのか). 決まり文句の理由が分かると,今回のゲームの長いプログラムを 短く,読みやすく書き直せるようになります.

使える命令の一覧は付録「タートル命令リファレンス」(第 4 章)に, 白紙のエディタは付録「じゆうに書く」にあります. 思いついたことを,いつでも試してみてください.