Python

Pythonista3でアクションゲームをつくってみる【iOSアプリ開発】

2020年3月26日

ゲームを作るならRPGツクール的なツールやUnityを使った方が何倍も簡単に作れるのですが、なんとなくPythonistaで作ってみようと思いました。

マップ作成やゲーム性を考えるのも大変だったので、非常に簡単に1画面のみで敵を倒すだけとなっています。

ゲーム紹介

▼左にアタックボタン、右に移動ボタンがあります。敵に攻撃すると消滅し、敵に当たるとノックバックします。

▼スクリプト全体です。使用している関数は以前に紹介したゲーム開発に用いたものを流用しています。

from scene import *
import math
import Map
import sound
import random

#主人公
class Chara(SpriteNode):
	def __init__(self, **kwargs):
		SpriteNode.__init__(self, 'plf:AlienYellow_stand', **kwargs)
		
	walk_textures = [Texture('plf:AlienYellow_walk1'), Texture('plf:AlienYellow_walk2')]
	hit_texture = Texture('plf:AlienYellow_hit')
	move_flg = False
	move_direction = (0, 0)
	
	def stand_on(self):
		self.texture = Texture('plf:AlienYellow_stand')
		
#剣
class Sword(SpriteNode):
	def __init__(self, **kwargs):
		SpriteNode.__init__(self, 'plf:SwordSilver', **kwargs)
	
#敵キャラ
class SlimeEnemy(SpriteNode):
	def __init__(self, **kwargs):
		SpriteNode.__init__(self, 'plf:Enemy_SlimeBlue', **kwargs)
		
	walk_textures = [Texture('plf:Enemy_SlimeBlue'), Texture('plf:Enemy_SlimeBlue_move')]
	hit_texture = Texture('plf:Enemy_SlimeBlue_dead')
	move_flg = True
	
	def move_on(self):
		self.move_flg = True
	
#石床
class StoneTile(SpriteNode):
	def __init__(self, **kwargs):
		SpriteNode.__init__(self, 'plf:Ground_StoneCenter', **kwargs)
	
#土床
class GrassTile(SpriteNode):
	def __init__(self, **kwargs):
		SpriteNode.__init__(self, 'plf:Ground_GrassCenter', **kwargs)

#上ボタン
class UpButton(SpriteNode):
	def __init__(self, **kwargs):
		SpriteNode.__init__(self, 'typb:Up', **kwargs)

#下ボタン		
class DownButton(SpriteNode):
	def __init__(self, **kwargs):
		SpriteNode.__init__(self, 'typb:Down', **kwargs)

#左ボタン		
class LeftButton(SpriteNode):
	def __init__(self, **kwargs):
		SpriteNode.__init__(self, 'typb:Left', **kwargs)

#右ボタン
class RightButton(SpriteNode):
	def __init__(self, **kwargs):
		SpriteNode.__init__(self, 'typb:Right', **kwargs)

#アタックボタン		
class AttackButton(SpriteNode):
	def __init__(self, **kwargs):
		SpriteNode.__init__(self, 'typb:Forward', **kwargs)

#画面
class GameScene(Scene):
	
	def setup(self):
		self.controll_buttons = []
		self.map_tiles = []
		self.enemys = []
		self.create_map()
		self.create_chara()
		for i in range(20):
			self.create_enemy()
		self.create_controller()
		
	def update(self):
		if self.my_chara.move_flg:
			self.chara_move()
		self.enemy_move()
		self.collision_check()

	#コントローラー作成
	def create_controller(self):
		up_button = UpButton(position = (self.size.w * 0.85, self.size.h * 0.2 + 80), size = (100, 100))
		down_button = DownButton(position = (self.size.w * 0.85, self.size.h * 0.2 - 80), size = (100, 100))
		left_button = LeftButton(position = (self.size.w * 0.85 - 80, self.size.h * 0.2), size = (100, 100))
		right_button = RightButton(position = (self.size.w * 0.85 + 80, self.size.h * 0.2), size = (100, 100))
		attack_button = AttackButton(position = (self.size.w * 0.15, self.size.h * 0.2), size = (100, 100))
		self.controll_buttons = [up_button, down_button, left_button, right_button, attack_button]
		for i in [0, 1, 2, 3, 4]:
			self.add_child(self.controll_buttons[i])

	#キャラ作成
	def create_chara(self):
		self.my_chara = Chara(position = self.size / 2, anchor_point = (0.5, 0))
		self.add_child(self.my_chara)
		
	#敵生成
	def create_enemy(self):
		slime_enemy = SlimeEnemy(position = (random.uniform(0, self.size.w), random.uniform(0, self.size.h)),anchor_point = (0.5, 0))
		self.enemys.append(slime_enemy)
		self.add_child(slime_enemy)
		
	#マップ作成	
	def create_map(self):
		self.spsize = Size(self.size.width / 39, self.size.width /39)
		self.d = self.spsize / 2
		#ベースとなるノードを用意
		self.board = Node()
		self.board.size = Size(self.spsize.width * 7, self.spsize.height * 25)
		self.add_child(self.board)
		#ゲームデータの用意
		self.data = Map.data
		#ボード作成
		y = -1
		for raw in self.data:
			y += 1
			x = -1
			for cell in raw:
				x += 1
				if self.data[y][x] == 1:
					block = GrassTile()
				elif self.data[y][x] == 2:
					block = StoneTile()
				block.size = self.spsize
				block.position = Point(self.spsize.width * x + self.d.width, self.spsize.height * y + self.d.height)
				self.board.add_child(block)
				self.map_tiles.append(block)
				
	#キャラ移動			
	def chara_move(self):
		self.my_chara.position += self.my_chara.move_direction
		texture = self.my_chara.walk_textures[int((self.my_chara.position.x + self.my_chara.position.y) / 50) % 2]
		if not texture is self.my_chara.texture:
			self.foot_sound()
		self.my_chara.texture = texture
		
	#敵移動
	def enemy_move(self):
		for enemy in self.enemys:
			if enemy.move_flg:
				enemy.move_flg = False
				direction = (self.my_chara.position - enemy.position) / 50
				if random.uniform(0, 100) < 20:
					direction += (random.choice([-20, -10, 0]), random.choice([-20, -10, 0]))
				elif random.uniform(0, 100) < 40:
					direction += (random.choice([0, 10, 20]), random.choice([0, 10, 20]))
				enemy.x_scale = -((direction.x > 0) - (direction.x < 0))
				direction += enemy.position
				enemy.texture = enemy.walk_textures[int((enemy.position.x + enemy.position.y) / 50) % 2]
				enemy.run_action(Action.sequence(Action.move_to(direction.x, direction.y, 0.5), Action.call(enemy.move_on)))

	#攻撃動作
	def attack_action(self):
		self.my_chara.stand_on()
		self.sword = Sword(size = (100, 100), anchor_point = (0.5, 0))
		if self.my_chara.x_scale == 1:
			self.sword.position = (self.my_chara.position.x + 15, self.my_chara.position.y + 20)
			action = [Action.rotate_by(-math.pi + 0.8, 0.2), Action.remove()]
		elif self.my_chara.x_scale == -1:
			self.sword.position = (self.my_chara.position.x - 15, self.my_chara.position.y + 20)
			action = [Action.rotate_by(math.pi * 0.8, 0.2), Action.remove()]
		self.sword.run_action(Action.sequence(action))
		self.add_child(self.sword)
		sound.play_effect('game:Woosh_1')
		self.attack_judge()
		
	#攻撃判定
	def attack_judge(self):
		if self.my_chara.x_scale == 1:
			sword_hitbox = Rect(self.sword.position.x, self.sword.position.y, 80, 100)
		elif self.my_chara.x_scale == -1:
			sword_hitbox = Rect(self.sword.position.x - 80, self.sword.position.y, 80, 100)
		for enemy in self.enemys:
			if enemy.frame.intersects(sword_hitbox):
				if isinstance(enemy, SlimeEnemy):
					sound.play_effect('arcade:Explosion_6')
					#self.knock_back(enemy)
					self.item_remove(enemy)
	
	#アイテム削除
	def item_remove(self, object):
		if isinstance(object, SlimeEnemy):
			self.enemys.remove(object)
			object.remove_from_parent()
			#self.create_enemy()
			
	#当たり判定
	def collision_check(self):
		player_hitbox = Rect(self.my_chara.position.x, self.my_chara.position.y, self.my_chara.size.w - 50, self.my_chara.size.h - 50)
		for enemy in self.enemys:
			if enemy.frame.intersects(player_hitbox):
				if isinstance(enemy, SlimeEnemy):
					sound.play_effect('arcade:Explosion_7')
					self.knock_back(self.my_chara)
								
	#ノックバック
	def knock_back(self, object):
		object.move_flg = False
		object.texture = object.hit_texture
		if isinstance(object, SlimeEnemy):
			direction = object.position - self.my_chara.position
			object.run_action(Action.sequence(Action.move_by(direction.x, direction.y, 0.5), Action.wait(1.0), Action.call(object.move_on)))
		elif isinstance(object, Chara):
			object.run_action(Action.sequence(Action.move_to(object.position.x - 150 * object.x_scale, object.position.y, 0.5), Action.call(object.stand_on)))
			
	#歩行音
	def foot_sound(self):
		player_hitbox = Rect(self.my_chara.position.x, self.my_chara.position.y, self.my_chara.size.w -100, self.my_chara.size.h - 100)
		for tile in self.map_tiles:
			if tile.frame.intersects(player_hitbox):
				if isinstance(tile, StoneTile):
					sound.play_effect('rpg:Footstep00')
				elif isinstance(tile, GrassTile):	
					sound.play_effect('rpg:Footstep05')
				break
			
	def touch_began(self, touch):
		touch_loc = self.point_from_scene(touch.location)
		for button in self.controll_buttons:
			if touch_loc in button.frame:
				if isinstance(button, UpButton):
					self.my_chara.move_flg = True
					self.my_chara.move_direction = (0, 10)
				if isinstance(button, DownButton):
					self.my_chara.move_flg = True
					self.my_chara.move_direction = (0, -10)					
				if isinstance(button, LeftButton):
					self.my_chara.move_flg = True
					self.my_chara.move_direction = (-10, 0)
					self.my_chara.x_scale = -1
				if isinstance(button, RightButton):
					self.my_chara.move_flg = True
					self.my_chara.move_direction = (10, 0)
					self.my_chara.x_scale = 1
				if isinstance(button, AttackButton):
					self.attack_action()
				
	def touch_ended(self, touch):
		touch_loc = self.point_from_scene(touch.location)
		for button in self.controll_buttons:
			if touch_loc in button.frame:
				if not isinstance(button, AttackButton):
					self.my_chara.move_flg = False
					self.my_chara.stand_on()
						
run(GameScene(), PORTRAIT, frame_interval = 2)

▼マップデータは長くなってしまったので別ファイルで作っています。

data = [
			[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
			[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
			[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
			[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
			[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
			[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
			[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
			[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
			[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
			[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
			[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
			[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
			[2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
			[2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
			[2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
			[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
			[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
			[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
			[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
			[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
			[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
			[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
			[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
			[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
			[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
			[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
			[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
			[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],	
			[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
			[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
			]

まとめ

スクリプト自体は以前にも紹介したゲーム開発で使った関数を流用しているので、そこまで大変ではなかったです。

ただPythonista3で開発してもPythonista経由でしか配布はできないですし、すぐに飽きてしまうので作り込むにはモチベがあまり上がりませんでした。

これからは便利アプリでも作ってみたいなと思います。

合わせて読みたい
合わせて読みたい

よく読まれている記事

1

DeepFaceLab 2.0とは DeepFaceLab 2.0は機械学習を利用して動画の顔を入れ替えるツールです。 以前にDeepFaceLab 1.0を記事としてアップしていましたが、2.0は以 ...

2

自作PCで、多くのパーツをCorsair製品で揃えたので、iCUEでライティング制御していきました。 私のPCでは、表示されている4つのパーツが制御されています。ここで、HX750i電源ユニットは、L ...

3

コンピュータは有限桁の数値しか扱う事はできないので、桁数の多い場合や無限小数の場合は四捨五入され切り捨てられます。なので実際の数値とは多少の誤差が生じますが、これを丸め誤差といいます。 なので、コンピ ...

-Python