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

ゲームをつくる

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

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

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

def と global は,第 10 章と同じく 決まり文句のままで作れます(仕組みは第 13 章でくわしく習います).

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

図 12.1 この章で作るゲーム.左右キーで自機を動かし,落ちてくる玉を受け止める(掲載のプログラムを動かしたもの).
この章でできるようになること
  • ゲームの骨組み(準備 → 毎フレームの処理 → 終了判定)を自分で書ける
  • onkeypress でキーボードから,onscreenclick や onclick でマウスから操作を受け取れる
  • 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 = -6 と dy = 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          # つかんでいる最中かどうか(フラグ)

# text に,表示したい文字が入ってくる(引数.第 13 章でくわしく習う)
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 つかんでいる間は色を変えておくと,いまどちらの状態かが目で分かる.
同じ操作でも,状態で意味が変わる holding が True か False かで, 同じクリックの意味が変わるのがこの書き方のポイントです. 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 とまとめて書くこともできます. 数学の書き方そのままです.上の if は if -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 段階ごとに必ず実行して, 思ったとおりに動くことを確かめてから次へ進んでください. ⑤ まで進めば完成です.⑥ は,弾を撃てるようにする発展編です.

段階できること使うもの
①自機を左右に動かすonkeypress,listen
②敵が上から落ちてくるontimer,random
③当たったら敵が出直すdistance
④取った数をスコアに出すスコア用のかめ,clear・write
⑤3 回取りそこねたら終わりフラグ変数,return
⑥(発展)弾を撃って敵に当てるフラグ変数,ht・st

12.7.1 ① 自機を動かす

まず自機だけ.画面の下のほうに置いて,左右のキーで動かします. はみ出さないように max と min で止めています. 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 ③ の画面.玉に当たると,次の玉が上から出直す.
ヒント if と elif にしているのは, 「取った」と「にがした」のどちらか一方しか起きないことを, 読む人にもはっきり示すためです. 「取った」ときは new_enemy() で敵が上に戻るので, 両方 if で書いても結果は同じですが, elif にしておけば,1 回の tick で両方を数えることが決して起きないと, 中身を追わなくても読むだけで分かります.

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 匹増やす,速さを少しずつ上げる,弾を撃てるようにする, shape と color を変えて見た目を作り込む — 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 True は False,not False は True になります.

not
playing = False

if not playing:                 # 「遊んでいる最中でなければ」
    print("ゲームは止まっています")
ヒント not playing は playing == 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 で作ります.
  • 画像を貼り付けた見た目は使えません.shape と color と shapesize で作ります.
  • 音は出せません.
  • マウスの「いまの位置」やドラッグは取れません.クリックされたときだけ座標が届きます.
  • 右クリック・ダブルクリックの区別もできません.クリックは 1 種類だけです.

演習

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

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

矢印キーで上下左右に 20 ずつ動かせるプログラムを与えます. これをもとに,次の要件を満たすお絵かきソフトにしてください. 線が見やすいように,shapesize(2, 2) で自機を大きくし, pensize(3) で線を太くしておきます(ここは準備です).

  • 画面からはみ出さないようにする.x は −290〜290,y は −210〜210 の中で止まるようにします(12.4.3 節)
  • onkeypress(関数, "space") を足し,スペースキーに働きを付ける
  • これもやりましょう:スペースキーを押すたびに,ペンの上げ下げが交互に切りかわるようにする.ペンを下ろしている間は色を "crimson" にして動いた跡を線で残し,上げている間は元の "royalblue" に戻して線を残さない.最初はペンを上げた状態です.いまペンを下ろしているかどうかを True/False の変数(フラグ)で覚えておき,not で切りかえます
評価の目安(10 段階)

必須(すべて満たして正しく動けば 8)

  1. 矢印キー 4 つで,これまでどおり上下左右に 20 ずつ動く
  2. 上下左右のどの向きでも画面からはみ出さない(x は −290〜290,y は −210〜210 の中で止まる)
  3. onkeypress(関数, "space") でスペースキーに関数を結び付けている
  4. スペースキーを押すたびに,ペンを下ろす(赤で線が残る)と上げる(青で線が残らない)が交互に切りかわる(フラグと not,global)

発展(必須に加えて 1 つ以上で 9,学んだ範囲を大きく超える作り込みで 10)

  • 12.2.3 節の書き方で,押しっぱなしでなめらかに動くようにする(キーでは速さの変数だけを変え,動かすのは ontimer の中)
  • 止まる範囲の端(290 と 210)を変数にまとめ,1 か所を変えるだけで動ける範囲が変わるようにする
  • 別のキー(たとえば c)で線を全部消し,そのあともフラグとペンの状態・色が食い違わないようにする

必須が 1〜2 つ足りない → 7 / 動かない・未着手が複数 → 6

演習演習 12-2 つかんで置くプログラムをアレンジする

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

  • つかんでいる間は,つかんだ図形を shapesize(2.6, 2.6) に大きくし,置いたら shapesize(2, 2) に戻す(いまつかんでいることが分かる印)
  • 画面の右上に,置き場所のわくを描く.わくは左下の角が (60, 60),横 200,縦 140 の四角(x は 60〜260,y は 60〜200)です.線を描くかめはスコア用とは別にします
  • 図形を置くたびに,わくの中にある図形の数を座標の範囲(60 < a.xcor() < 260 and 60 < a.ycor() < 200 のような条件)で数え直す
  • その数を,(−290, 190) に置いたスコア用のかめで「わくの中 ○ 個」と表示し,置くたびに clear() → write() で書き直す(最初は「わくの中 0 個」)
  • これもやりましょう:3 つともわくに入ったら,スコアの下 (−290, 150) に「クリア!」と表示する.1 つでもわくから出したら「クリア!」は消えるようにします
ヒント held にはかめそのものを入れています. 何もつかんでいないことを表すのに None(「なし」を表す Python の値)を使い, if held is not None: で「何かつかんでいるか」を調べています.
評価の目安(10 段階)

必須(すべて満たして正しく動けば 8)

  1. つかんでいる間は,つかんだ図形が 2.6 倍に大きくなり,置くと 2 倍に戻る
  2. x が 60〜260,y が 60〜200 のわくが画面に描かれている
  3. 置くたびに,わくの中にある図形の数を座標の範囲(and か < x <)で数え,「わくの中 ○ 個」を clear() → write() で書き直す(最初は「わくの中 0 個」)
  4. 3 つともわくに入ると「クリア!」と出て,1 つ出すと消える

発展(必須に加えて 1 つ以上で 9,学んだ範囲を大きく超える作り込みで 10)

  • わくを 2 つにし,図形ごとに入れるべきわくを決めて,正しいわくに入った数だけを数える(条件の組み合わせ)
  • ontimer で,つかんでいないものだけが勝手に動くようにする(held で動きを分ける)
  • クリアまでにクリックした回数を数えて表示し,クリアしたあとは数えないようにする

必須が 1〜2 つ足りない → 7 / 動かない・未着手が複数 → 6

演習演習 12-3 得点板のプログラムをアレンジする

アイテムを拾うと点が増えるプログラムを与えます. 自機を動かしてアイテムに触れると,アイテムが別の場所に現れ,スコアが 1 増えます. これを発展させて,次の要件を満たす30 秒の制限時間つきゲームにしてください.

  • 残り時間を表す変数 rest = 30 を作り,ontimer(tick, 1000) で 1 秒ごとに呼ばれる関数 tick の中で 1 ずつ減らす
  • スコアの表示を「スコア ○  のこり ○ 秒」に変え,アイテムを取ったときと 1 秒たつごとに書き直す
  • 残りが 0 になったら,画面の中央付近 (−110, 0) に「おしまい」と大きく表示し,次の ontimer を予約せずに return で止める
  • 終わったあとは,矢印キーを押しても自機が動かないようにする(フラグ変数 playing を True で始め,終わったら False にする)
  • これもやりましょう:終わりぎわを盛り上げるため,残り時間が 10 秒以下(rest が 10 のときを含む)になったら,金色のアイテムを取ったときの点を 2 点にする.残りが 11 秒以上のあいだは,これまでどおり 1 点です
評価の目安(10 段階)

必須(すべて満たして正しく動けば 8)

  1. ontimer(tick, 1000) で 1 秒ごとに残り時間が 1 ずつ減り,「スコア ○  のこり ○ 秒」が画面に表示される
  2. 残りが 0 になると「おしまい」と表示され,return で tick が止まる
  3. 終わったあとは,キーを押しても自機が動かない(フラグ変数 playing)
  4. 残りが 11 秒以上なら 1 点,10 秒以下(10 秒ちょうどを含む)なら 2 点入る

発展(必須に加えて 1 つ以上で 9,学んだ範囲を大きく超える作り込みで 10)

  • スコアに応じて自機の進む量やアイテムの動きを変え,取るほど難しくなるようにする
  • 終わったときに,スコアの範囲(境目を決めて if/elif)で「たいへんよくできました」などのことばを変える
  • 取ると点が減る「はずれ」のアイテムを足し,スコアが 0 より小さくならないようにする

必須が 1〜2 つ足りない → 7 / 動かない・未着手が複数 → 6

演習演習 12-4 よけるゲームを作る

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

出発点には当たり判定とゲームオーバーがまだ書かれていません. # ここに「岩に当たったら終わり」を書く の場所に,自分で足してください.

  • player.distance(rock) が 26 より小さくなったら「当たった」とする
  • 当たったら,画面の中央付近 (−120, 0) に「ゲームオーバー」と大きく表示し,次の ontimer を予約せずに return で止める
  • 止まったあとは,矢印キーを押しても自機が動かないようにする(フラグ変数 playing を True で始め,当たったら False にする)
  • これもやりましょう:だんだん難しくする.岩が下まで落ちて「よけた数」が 1 増えるたびに,落ちる速さ speed_y を 1 ずつ大きくする(最初は 8).ただし speed_y は 20 より大きくしない(20 になったら,それ以上よけても 20 のまま)
評価の目安(10 段階)

必須(すべて満たして正しく動けば 8)

  1. player.distance(rock) が 26 未満になると当たったと判定する
  2. 当たったら「ゲームオーバー」と画面に表示し,return で次の ontimer を予約せずに止める
  3. 止まったあとは,キーを押しても自機が動かない(フラグ変数 playing)
  4. よけるたびに speed_y が 1 ずつ大きくなり,20 で止まる(それ以上速くならない)

発展(必須に加えて 1 つ以上で 9,学んだ範囲を大きく超える作り込みで 10)

  • 速さの増え方(1)と上限(20)を変数にまとめ,1 か所を変えるだけで難しさを調整できるようにする
  • よけた数が決めた数になったら「クリア」と出して止める(終わり方を 2 通りにする)
  • 自機が左右の端を越えたら反対側から出てくるようにする(端の判定を自分で考える)

必須が 1〜2 つ足りない → 7 / 動かない・未着手が複数 → 6

演習演習 12-5 2 人で遊べるようにする

キーを分ければ,1 台のパソコンで 2 人遊べます. 左のプレイヤーは a と z,右のプレイヤーは ↑ と ↓ で動かす「テニス」の出発点を与えます. ボールがラケットに当たると跳ね返り,後ろの壁に当たると相手の得点です. これを発展させて,次の要件を満たす試合にしてください.

  • どちらかが 5 点取ったら,画面の中央付近 (−90, 0) に「左の勝ち」か「右の勝ち」を大きく表示し,return で止める
  • 止まったあとは,キーを押してもラケットが動かないようにする(フラグ変数 playing を True で始め,終わったら False にする)
  • ラケットではね返るたびに,ボールの横の速さを 1 ずつ大きくする(左のラケットでは dx = -dx + 1,右のラケットでは dx = -dx - 1).点が入ってボールを (0, 0) に戻すときは,速さも元に戻す(右の得点なら dx = 6,左の得点なら dx = -6)
  • これもやりましょう:速くなりすぎないように,横の速さの大きさを15 までにする.左のラケットではね返ったあとの dx は 15 より大きくせず,右のラケットではね返ったあとの dx は −15 より小さくしない(min・max か if を使う)
評価の目安(10 段階)

必須(すべて満たして正しく動けば 8)

  1. どちらかが 5 点取ったら「左の勝ち」か「右の勝ち」を画面に表示し,return で止める
  2. 止まったあとは,キーを押してもラケットが動かない(フラグ変数 playing)
  3. ラケットではね返るたびに,ボールの横の速さ dx の大きさが 1 ずつ大きくなり,点が入ると 6 に戻る
  4. 何度はね返っても dx の大きさが 15 を超えない(左では 15,右では −15 で止まる)

発展(必須に加えて 1 つ以上で 9,学んだ範囲を大きく超える作り込みで 10)

  • ラケットの長さを 1 つの変数で決め,見た目(shapesize)と当たり判定の数が一緒に変わるようにする
  • スペースキーで,終わったあとに得点・速さ・フラグを元に戻し,もう一度遊べるようにする
  • ボールが当たったラケットの位置で,はね返る向き(dy)を変える

必須が 1〜2 つ足りない → 7 / 動かない・未着手が複数 → 6

自由演習 12-6 オリジナルゲームを作る

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

評価の目安(10 段階)

必須(すべて満たして正しく動けば 8)

  1. 操作できる — onkeypress か onscreenclick で,遊ぶ人が何かを動かせる
  2. 当たり判定がある — distance か座標の範囲で,ものとものが当たったかを判定している
  3. スコアと終了がある — 点数を画面に出し,条件を満たしたら return で止まる
  4. 先頭のコメントに題名・氏名・日付と遊び方を書いている

発展(必須に加えて 1 つ以上で 9,学んだ範囲を大きく超える作り込みで 10)

  • スコアや時間に応じて速さなどを変え,だんだん難しくなる(増え方・上限を変数で決める)
  • 12.7.6 節のように弾を撃てる(フラグで「飛んでいる最中」を管理する),2 人で対戦できる
  • 終わり方を複数にして結果の表示を分ける,やり直しで変数をすべて元に戻すなど,ゲームの流れを状態で組み立てる

必須が 1〜2 つ足りない → 7 / 動かない・未着手が複数 → 6

ヒント 題材のヒント:よける/取る/当てる/逃げる/追いかける/積む/ もぐらたたき/リアクションの速さをはかる/迷路を抜ける/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 回で,def と global を 決まり文句として使い続けてきました. tick・go_left・draw_board — 気がつけば,ずいぶんたくさんの「名前の付いた中身」を書いています.

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

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

復習:虫食いで確かめる

コードの ____ を埋めてください.実行前に結果を予測し,実行後に問題文の条件と照合しましょう.assert は条件が成り立つかを調べ,成り立たないとエラーになります.

虫食い虫食い・復習 1 右矢印に操作を割り当てる

右矢印を離すたびに20進むようにします.関数の名前とキーを補い,実行後にキー操作で確認してください.

fill_review_lec09_1_1.py
from turtle import *
def move_right():
    forward(20)
onkey(____, ____)
listen()
done()