Pythonista ゲーム開発②
上から降ってくる隕石を避けるゲーム
▼ゲームというか、ただ横スクロールしているだけなのでゲーム性はありません。一応進んだ分だけ距離を表示するようにしています。

▼衝突判定処理:衝突判定を行うための範囲をRect(left, top, width, height)で指定します。衝突は"item.frame.intersects(player_hitbox)"で判定し、衝突しているスプライトは"isinstance(item, Meteor)"で判別します。
def check_collision(self):
player_hitbox = Rect(self.man.position.x, self.man.position.y, self.man.size.w -50, self.man.size.h - 50)
for item in list(self.items):
if item.frame.intersects(player_hitbox):
if isinstance(item, Meteor):
self.player_hit(self.man.position)
elif not item.parent:
self.items.remove(item)
▼アイテムのスポーン:アイテムをランダムスポーンさせるため、スポーンするタイミングやスポーン位置をランダムで追加していきます。
def spawn_item(self):
if random.random() < 0.3:
meteor = Meteor(parent = self)
meteor.position = (random.uniform(-self.size.w, self.size.w * 2), self.size.h + 30)
rotate = Action.repeat(Action.rotate_by(2 * pi, 2.0, TIMING_LINEAR), 4)
actions = [rotate, Action.remove()]
meteor.run_action(Action.sequence(actions))
self.items.append(meteor)
▼キャラ移動時の反転:キャラ移動に伴う振り向きを"self.man.x_scale = -1"で反転させることができます。今回は画面を横向きで重力センサーを用いているので、左に傾けたときに反転させるようにしています。
g = gravity()
self.man.x_scale = -((g.y > 0) - (g.y < 0))
▼音声処理:効果音は"sound.play_effect()"で呼び出します。パラメータは以下の要素で指定します。
sound.play_effect('arcade:Explosion_4', 1.0, 1.0)
パラメータの要素
name | 音声データ |
volume | ボリューム |
pitch | 再生スピード |
pan | 音の方向を-1.0~1.0で設定。(左:-1.0, 右:1.0) |
looping | 音声のループ設定。デフォルトではFalse |
▼スクリプト全体です。一応丁寧に書いたつもりですが、見にくい部分あるかもしれません。
# coding: utf-8
from scene import *
import random
from math import *
import sound
#主人公
class Man(SpriteNode):
distance_running = 0.0
standing_texture = Texture('plf:AlienPink_stand')
walk_textures = [Texture('plf:AlienPink_walk1'), Texture('plf:AlienPink_walk2')]
walk_step = -1
jmp_flg1 = False
jmp_flg2 = False
jmp = 0.0
#隕石
class Meteor(SpriteNode):
def __init__(self, **kwargs):
SpriteNode.__init__(self, 'spc:MeteorBrownBig2', **kwargs)
self.direction = random.uniform(5.0, 10.0)
#画面
class Game (Scene):
#初期設定
def setup(self):
#背景描画
self.background = Node()
self.background.add_child(SpriteNode('plf:BG_Blue_land', position=(0, self.size.h / 2), size = self.size))
self.background.add_child(SpriteNode('plf:BG_Blue_land', position=(self.size.w, self.size.h / 2),size = self.size))
#パネル地面設置
x = -self.size.w
while x <= self.size.w + self.size.w:
tile = SpriteNode('plf:Ground_GrassMid', position=(x, 64))
tile.size *= 2
self.background.add_child(tile)
x += 128
self.add_child(self.background)
#キャラ作成
self.man = Man(parent=self)
self.man.anchor_point = (0.5, 0)
self.man.position = (self.size.w/2, 128)
self.add_child(self.man)
#スコア作成
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.items = []
self.game_over = False
self.max_speed = 40
#NewGame
def new_game(self):
for item in self.items:
item.remove_from_parent()
self.items = []
self.score = 0
self.man.distance_running = 0
self.walk_step = -1
self.man.alpha = 1.0
self.man.position = (self.size.w/2, 128)
self.game_over = False
#更新
def update(self):
if not self.game_over:
if(not self.man.jmp_flg1):
self.update_running()
if(self.man.jmp_flg1):
self.update_jumping()
self.update_score()
self.update_jump()
self.update_items()
self.check_collision()
if random.random() < 0.05 * 10:
self.spawn_item()
def update_running(self):
g = gravity()
if abs(g.y) > 0.05:
self.man.x_scale = -((g.y > 0) - (g.y < 0))
self.man.distance_running = self.man.distance_running - g.y * self.max_speed
step = int(self.man.distance_running / self.max_speed) % 2
if step != self.man.walk_step:
self.man.texture = self.man.walk_textures[step]
sound.play_effect('rpg:Footstep00', 0.20, 1.0 + 0.5 * step)
self.man.walk_step = step
self.update_background(-g.y)
else:
self.man.texture = self.man.standing_texture
self.man.walk_step = -1
def update_jumping(self):
g = gravity()
if abs(g.y) > 0.05:
self.man.x_scale = -((g.y > 0) - (g.y < 0))
self.update_background(-g.y)
self.man.distance_running = self.man.distance_running - g.y * 60
def update_score(self):
self.score_label.text = str(math.floor(self.man.distance_running/100)) + 'm'
def update_background(self, v):
if(abs(self.background.position.x) <= self.size.w / 2):
self.background.position -= (v * 30, 0)
elif(self.background.position.x > self.size.w / 2):
self.background.position = (-self.size.w / 2, 0)
else:
self.background.position = (self.size.w / 2, 0)
def update_jump(self):
if(self.man.jmp > 0):
self.man.position += (0.0, self.man.jmp)
self.man.jmp -= 0.5
if(self.man.jmp_flg1 and self.man.jmp <= 0):
self.man.jmp -= 1.0
self.man.position += (0.0, self.man.jmp)
if(self.man.position.y <= 128):
self.man.position = (self.size.w / 2, 128)
self.man.jmp = 0.0
self.man.jmp_flg1 = False
self.man.jmp_flg2 = False
sound.play_effect('drums:Drums_01', 1.0, 1.0)
def update_items(self):
g = gravity()
for item in self.items:
direction = item.direction
item.position += (g.y * self.max_speed, -direction)
def spawn_item(self):
if random.random() < 0.3:
meteor = Meteor(parent = self)
meteor.position = (random.uniform(-self.size.w, self.size.w * 2), self.size.h + 30)
rotate = Action.repeat(Action.rotate_by(2 * pi, 2.0, TIMING_LINEAR), 4)
actions = [rotate, Action.remove()]
meteor.run_action(Action.sequence(actions))
self.items.append(meteor)
def check_collision(self):
player_hitbox = Rect(self.man.position.x, self.man.position.y, self.man.size.w -50, self.man.size.h - 50)
for item in self.items:
if item.frame.intersects(player_hitbox):
if isinstance(item, Meteor):
self.player_hit(self.man.position)
elif not item.parent:
self.items.remove(item)
def player_hit(self, position):
self.game_over = True
sound.play_effect('arcade:Explosion_4', 1.0, 1.0)
explosion = SpriteNode('shp:Explosion00', position, size = (50, 50))
actions = [Action.scale_to(10.0, 1.0, TIMING_LINEAR), Action.fade_to(0.0, 1.0, TIMING_LINEAR)]
explosion.run_action(Action.group(actions))
self.add_child(explosion)
self.man.alpha = 0.0
for item in list(self.items):
item.remove_from_parent()
self.run_action(Action.sequence(Action.wait(1.0), Action.call(self.new_game)))
def touch_began(self, touch):
if(not self.man.jmp_flg1 and not self.game_over):
sound.play_effect('game:Woosh_1', 1.0, 1.0)
self.man.jmp = 10.0
self.man.jmp_flg1 = True
elif(self.man.jmp_flg1 and not self.man.jmp_flg2):
sound.play_effect('game:Woosh_1', 1.0, 1.0)
self.man.jmp = 10.0
self.man.jmp_flg2 = True
if __name__ == '__main__':
run(Game(), LANDSCAPE, show_fps=True)
飛んでくる星を避けてコインを取るゲーム
▼1個目のゲームの内容をほぼ流用していますが、無限ジャンプで星を避けながらコインを取りスコアを稼ぎます。

▼アイテム獲得処理:アイテムの獲得処理は実は衝突処理と全く同じ処理です。アイテムごとに動作を決めればOKです。
def check_collision(self):
player_hitbox = Rect(self.man.position.x, self.man.position.y, self.man.size.w -100, self.man.size.h - 100)
for item in list(self.items):
if item.frame.intersects(player_hitbox):
if isinstance(item, Star):
self.player_hit(self.man.position)
elif isinstance(item, Coin):
self.get_item(item)
elif isinstance(item, Food):
self.get_item(item)
elif not item.parent:
self.items.remove(item)
▼スクリプト全体です。一応丁寧に書いたつもりですが、見にくい部分あるかもしれません。
from scene import *
import random
from math import *
import sound
#主人公
class Man(SpriteNode):
def __init__(self, **kwargs):
SpriteNode.__init__(self, 'plf:AlienBlue_stand', **kwargs)
stand_texture = Texture('plf:AlienBlue_stand')
walk_textures = [Texture('plf:AlienBlue_walk1'), Texture('plf:AlienBlue_walk2')]
jmp_flg = False
jmp = 0.0
score = 0
jmp_count = 0
#隕石
class Star(SpriteNode):
def __init__(self, **kwargs):
SpriteNode.__init__(self, 'emj:Star_1', **kwargs)
self.direction = random.uniform(5.0, 10.0)
#コイン
class Coin (SpriteNode):
def __init__(self, **kwargs):
SpriteNode.__init__(self, 'plf:Item_CoinGold', **kwargs)
self.direction = random.uniform(5.0, 10.0)
#食べ物
class Food (SpriteNode):
def __init__(self, **kwargs):
SpriteNode.__init__(self, 'emj:Meat_On_Bone', **kwargs)
self.direction = 2.0
#画面
class Game (Scene):
#初期設定
def setup(self):
#背景
self.background = Node()
self.background.add_child(SpriteNode('plf:BG_Blue_shroom', position=(0, self.size.h / 2), size = self.size))
self.background.add_child(SpriteNode( 'plf:BG_Blue_shroom', position=(self.size.w, self.size.h / 2),size = self.size))
#地面設置
x = -self.size.w
while x <= self.size.w + self.size.w:
tile = SpriteNode('plf:Ground_GrassMid', position=(x, 64))
tile.size *= 2
self.background.add_child(tile)
x += 128
self.add_child(self.background)
#キャラ作成
self.man = Man(parent=self)
self.man.texture = self.man.stand_texture
self.man.anchor_point = (0.5, 0)
self.man.position = (self.size.w / 10, 128)
self.add_child(self.man)
#スコア作成
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.items = []
self.game_over = False
self.scroll_speed = 2
#NewGame
def new_game(self):
for item in self.items:
item.remove_from_parent()
self.items = []
self.man.alpha = 1.0
self.man.score = 0
self.man.texture = self.man.stand_texture
self.man.position = (self.size.w / 10, 128)
self.game_over = False
#更新
def update(self):
if not self.game_over:
self.update_background()
self.update_score()
self.update_jump()
self.update_items()
self.check_collision()
self.update_walking()
self.spawn_item()
def update_walking(self):
self.man.score += 10
if(not self.man.jmp_flg):
step = int(self.man.score / 100) % 2
self.man.texture = self.man.walk_textures[step]
def update_score(self):
self.score_label.text = 'Score' + ' : ' + str(math.floor(self.man.score))
#背景移動
def update_background(self):
if(abs(self.background.position.x) <= self.size.w / 2):
self.background.position -= (self.scroll_speed, 0)
elif(self.background.position.x > self.size.w / 2):
self.background.position = (-self.size.w / 2, 0)
else:
self.background.position = (self.size.w / 2, 0)
#ジャンプ処理
def update_jump(self):
if self.man.jmp > 0:
if self.man.position.y + self.man.jmp < self.size.h:
self.man.position += (0.0, self.man.jmp)
self.man.jmp -= 0.5
else:
self.man.position = (self.size.w / 10, self.size.h)
self.man.jmp = 0
if self.man.jmp_flg and self.man.jmp <= 0 :
self.man.jmp -= 1.0
self.man.position += (0.0, self.man.jmp)
if(self.man.position.y <= 128):
self.man.position = (self.size.w / 10, 128)
self.man.jmp = 0.0
self.man.jmp_flg = False
sound.play_effect('drums:Drums_01', 1.0, 1.0)
#アイテム移動
def update_items(self):
for item in self.items:
direction = item.direction
item.position -= (direction, 0)
#スポーン
def spawn_item(self):
if random.random() < 0.05:
star = Star(parent = self)
star.position = (self.size.w + 120, random.uniform(128, self.size.h))
star.run_action(Action.repeat(Action.rotate_by(2 * pi, 2.0, TIMING_LINEAR), 4))
self.items.append(star)
if random.random() < 0.10:
coin = Coin(parent = self)
coin.position = (self.size.w + 128, random.uniform(128, self.size.h))
coin.run_action(Action.repeat(Action.rotate_by(2 * pi, 2.0, TIMING_LINEAR), 4))
self.items.append(coin)
if random.random() < 0.001:
food = Food(parent = self)
food.anchor_point = (0.5, 0)
food.position = (self.size.w + 128, 128)
self.items.append(food)
#衝突判定
def check_collision(self):
player_hitbox = Rect(self.man.position.x, self.man.position.y, self.man.size.w -100, self.man.size.h - 100)
for item in self.items:
if item.frame.intersects(player_hitbox):
if isinstance(item, Star):
self.player_hit(self.man.position)
elif isinstance(item, Coin):
self.get_item(item)
elif isinstance(item, Food):
self.get_item(item)
elif not item.parent:
self.items.remove(item)
#アイテムゲット
def get_item(self, item):
sound.play_effect('digital:PowerUp7')
item.remove_from_parent()
self.items.remove(item)
self.man.score += 1000
#衝突
def player_hit(self, position):
self.explosion(position)
self.game_over = True
self.man.alpha = 0.0
for item in list(self.items):
item.remove_from_parent()
self.run_action(Action.sequence(Action.wait(1.0), Action.call(self.new_game)))
#爆発
def explosion(self, position):
sound.play_effect('arcade:Explosion_4', 1.0, 1.0)
explosion = SpriteNode('shp:Explosion00', position, size = (50, 50))
actions = [Action.scale_to(10.0, 1.0, TIMING_LINEAR), Action.fade_to(0.0, 1.0, TIMING_LINEAR)]
explosion.run_action(Action.group(actions))
self.add_child(explosion)
#タッチ処理
def touch_began(self, touch):
if(not self.game_over):
sound.play_effect('game:Woosh_1', 1.0, 1.0)
self.man.jmp = 10.0
self.man.jmp_flg = True
self.man.jmp_count += 1
self.man.texture = self.man.walk_textures[self.man.jmp_count % 2]
if __name__ == '__main__':
run(Game(), LANDSCAPE, show_fps=True)
まとめ
ゲームといっても単純なものしか作っていません。もう少し凝ったゲームを作ろうと思ったのですが、一人でマップを作ったり背景画像を作ったりするのはモチベ上がらず大変だったのでやめました。
時間をかければ結構なゲームを作り出せる可能性を感じましたが、手っ取り早くクオリティの高いゲームを作りたいならUnity使ったほうが良い気がしますね。
次回は便利アプリに挑戦したいと思います。