iPadでプログラミングにチャレンジしてゲームを作ってみましょう。今回は後編です。
このプログラミングにパソコンは不要です。iPadでもiPad AirでもiPad ProでもiPad miniでもできてしまいます。
Pythonistaをダウンロード
前編と同様、利用するツールはPythonistaです。iPadでプログラミングをするには必須とも言えるツールです。 今回、Pythonistaに付属する多数のサンプルのうち、ゲームチュートリアルをベースにして説明します。PythonistaのExampleに「Game Tutorial」フォルダがあり、その中に収録されているものを分かりやすく説明します。今回の出来上がりイメージ
ゲームの作り方を説明で今回は後編です。前編ではファイルの作り方から始まり、iPadを傾けてプレーヤーを操作するところまでを説明しています。順に説明しているので、初めての方は前編からご確認ください。 今回、前編を大幅に機能追加ます。下記イメージのように、降ってくる隕石を破壊しつつ、コインを集める本格的なシューティングゲームに仕上げていきます。空からコインを降らせる
コインが空から降ってくることで、グッとゲームっぽくなります。コインがプレーヤーに触れるか、画面の外に出るとコインが消えるようになっています。早速みていきましょう。# coding: utf-8
from scene import *
import sound
import random
A = Action
def cmp(a, b):
return ((a > b) - (a < b))
standing_texture = Texture('plf:AlienGreen_front')
walk_textures = [Texture('plf:AlienGreen_walk1'), Texture('plf:AlienGreen_walk2')]
# ---[1]
class Coin (SpriteNode):
def __init__(self, **kwargs):
SpriteNode.__init__(self, 'plf:Item_CoinGold', **kwargs)
class Game (Scene):
def setup(self):
self.background_color = '#004f82'
self.ground = Node(parent=self)
x = 0
while x <= self.size.w + 64:
tile = SpriteNode('plf:Ground_PlanetHalf_mid', position=(x, 0))
self.ground.add_child(tile)
x += 64
self.player = SpriteNode(standing_texture)
self.player.anchor_point = (0.5, 0)
self.player.position = (self.size.w/2, 32)
self.add_child(self.player)
self.walk_step = -1
# ---[2]
self.items = []
def update(self):
# ---[3]
self.update_player()
self.check_item_collisions()
if random.random() < 0.05: self.spawn_item() def update_player(self): g = gravity() if abs(g.x) > 0.05:
self.player.x_scale = cmp(g.x, 0)
x = self.player.position.x
max_speed = 40
x = max(0, min(self.size.w, x + g.x * max_speed))
self.player.position = x, 32
step = int(self.player.position.x / 40) % 2
if step != self.walk_step:
self.player.texture = walk_textures[step]
sound.play_effect('rpg:Footstep00', 0.05, 1.0 + 0.5 * step)
self.walk_step = step
else:
self.player.texture = standing_texture
self.walk_step = -1
def check_item_collisions(self):
# ---[4]
player_hitbox = Rect(self.player.position.x - 20, 32, 40, 65)
for item in list(self.items):
if item.frame.intersects(player_hitbox):
self.collect_item(item)
elif not item.parent:
self.items.remove(item)
def spawn_item(self):
# ---[5]
coin = Coin(parent=self)
coin.position = (random.uniform(20, self.size.w-20), self.size.h + 30)
d = random.uniform(2.0, 4.0)
actions = [A.move_by(0, -(self.size.h + 60), d), A.remove()]
coin.run_action(A.sequence(actions))
self.items.append(coin)
def collect_item(self, item):
sound.play_effect('digital:PowerUp7')
item.remove_from_parent()
self.items.remove(item)
if __name__ == '__main__':
run(Game(), PORTRAIT, show_fps=True)
ではコードの解説です。
# ---[1]
class Coin (SpriteNode):
def __init__(self, **kwargs):
SpriteNode.__init__(self, 'plf:Item_CoinGold', **kwargs)
画像を扱うSpriteNodeを継承したCoinクラスを作成します。
「def __init__(self, **kwargs)」はコンストラクタと言って、クラスを使う時、最初に呼び出される処理のことです。このコンストラクタの中でSpriteNodeのコンストラクタを呼び出していますね。
その際、「'plf:Item_CoinGold'」でコインの画像を指定しています。Coinクラスを作成すると、この画像が使われます。
# ---[2]
self.items = []
画面に表示するコインを管理するための変数を用意しています。コインを画面に表示するタイミングで配列に追加され、非表示になるタイミングで配列から取り除かれます。
# ---[3]
self.update_player()
self.check_item_collisions()
if random.random() < 0.05:
self.spawn_item()
今までupdate()メソッドにあった処理は「update_player()」メソッドに分離しています。分離しなくても動作には問題ないのですが、この方がスッキリしますね。コードが長くなると読みづらくなってくるので、このように小さい単位に切り出して整理することを「リファクタリング」と呼びます。
「check_item_collisions()」はプレーヤーとコインが重なるかどうかをチェックするためのメソッドです。次に説明します。
「if random.random() < 0.05」は面白いですね。random()関数は0から1未満の数字をランダムに作ってくれるのですが、0.05未満になる場合だけ「spawn_item()」を呼び出すようにしています。0.05は5%と同義になっていて、100回のうち5回だけspawn_item()を呼び出すようになります。この処理はupdate()の中に書かれていますが、update()は1秒間に60回呼び出されるので、5%だと1秒間に3回spawn_item()が呼び出されることになります。
# ---[4]
player_hitbox = Rect(self.player.position.x - 20, 32, 40, 65)
for item in list(self.items):
if item.frame.intersects(player_hitbox):
self.collect_item(item)
elif not item.parent:
self.items.remove(item)
check_item_collisions()メソッドの中身になります。player_hitboxの矩形をRect()で定義しています。
player_hitboxはコインとの当たり判定に使う矩形ですが、プレーヤーよりやや小さめのサイズを指定しています。これはプレーヤー画像の周りに透明の領域があるための、当たり判定をより厳密にできるように調整しています。
「for item in list(self.items)」では画面に表示しているコインを全てループでチェックし、プレーヤーとの当たり判定を行っています。
「elif not item.parent」が少し分かりづらいですね。コインを作るところで説明しますが、コインが画面の一番下にくると自動的にコインが非表示になるように設定されています。非表示になったタイミングではitems配列から削除されないので、このタイミングで検知して配列から除外するようになっています。
# ---[5]
coin = Coin(parent=self)
coin.position = (random.uniform(20, self.size.w-20), self.size.h + 30)
d = random.uniform(2.0, 4.0)
actions = [A.move_by(0, -(self.size.h + 60), d), A.remove()]
coin.run_action(A.sequence(actions))
self.items.append(coin)
いよいよコインを作り出すところです。最初の2行でコインを生成し、表示する位置をランダムに設定しています。
「random.uniform()」で2.0〜4.0の範囲でランダムな数字を生成しています。
「actions = [A.move_by(0, -(self.size.h + 60), d), A.remove()]」と「coin.run_action(A.sequence(actions))」でアニメーションを定義しています。
move_by()でどこからどこまで移動するかを定義しています。移動速度はrandom.uniform()で生成した乱数によって変わります。
remove()は画面から削除されるアニメーションです。この2つは配列になっており、sequence()に渡すことで順番にアニメーションを実行してくれるようになっています。この場合、画面の上から下まで移動したら、画面から削除する、という一連の流れをこの2行で行ってくれます。非常に簡単ですね。
「items.append(coin)」で今まで見てきたitems配列にコインを追加しています。これで画面に表示されているコインが管理できるようになります。
ここまでを動かすと、空から降ってくるコインをプレーヤーが集めることができるようになっています。
コインを集めて点数をつける
プレーヤーがコインを集めるだけだとなんだか物足りません。ここのパートでは、コインを集めて点数を表示できるようになります。# coding: utf-8
from scene import *
import sound
import random
A = Action
def cmp(a, b):
return ((a > b) - (a < b))
standing_texture = Texture('plf:AlienGreen_front')
walk_textures = [Texture('plf:AlienGreen_walk1'), Texture('plf:AlienGreen_walk2')]
class Coin (SpriteNode):
def __init__(self, **kwargs):
SpriteNode.__init__(self, 'plf:Item_CoinGold', **kwargs)
class Game (Scene):
def setup(self):
self.background_color = '#004f82'
self.ground = Node(parent=self)
x = 0
while x <= self.size.w + 64:
tile = SpriteNode('plf:Ground_PlanetHalf_mid', position=(x, 0))
self.ground.add_child(tile)
x += 64
self.player = SpriteNode(standing_texture)
self.player.anchor_point = (0.5, 0)
self.player.position = (self.size.w/2, 32)
self.add_child(self.player)
# ---[1]
score_font = ('Futura', 40)
self.score_label = LabelNode('0', score_font, parent=self)
self.score_label.position = (self.size.w/2, self.size.h - 70)
self.score_label.z_position = 1
self.score = 0
self.walk_step = -1
self.items = []
def update(self):
self.update_player()
self.check_item_collisions()
if random.random() < 0.05: self.spawn_item() def update_player(self): g = gravity() if abs(g.x) > 0.05:
self.player.x_scale = cmp(g.x, 0)
x = self.player.position.x
max_speed = 40
x = max(0, min(self.size.w, x + g.x * max_speed))
self.player.position = x, 32
step = int(self.player.position.x / 40) % 2
if step != self.walk_step:
self.player.texture = walk_textures[step]
sound.play_effect('rpg:Footstep00', 0.05, 1.0 + 0.5 * step)
self.walk_step = step
else:
self.player.texture = standing_texture
self.walk_step = -1
def check_item_collisions(self):
player_hitbox = Rect(self.player.position.x - 20, 32, 40, 65)
for item in list(self.items):
if item.frame.intersects(player_hitbox):
self.collect_item(item)
elif not item.parent:
self.items.remove(item)
def spawn_item(self):
coin = Coin(parent=self)
coin.position = (random.uniform(20, self.size.w-20), self.size.h + 30)
d = random.uniform(2.0, 4.0)
actions = [A.move_by(0, -(self.size.h + 60), d), A.remove()]
coin.run_action(A.sequence(actions))
self.items.append(coin)
def collect_item(self, item, value=10):
sound.play_effect('digital:PowerUp7')
item.remove_from_parent()
self.items.remove(item)
# ---[2]
self.score += value
self.score_label.text = str(self.score)
if __name__ == '__main__':
run(Game(), PORTRAIT, show_fps=True)
では早速見ていきましょう。
---[1]
score_font = ('Futura', 40)
self.score_label = LabelNode('0', score_font, parent=self)
self.score_label.position = (self.size.w/2, self.size.h - 70)
self.score_label.z_position = 1
self.score = 0
1行目の「'Futura'」というのはフォントの名前で、40は文字サイズです。
LebelNode()に表示する文字と一緒にこのフォントを指定しています。
z_positionというのは重なり順を設定しています。デフォルトではz_positionが0なので、ここで1を指定しておけばプレーヤーやコインよりも上に表示されることになります。
# ---[2]
self.score += value
self.score_label.text = str(self.score)
プレーヤーとコインが重なったタイミングで呼び出されるメソッドなの中になります。このタイミングでscoreを加算することで点数が決まるようになっています。
ここまでで動かすと、コインに触れるたびに10ポイントずつ増えるのが分かります。
隕石も降らせる
コインを集めるだけだとなんだか退屈ですよね。もっとゲームにスリルを加えるために隕石を降らせます。当然、隕石がプレーヤーに当たるとゲームオーバーで最初からやり直し!# coding: utf-8
from scene import *
import sound
import random
A = Action
def cmp(a, b):
return ((a > b) - (a < b))
standing_texture = Texture('plf:AlienGreen_front')
walk_textures = [Texture('plf:AlienGreen_walk1'), Texture('plf:AlienGreen_walk2')]
# ---[1]
hit_texture = Texture('plf:AlienGreen_hit')
class Coin (SpriteNode):
def __init__(self, **kwargs):
SpriteNode.__init__(self, 'plf:Item_CoinGold', **kwargs)
# ---[2]
class Meteor (SpriteNode):
def __init__(self, **kwargs):
img = random.choice(['spc:MeteorBrownBig1', 'spc:MeteorBrownBig2'])
SpriteNode.__init__(self, img, **kwargs)
class Game (Scene):
def setup(self):
self.background_color = '#004f82'
self.ground = Node(parent=self)
x = 0
while x <= self.size.w + 64:
tile = SpriteNode('plf:Ground_PlanetHalf_mid', position=(x, 0))
self.ground.add_child(tile)
x += 64
self.player = SpriteNode(standing_texture)
self.player.anchor_point = (0.5, 0)
self.add_child(self.player)
score_font = ('Futura', 40)
self.score_label = LabelNode('0', score_font, parent=self)
self.score_label.position = (self.size.w/2, self.size.h - 70)
self.score_label.z_position = 1
self.items = []
# ---[3]
self.new_game()
def new_game(self):
for item in self.items:
item.remove_from_parent()
self.items = []
self.score = 0
self.score_label.text = '0'
self.walk_step = -1
self.player.position = (self.size.w/2, 32)
self.player.texture = standing_texture
self.speed = 1.0
# ---[4]
self.game_over = False
def update(self):
if self.game_over:
return
self.update_player()
self.check_item_collisions()
if random.random() < 0.05 * self.speed: self.spawn_item() def update_player(self): g = gravity() if abs(g.x) > 0.05:
self.player.x_scale = cmp(g.x, 0)
x = self.player.position.x
max_speed = 40
x = max(0, min(self.size.w, x + g.x * max_speed))
self.player.position = x, 32
step = int(self.player.position.x / 40) % 2
if step != self.walk_step:
self.player.texture = walk_textures[step]
sound.play_effect('rpg:Footstep00', 0.05, 1.0 + 0.5 * step)
self.walk_step = step
else:
self.player.texture = standing_texture
self.walk_step = -1
def check_item_collisions(self):
# ---[5]
player_hitbox = Rect(self.player.position.x - 20, 32, 40, 65)
for item in list(self.items):
if item.frame.intersects(player_hitbox):
if isinstance(item, Coin):
self.collect_item(item)
elif isinstance(item, Meteor):
self.player_hit()
elif not item.parent:
self.items.remove(item)
def player_hit(self):
# ---[6]
self.game_over = True
sound.play_effect('arcade:Explosion_1')
self.player.texture = hit_texture
self.player.run_action(A.move_by(0, -150))
self.run_action(A.sequence(A.wait(2*self.speed), A.call(self.new_game)))
def spawn_item(self):
if random.random() < 0.3:
# ---[7]
meteor = Meteor(parent=self)
meteor.position = (random.uniform(20, self.size.w-20), self.size.h + 30)
d = random.uniform(2.0, 4.0)
actions = [A.move_to(random.uniform(0, self.size.w), -100, d), A.remove()]
meteor.run_action(A.sequence(actions))
self.items.append(meteor)
else:
coin = Coin(parent=self)
coin.position = (random.uniform(20, self.size.w-20), self.size.h + 30)
d = random.uniform(2.0, 4.0)
actions = [A.move_by(0, -(self.size.h + 60), d), A.remove()]
coin.run_action(A.sequence(actions))
self.items.append(coin)
# ---[8]
self.speed = min(3, self.speed + 0.005)
def collect_item(self, item, value=10):
sound.play_effect('digital:PowerUp7')
item.remove_from_parent()
self.items.remove(item)
self.score += value
self.score_label.text = str(self.score)
if __name__ == '__main__':
run(Game(), PORTRAIT, show_fps=True)
それでは早速見ていきましょう。
# ---[1]
hit_texture = Texture('plf:AlienGreen_hit')
隕石にぶつかった際にプレーヤーが何一つ表情を変えないと変ですよね。この1行は隕石がぶつかった際に表示する画像を定義しています。
# ---[2]
class Meteor (SpriteNode):
def __init__(self, **kwargs):
img = random.choice(['spc:MeteorBrownBig1', 'spc:MeteorBrownBig2'])
SpriteNode.__init__(self, img, **kwargs)
いよいよ隕石のクラス定義です。基本的にはCoinクラスと同じですが、大きな違いは「random.choice(['spc:MeteorBrownBig1', 'spc:MeteorBrownBig2'])」ですね。これは隕石の画像を2つ用意して、ランダムで表示する画像を決めています。
# ---[3]
self.new_game()
今までsetup()に記載していたものの一部を「new_game()」に切り出しています。これでゲームオーバーになっても、アプリを終了することなくやり直すことができるようになります。
# ---[4]
self.game_over = False
プレーヤーが隕石とぶつかると「game_over」がTrueになります。Trueになることでupdate()の中では何もしなくなるため、無駄な処理をしなくて済むようになっています。
# ---[5]
player_hitbox = Rect(self.player.position.x - 20, 32, 40, 65)
for item in list(self.items):
if item.frame.intersects(player_hitbox):
if isinstance(item, Coin):
self.collect_item(item)
elif isinstance(item, Meteor):
self.player_hit()
elif not item.parent:
self.items.remove(item
コインと同様に隕石との当たり判定を行います。「isinstance()」というのは、itemがCoinクラスなのか、Meteorクラスなのかを判定するために利用しています。つまり、items配列にはコインと隕石が一緒に登録されているということですね。
# ---[6]
self.game_over = True
sound.play_effect('arcade:Explosion_1')
self.player.texture = hit_texture
self.player.run_action(A.move_by(0, -150))
self.run_action(A.sequence(A.wait(2*self.speed), A.call(self.new_game)))
プレーヤーが隕石とぶつかった場合の処理です。game_overをTrueに変更していますね。これでupdate()が何も処理しなくなります。
隕石とぶつかったので、「play_effect()」で音を再生し、hit_textureをプレヤー画像に差し替え、move_by()で画面の下に落ちていくアニメーションをを設定しています。
最後に「A.call(self.new_game)」を行うことで、new_game()を呼び出しています。
# ---[7]
meteor = Meteor(parent=self)
meteor.position = (random.uniform(20, self.size.w-20), self.size.h + 30)
d = random.uniform(2.0, 4.0)
actions = [A.move_to(random.uniform(0, self.size.w), -100, d), A.remove()]
meteor.run_action(A.sequence(actions))
self.items.append(meteor)
30%の確率でコインの代わりに隕石が出現するようになっています。また、隕石は真っ直ぐ下に落ちるのではなく,move_to()で座標を指定することで斜めに向かって落ちるようになっています。落ちる場所もランダムになっていますね。
# ---[8]
self.speed = min(3, self.speed + 0.005)
ゲームを少し楽しくするために、コインと隕石が作られるたびに少しずつ出現率が上がるように設定しています。
ここまでを動かすことで、隕石を避けながらコインを集めるゲームに仕上がっています。
シューティングゲームの仕上げ
いよいよ大詰めです。隕石を避けるだけじゃなく、プレーヤーがレーザーで隕石を撃ち落とせるようにします。# coding: utf-8
from scene import *
import sound
import random
from math import sin, cos, pi
A = Action
def cmp(a, b):
return ((a > b) - (a < b))
standing_texture = Texture('plf:AlienGreen_front')
walk_textures = [Texture('plf:AlienGreen_walk1'), Texture('plf:AlienGreen_walk2')]
hit_texture = Texture('plf:AlienGreen_hit')
class Coin (SpriteNode):
def __init__(self, **kwargs):
SpriteNode.__init__(self, 'plf:Item_CoinGold', **kwargs)
class Meteor (SpriteNode):
def __init__(self, **kwargs):
img = random.choice(['spc:MeteorBrownBig1', 'spc:MeteorBrownBig2'])
SpriteNode.__init__(self, img, **kwargs)
self.destroyed = False
class Game (Scene):
def setup(self):
self.background_color = '#004f82'
self.ground = Node(parent=self)
x = 0
while x <= self.size.w + 64:
tile = SpriteNode('plf:Ground_PlanetHalf_mid', position=(x, 0))
self.ground.add_child(tile)
x += 64
self.player = SpriteNode(standing_texture)
self.player.anchor_point = (0.5, 0)
self.add_child(self.player)
score_font = ('Futura', 40)
self.score_label = LabelNode('0', score_font, parent=self)
self.score_label.position = (self.size.w/2, self.size.h - 70)
self.score_label.z_position = 1
self.items = []
self.lasers = []
self.new_game()
def new_game(self):
for item in self.items:
item.remove_from_parent()
self.items = []
self.lasers = []
self.score = 0
self.score_label.text = '0'
self.walk_step = -1
self.player.position = (self.size.w/2, 32)
self.player.texture = standing_texture
self.speed = 1.0
self.game_over = False
def update(self):
if self.game_over:
return
self.update_player()
self.check_item_collisions()
self.check_laser_collisions()
if random.random() < 0.05 * self.speed: self.spawn_item() def touch_began(self, touch): self.shoot_laser() def update_player(self): g = gravity() if abs(g.x) > 0.05:
self.player.x_scale = cmp(g.x, 0)
x = self.player.position.x
max_speed = 40
x = max(0, min(self.size.w, x + g.x * max_speed))
self.player.position = x, 32
step = int(self.player.position.x / 40) % 2
if step != self.walk_step:
self.player.texture = walk_textures[step]
sound.play_effect('rpg:Footstep00', 0.05, 1.0 + 0.5 * step)
self.walk_step = step
else:
self.player.texture = standing_texture
self.walk_step = -1
def check_item_collisions(self):
player_hitbox = Rect(self.player.position.x - 20, 32, 40, 65)
for item in list(self.items):
if item.frame.intersects(player_hitbox):
if isinstance(item, Coin):
self.collect_item(item)
elif isinstance(item, Meteor):
if item.destroyed:
self.collect_item(item, 100)
else:
self.player_hit()
elif not item.parent:
self.items.remove(item)
def check_laser_collisions(self):
for laser in list(self.lasers):
if not laser.parent:
self.lasers.remove(laser)
continue
for item in self.items:
if not isinstance(item, Meteor):
continue
if item.destroyed:
continue
if laser.position in item.frame:
self.destroy_meteor(item)
self.lasers.remove(laser)
laser.remove_from_parent()
break
def destroy_meteor(self, meteor):
sound.play_effect('arcade:Explosion_2', 0.2)
meteor.destroyed = True
meteor.texture = Texture('plf:Item_Star')
for i in range(5):
m = SpriteNode('spc:MeteorBrownMed1', parent=self)
m.position = meteor.position + (random.uniform(-20, 20), random.uniform(-20, 20))
angle = random.uniform(0, pi*2)
dx, dy = cos(angle) * 80, sin(angle) * 80
m.run_action(A.move_by(dx, dy, 0.6, TIMING_EASE_OUT))
m.run_action(A.sequence(A.scale_to(0, 0.6), A.remove()))
def player_hit(self):
self.game_over = True
sound.play_effect('arcade:Explosion_1')
self.player.texture = hit_texture
self.player.run_action(A.move_by(0, -150))
self.run_action(A.sequence(A.wait(2*self.speed), A.call(self.new_game)))
def spawn_item(self):
if random.random() < 0.3 * self.speed: meteor = Meteor(parent=self) meteor.position = (random.uniform(20, self.size.w-20), self.size.h + 30) d = random.uniform(2.0, 4.0) actions = [A.move_to(random.uniform(0, self.size.w), -100, d), A.remove()] meteor.run_action(A.sequence(actions)) self.items.append(meteor) else: coin = Coin(parent=self) coin.position = (random.uniform(20, self.size.w-20), self.size.h + 30) d = random.uniform(2.0, 4.0) actions = [A.move_by(0, -(self.size.h + 60), d), A.remove()] coin.run_action(A.sequence(actions)) self.items.append(coin) self.speed = min(3, self.speed + 0.005) def collect_item(self, item, value=10): if value > 10:
sound.play_effect('digital:PowerUp8')
else:
sound.play_effect('digital:PowerUp7')
item.remove_from_parent()
self.items.remove(item)
self.score += value
self.score_label.text = str(self.score)
def shoot_laser(self):
if len(self.lasers) >= 3:
return
laser = SpriteNode('spc:LaserGreen12', parent=self)
laser.position = self.player.position + (0, 30)
laser.z_position = -1
actions = [A.move_by(0, self.size.h, 1.2 * self.speed), A.remove()]
laser.run_action(A.sequence(actions))
self.lasers.append(laser)
sound.play_effect('digital:Laser4')
if __name__ == '__main__':
run(Game(), PORTRAIT, show_fps=True)
ここまで来れば説明なしで大枠は理解できるようになっていると思います。上から見てみると新たに追加された行の意味が理解できると思います。
ポイントは下記です。読み解く上での参考にしてみてください。
- レーザーは「lasers」配列で管理される
- レーザーは隕石やコインと違い、下から上にアニメーションする
- レーザーは隕石だけ撃ち落とし、コインは撃ち落とさない(当たり判定は隕石だけ)
- レーザーの当たり判定は「check_laser_collisions()」で記載されている
- 「destroy_meteor()」は隕石が破壊された時の処理。5つの破片をランダムに飛び散らせている。
- 「touch_began()」は画面をタッチした時に呼び出される