ゲームをつくる
キーボード,マウス,当たり判定.ここまでの全部を使って,遊べるものを作ります.
ここまでのプログラムは,どれもこちらから一方的に命令するものでした. 実行すれば決まった絵が出て,決まったところで終わります. 今回は,人の操作に反応するプログラムを作ります. キーを押すと自機が動き,敵に当たると点が入り,失敗が続くと終わる — ゲームです.
新しく覚えることは多くありません.
「キーが押されたら,この関数を呼んでください」と登録する命令と,
「2 つのかめが近いかどうか」を調べる命令.この 2 つだけです.
あとは第 10 章の ontimer と def,第 5 章の if,
第 3 章の変数 — これまで習ったものの組み合わせです.
def と global は,第 10 章と同じく
決まり文句のままで作れます(仕組みは第 13 章でくわしく習います).
この章で作るもの.最後に作るのは,次のような画面のシューティングゲームです.
← → で下の自機を動かし,上から落ちてくる玉を受け止めます.
取れば左上のスコアが増え,3 回にがすとゲームオーバーです.
12.7 節で,これを5 段階に分けて少しずつ作ります.
- ゲームの骨組み(準備 → 毎フレームの処理 → 終了判定)を自分で書ける
onkeypressでキーボードから,onscreenclickやonclickでマウスから操作を受け取れるontimerでゲームループを作れる(while Trueは使えないことがわかる)distanceや座標の範囲で当たり判定を書ける- スコアを画面に出し,条件を満たしたらゲームを終われる
- 自分で考えた小さなゲームを,最後まで組み立てられる
- キーが効かないときは,一度タートル画面をクリックしてください.コード欄に文字カーソルがあると,キーはコード欄に取られます.
- 止めたいときは「■ 停止」を押します.押さないと動き続けます.
- 速さの設定は,今回のように
ontimerで動かすプログラムには効きません.速さを変えたいときはontimerのミリ秒の数を変えます.
12.1 ゲームの骨組み
どんなゲームも,中身は次の 3 つに分かれます. シューティングも,パズルも,レースゲームも,大きさが違うだけで形は同じです.
- 準備 — 自機や敵を作り,位置と色を決める.得点を 0 にする.1 回だけ行う.
- 毎フレームの処理 — 少し動かす,入力を見る,当たったか調べる,得点を書き直す.1 秒に何十回も繰り返す.
- 終了判定 — 続けるか,終わるかを決める.終わるなら繰り返しをやめる.
12.1.1 なぜ while True: が使えないのか
紙の教科書に載っているゲームのプログラムは,たいてい次のような形をしています.
「ずっと繰り返す」という意味の while True: です.
while True: # ← このページでは使えません
# 少し待つ
# 少し動かす
# 画面を描き直す
while True: で待ち続けると,絵は出ず,キーも効かず,
ページ全体が固まってしまいます.
この教材では,しばらく待っても終わらないプログラムは
実行中断 という表示を出して自動的に止めます.
12.1.2 ontimer でループを作る(第 10 章の復習)
かわりに使うのが ontimer(関数, ミリ秒) です.
「指定したミリ秒だけ経ったら,この関数を 1 回呼んでください」と
ブラウザにお願いする命令です.お願いしたらプログラムはいったん終わります.
ブラウザは空いた時間で画面を描き直し,キー入力を受け取り,時間が来たら関数を呼びます.
1 回しか呼ばれないので,呼ばれた関数の最後で,もう一度 ontimer を書く
のがコツです.これで「少し動く → 少し待つ → また少し動く」が続きます.
# 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 回書く |
# 矢印キーでかめを動かす
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()
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 で受け取れます.
# 押しっぱなしでなめらかに動かす
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 変数名 と書きます.これを忘れると,
関数の中だけの別の変数ができてしまい,外の dx は 0 のままです.
読むだけなら global は要りません(tick の中の
dx がそうです).書き換えるときだけ必要です.
第 10 章と同じ決まり文句です.
なぜ関数の中と外で変数が別ものになるのかは,第 13 章でくわしく習います.
dx = -6 と dy = 6 の両方が立ち,
ななめに動きます.
矢印キーの左右で動き,スペースキーを押すと色が変わるプログラムです. ____ を埋めて完成させてください. スペースキーの名前は表のとおりです.関数を渡すときはかっこを付けません.
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) # ← ここでもかっこは付けない
# クリックした所へかめが飛ぶ
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()
12.3.2 かめ自身をクリックする
画面全体ではなく,そのかめが押されたときだけ反応させたいこともあります.
もぐらたたきや,ボタンのような使い方です.そのときは
かめ.onclick(関数) と,かめに直接登録します.
# 押されたかめの色が変わる(もぐらたたき風)
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()
shapesize で大きく見せても,押せる範囲は変わりません.
小さくて押しにくいときは,onscreenclick で座標を受け取り,
かめ.distance(x, y) < 30 のように自分で判定してください
(12.4 節でやります).
クリックするたびに,その場所に色つきの点を打つプログラムです.
____ を埋めてください.
onscreenclick が呼ぶ関数は,引数を 2 つ受け取る約束でした.
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 節でも同じ考え方が出てきます.
# ドラッグのかわり: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()
holding が True か False かで,
同じクリックの意味が変わるのがこの書き方のポイントです.
if の順番にも意味があります.
先に「つかんでいるか」を調べるので,つかんでいる間はどこを押しても「置く」になります.
12.4 当たり判定
ゲームらしさの正体は当たり判定です. 弾が敵に当たった,自機が壁にぶつかった,アイテムを拾った — すべて 「2 つのものが近いかどうか」を調べているだけです.
12.4.1 距離で判定する
t.distance(u) と書くと,かめ t とかめ u の
中心どうしの距離が返ってきます.
これがある値より小さければ「当たった」とみなします.
if t.distance(u) < 30:
print("当たった!")
判定に使う数(上の図の r)は,見た目の大きさに合わせて自分で決めます. 小さくすると「かすっても当たらない」きびしいゲームに, 大きくすると「近づけば当たる」やさしいゲームになります. 20〜30 くらいから試して,遊びながら調整してください.
# クリックで丸を動かし,四角に近づくと色が変わる
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()
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("四角いゾーンの中にいます")
-100 < x < 100 とまとめて書くこともできます.
数学の書き方そのままです.上の if は
if -100 < x < 100 and 50 < y < 150: と書けます.
12.4.3 画面の端の判定と跳ね返り
画面からはみ出さないようにする,あるいは端でボールを跳ね返す — これも当たり判定です. 620 × 460 の画面なら,画面の端そのものは x が ±310,y が ±230 です. かめには大きさがあるので,少し内側の ±300・±215 あたりで折り返すと ちょうどよく見えます.
跳ね返らせる方法はとても簡単で,
進む向きの符号を反対にするだけです.
右へ 7 ずつ進んでいた(dx = 7)なら,dx = -dx とすれば
左へ 7 ずつ進むようになります.
# 画面の端ではね返るボール
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()
or は「どちらか一方でも成り立てば」という意味です.
ball.xcor() > 300 or ball.xcor() < -300 は
「右の壁を越えたか,左の壁を越えたか」を 1 行で調べています.
左右のキーで動かせますが,このままでは画面の外まで行ってしまいます. x が −290 より小さくならず,290 より大きくならないようにしてください. ____ を埋めましょう.
if の後は「行きすぎていたら,行きすぎない位置に戻す」と考えます.
左へ行きすぎたら x を -290 に,右へ行きすぎたら x を 290 にします.
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")) # 新しく書く
write に渡せるのは文字列です.
score は数なので,str(score) で文字列に直してから
+ でつなぎます."スコア " + str(3) は
"スコア 3" になります.
# スコア板:スペースキーを押すと点が増える
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()
clear() は,画面に描かれた線と文字をすべて消します.
かめ 1 匹ぶんだけを消すのではありません.
ですからゲームでは,自機も敵も pu() で動かして線を残さない
ようにしておきます.そうすれば,画面に残っている絵はスコアの文字だけになり,
clear() しても困りません.
スペースキーを押すたびに,残りが 1 ずつ減っていく表示を作ります.
____ を埋めてください.
書き換えの 2 行(消す・書く)と,global を忘れずに.
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(偽)は,
「はい・いいえ」を表す 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 になります.
# シューティング ① 自機を動かす
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.7.2 ② 敵を落とす
敵を 1 匹追加し,ontimer で少しずつ下げます.
下まで行ったら,また上のどこかに戻します.
横の位置は random.randint(-270, 270) で毎回変えると,
それだけでゲームらしくなります.
# シューティング ② 敵が落ちてくる
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.7.3 ③ 当たり判定を入れる
tick の中で,自機と敵の距離を調べます.
近ければ「取った」ことにして,敵を上から出直させます.
ここでゲームとして成立します.必ず実行して,取れることを確かめてください.
# シューティング ③ 当たり判定
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()
if と elif にしているのは,
「取った」と「にがした」が同時に起きないようにするためです.
両方 if で書くと,画面の下のほうで取ったときに
2 回ぶん処理されてしまうことがあります.
12.7.4 ④ スコアを付ける
スコア用のかめを足し,取った数を数えます.
tick の中で score を書き換えるので
global score が要ります.
# シューティング ④ スコア
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.7.5 ⑤ ゲームオーバーを付ける
最後に終わりを付けます.ミスが 3 回たまったら,
大きな文字で「ゲームオーバー」と出して return で止めます.
フラグ playing を下ろしておくと,そのあとキーを押しても自機は動きません.
これで完成です.
# シューティング ⑤ 完成版
# 遊び方:← → で動かし,落ちてくる玉を取る.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()
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()で出す
# シューティング ⑥ 弾を撃つ
# 遊び方:← → で動かし,スペースキーで弾を撃つ.横に動く敵に当てる.
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()
flying のようなフラグを使わずに書くと,
スペースキーを押しっぱなしにしたときに弾が毎コマ出直してしまい,
弾がその場に止まって見えます.
「いま飛んでいるか」を覚えておくことが大事です.
12.8 ゲームで役に立つ文法
ゲームを作っていると出てくる,ここまでで説明しそびれた書き方を 4 つ紹介します. どれも短いものです.読めるようになっておけば十分です.
12.8.1 break — 繰り返しを途中でやめる
for の繰り返しの途中で break を実行すると,
その場で繰り返しから抜けます.「見つかったらもう探さなくてよい」ときに使います.
for i in range(10):
if i == 4:
break # ここで for から抜ける
print(i) # 0 1 2 3 だけが出る
break は「入口ひとつ・出口ひとつ」というプログラムの作法をくずすので,
使いすぎないほうがよいと言われます.
playing のようなフラグ変数で制御すれば,
break を使わずに同じことが書けます(12.6 節でやったとおりです).
12.8.2 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 になります.
playing = False
if not playing: # 「遊んでいる最中でなければ」
print("ゲームは止まっています")
not playing は playing == False と同じ意味です.
どちらでもかまいませんが,not のほうが短く,英語の文として読めます.
12.8.4 and と or — 条件を組み合わせる
条件は組み合わせられます.
and は「両方とも成り立つ」,
or は「どちらか一方でも成り立つ」です.
| 書き方 | 意味 | ゲームでの使いどころ |
|---|---|---|
A and B | A も B も成り立つとき | 「四角いゾーンの中」「自機が生きていて,かつ当たった」 |
A or B | A か B の少なくとも一方が成り立つとき | 「左の壁か右の壁に当たった」 |
not A | A が成り立たないとき | 「ゲーム中でなければ操作を受け付けない」 |
# 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:どちらか一方でも成り立つ")
ゲームが止まっているときは自機が動かないようにします.
____ を埋めてください.
スペースキーで止める/再開する仕組みです.
「止まっていれば再開,動いていれば停止」は not で書けます.
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.4.3 節)
shapeとcolorとshapesizeで見た目を作る- スペースキーなど,矢印以外のキーにも働きを付ける
- 動いた跡を線で残す(
pd()にしてみる)と,お絵かきソフトになります
図形をマウスで動かすプログラムを与えます. このランタイムはドラッグに対応していない(ボタンを押したままの状態も, いまのマウスの位置も取れない)ので, 1 回目のクリックでつかみ,2 回目のクリックで置く方式にしてあります. できることはドラッグと同じです.これを発展させてください.
- つかんでいる間は色を変える・大きくするなど,いまつかんでいることが分かる印を付ける
- 置ける場所を決めておき,そこに置けたら得点が入るようにする(並べかえパズル)
- 動かせるものを増やす(
elifを 1 つ足すだけです) ontimerで,つかんでいないものが勝手に動くようにする(もぐらたたきになります)- つかむ判定の
30を変えて,つかみやすさを調整する
held にはかめそのものを入れています.
何もつかんでいないことを表すのに None(「なし」を表す Python の値)を使い,
if held is not None: で「何かつかんでいるか」を調べています.
アイテムを拾うと点が増えるプログラムを与えます. 自機を動かしてアイテムに触れると,アイテムが別の場所に現れ,スコアが 1 増えます. これを発展させてください.
- 制限時間を付ける(
ontimerが呼ばれた回数を数える) - アイテムの種類を増やし,点の高いアイテムを作る
- 取るとアイテムが速くなる/自機が遅くなる
- スコアの表示を大きくする,色を変える,画面の中央に出す
こんどは取るのではなくよけるゲームです. 落ちてくる岩に当たったら終わり,よけた数がスコアになります. 出発点を与えるので,完成させて発展させてください.
____の部分ではなく,まだ書かれていない当たり判定とゲームオーバーを自分で足します- 岩を 2 個・3 個に増やす
- よけた数が増えるほど岩を速くする(
speed_yを少しずつ大きくする) - 当たったときに色を変える,画面を赤くするなどの演出を付ける
キーを分ければ,1 台のパソコンで 2 人遊べます.
左のプレイヤーは a と z,右のプレイヤーは
↑ と ↓ で動かす「テニス」の出発点を与えます.
ボールがラケットに当たると跳ね返り,後ろの壁に当たると相手の得点です.
- 得点の表示を,左右それぞれに分けて出す
- ボールが跳ね返るたびに少し速くする
- 5 点先取で「勝ち」と出して止める
- ラケットの長さ(
shapesizeと判定の数)を変えて難しさを調整する
自由な発想で,自分のゲームを作ってください. 12.7 節の 5 段階をそのまま真似て,①から順に少しずつ育ててください. いきなり完成形を書こうとすると,まず動きません. 弾を撃つゲームにしたいときは,12.7.6 節の ⑥ が出発点になります.
作品の要件(次の 4 つを満たしていること)
- 操作できること — キーボードかマウスで,遊ぶ人が何かを動かせる
- 当たり判定があること —
distanceか座標の範囲で,ものとものが当たったかを判定している - スコアか終了があること — 点数を画面に出すか,条件を満たしたら終わる(両方あればなおよい)
- コメントで題名と氏名を書くこと — 先頭に題名・氏名・日付と,遊び方を書く
まとめ
- ゲームは準備 → 毎フレームの処理 → 終了判定の 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 章)に, 白紙のエディタは付録「じゆうに書く」にあります. 思いついたことを,いつでも試してみてください.