【Python】斜方蹴射をPythonistaで作ってみた(コードあり)

あわせて読みたい
【ゲーム】斜方蹴射 enchant.jsで作った、マウスクリックのみで操作できる的あてゲームです。

Pythonでブラウザゲームを作るために、まずは先日公開したenchant.js製ブラウザゲーム「斜方蹴射」をPythonで作り直してみました。

目次

Pythonistaを使用

開発に使用したアプリは「Pythonista 3」です。

App Store
‎Pythonista 3 ‎Pythonista is a complete scripting environment for Python 3.10, running right on your iPad or iPhone, so you can develop and run Python scripts on the go. Lik...

このアプリはスマホ上で簡単にPythonのコードを作成できるのですが、

  • 作成したアプリを実行ファイル形式で出力できない
  • ゲームライブラリが独自形式(Scene)

という欠点があります。

作成手順

Pythonistaを活用しつつ、最終的にはPyGameを使用したブラウザ実行環境を作るために、

  1. 独自ライブラリに依存しないゲームロジック部分
  2. ライブラリによる描画処理

にコードを分け、

  • 1,2をPythonistaで作る
  • 2だけをPyGameで作り直してブラウザゲーム化する

という手順でやっていくことにします。

1の部分をPythonistaで作れれば、描画以外の部分をスマホだけで作れるからです。

設計で言うところのviewとmodelの分離です。

コード

実際に動くコードを公開します。

動かしたい方は、Pythonistaをインストールしてコードをコピペしてください。

Model.py

Python標準ライブラリだけで作ったゲーム本体です。

# インポート
from enum import Enum
import random
#===========================
# ゲームモード
#===========================
class Mode(Enum):
	INIT 			= 0		# 初期
	POW_X 		= 1 	# Xパワー決定待ち
	POW_Y			= 2 	# Yパワー決定待ち
	FLYING  	= 3 	# 飛行中
	WAIT_HIT  = 4		# ヒット演出中
	WAIT_OUT  = 5   # 外れ演出中
	TIME_UP   = 6   # 時間切れ演出中
	RESULT		= 7   # リザルト
#===========================
# ゲーム本体
#===========================
class Game:
	# クラス変数
	max_pow_x = 100		# パワー横最大値
	max_pow_y = 100 	# パワー縦最大値
	max_time  = 60 # 制限時間
	init_ball_x = 20 	# ボール初期位置x
	init_ball_y = 22 	# ボール初期位置x
	waittime_hit = 1.0 # ヒット演出時間
	waittime_end = 2.0 # タイムアップ演出時間
	ground_h = 20			# 地面の高さ
	min_target_x = 150
	max_target_x = 350
	min_target_y = 150
	max_target_y = 500
	
	# コンストラクタ
	def __init__(self):
		print('Init Game')
		self.stage = Stage(Game.ground_h)
		self.reset()
		self.time	 = 0        # 残り時間
		self.mode = Mode.INIT # モード番号
		self.decide = False		# 決定操作がされたか
		self.wait_timer = 0		# 演出タイマー
		self.score = 0 				# スコア
		
	# 状態リセット
	def reset(self):
		self.ball = Ball()
		self.ball.reset(Game.init_ball_x, Game.init_ball_y)
		self.pow_x = 0
		self.pow_y = 0	
		self.stage.reset_wind()
		self.target = Target()
		self.target.set_pos(random.randint(Game.min_target_x, Game.max_target_x), random.randint(Game.min_target_y, Game.max_target_y))
		
	# 更新
	def update(self, delta):
		# 初期設定
		if self.mode == Mode.INIT:
			# インスタンス変数のリセット
			self.reset()
			self.score = 0
			self.time = Game.max_time
					
			print('mode->POW_X')
			self.mode = Mode.POW_X
			
		# 横パワー決定待ち
		elif self.mode == Mode.POW_X:
			# 残り時間更新
			self.time -= delta
			if self.time <= 0:
				self.time = 0
				self.wait_timer = Game.waittime_end
				self.mode = Mode.TIME_UP
				return 
			# 横パワー更新
			else:
				self.pow_x += 1
				if Game.max_pow_x < self.pow_x:
					self.pow_x = 0		
				if self.decide:
					print('pow_x=' + str(self.pow_x))
					self.mode = Mode.POW_Y
				
		# 縦パワー決定待ち
		elif self.mode == Mode.POW_Y:
			# 残り時間更新
			self.time -= delta
			if self.time <= 0:
				self.time = 0
				self.wait_timer = Game.waittime_end
				self.mode = Mode.TIME_UP	
			# 縦パワー更新			
			else:		
				self.pow_y += 1
				if Game.max_pow_y < self.pow_y:
					self.pow_y = 0
				
				if self.decide:
					print('pow_y=' + str(self.pow_y))
					self.ball.kick(self.pow_x, self.pow_y)
					self.mode = Mode.FLYING
				
		# ボール飛行中
		elif self.mode == Mode.FLYING:
			self.ball.update(delta, self.stage.wind)
			
			# 的に接触
			if self.target.is_hit(self.ball.x, self.ball.y):
				self.wait_timer = Game.waittime_hit
				self.mode = Mode.WAIT_HIT
			
			# 地面に接触
			elif self.ball.y <= Game.ground_h:
				print('hit_ground')
				self.ball.y = Game.ground_h
				self.wait_timer = Game.waittime_hit
				self.mode = Mode.WAIT_OUT
			
		# ヒット演出待ち
		elif self.mode == Mode.WAIT_HIT:
			self.wait_timer -= delta
			if self.wait_timer <= 0:
				print('end_hitwait')
				self.score += int(self.ball.time)
				self.reset()
				self.mode = Mode.POW_X
				
		# 外れ演出待ち
		elif self.mode == Mode.WAIT_OUT:
			self.wait_timer -= delta
			if self.wait_timer <= 0:
				print('end_outwait')
				self.reset()
				self.mode = Mode.POW_X
			
		# タイムアップ演出待ち
		elif self.mode == Mode.TIME_UP:
			self.wait_timer -= delta
			if self.wait_timer <= 0:
				self.wait_timer = 0
				print('end_timeup')
				self.mode = Mode.RESULT
			
		# リザルト
		elif self.mode == Mode.RESULT:
			if self.decide:
				self.mode = Mode.INIT
			
		self.decide = False	
		
	# 決定操作
	def decide(self):
		self.decide = True
		
	# ボール飛行中か
	def is_flying(self):
		return self.mode == Mode.FLYING
	
	# ヒット演出中か
	def is_hit(self):
		return self.mode == Mode.WAIT_HIT
	
	# 時間切れか
	def is_timeup(self):
		return self.mode == Mode.TIME_UP
		
	# リザルト表示中か
	def is_result(self):
		return self.mode == Mode.RESULT
#===========================
# ステージ
#===========================
class Stage:
	# クラス変数
	wind_range = (-10, 10) # 風速の範囲
	# コンストラクタ
	def __init__(self, ground_h):
		self.ground_h = ground_h # 地面の高さ
		self.wind = 0
	
	# 風速の再設定
	def reset_wind(self):
		(min, max) = Stage.wind_range
		self.wind = random.randint(min, max)
#===========================
# ボール
#===========================		
class Ball:
	# クラス変数
	size = 10			# 大きさ
	gravity = 0.4 # ボールにかかる重力
	wind_rate = 0.01 # 風の適用率
	pow_rate_x = 0.1  # パワー横の適用率
	pow_rate_y = 0.25  # パワー縦の適用率
	
	# コンストラクタ
	def __init__(self):
		self.reset()
		
	# リセット
	def reset(self, x=0, y=0):
		self.x = x		# X座標
		self.y = y		# Y座標
		self.sx = 0		# X速さ
		self.sy = 0		# Y速さ
		self.time = 0 # 飛行時間
	
	# キック	
	def kick(self, pow_x, pow_y):
		self.sx = pow_x * Ball.pow_rate_x
		self.sy = pow_y * Ball.pow_rate_y
		self.time = 0
		
	# 更新
	def update(self, delta, wind):
		self.sx += wind * Ball.wind_rate
		self.sy -= Ball.gravity
		self.x += self.sx
		self.y += self.sy
		self.time += delta * 10
#===========================
# 的
#===========================		
class Target:
	# クラス変数
	size_w = 20
	size_h = 120
	
	# コンストラクタ
	def __init__(self):
		self.set_pos(0,0)
		
	# 座標設定
	def set_pos(self, x,y):
		self.x = x
		self.y = y
				
	# 当たり判定
	def is_hit(self, x,y):
		if self.x < x and x < self.x + Target.size_w and self.y < y and y < self.y + Target.size_h:
			return True
		return False

描画機能は一切持たず、入力もdecide関数を呼び出して操作するようにしています。

こうすることで、ライブラリによって描画やタッチ操作の取得方法が異なってもModel側の修正が不要になります。

また、マウスクリックだけでなくEnterキーやゲームパッドのAボタンでも操作するようにもできます。

Main.py

PythonistaのゲームライブラリSceneを使用した、モデルの呼び出し&描画処理です。
※Pythonista上でしか動きません。

# インポート
import Model
from scene import *
#===========================
# ゲーム(ビュー)
#===========================
class Game(Scene):
	# セットアップ
	def setup(self):
		self.background_color = '#00acff'
		self.game = Model.Game()
		
	# 更新
	def update(self):	
		self.game.update(1.0/60.0)
		
	# 描画
	def draw(self):
		if self.game.is_result():
			self.draw_result()
		else:
			self.draw_main()
	
	# メインゲーム中の描画		
	def draw_main(self):
		scr_size = get_screen_size()
		
		# 地面
		fill(0,0,0)
		rect(0,0, scr_size.x, self.game.stage.ground_h)	
		
		# 横パワーゲージ
		fill(0,1,0)
		rect(0,10,self.game.pow_x / Model.Game.max_pow_x * scr_size.x,10)
		
		# 縦パワーゲージ
		fill(1,0,0)
		rect(0,0,self.game.pow_y/Model.Game.max_pow_y * scr_size.x,10)
		
		# 的
		fill(0,0,1)
		rect(self.game.target.x, self.game.target.y, Model.Target.size_w, Model.Target.size_h)
		
		# ボール
		fill(1,1,1)
		ellipse(self.game.ball.x, self.game.ball.y, Model.Ball.size, Model.Ball.size)
		
		# スコア
		tint(1,1,1)
		text('スコア=' + str(self.game.score), x=10, y=scr_size.y-32, alignment=6)
		
		# 飛行時間
		if self.game.is_flying() or self.game.is_hit():
			tint(1,1,0)
			text(str(int(self.game.ball.time)), x=200, y=scr_size.y-32, alignment=5)
		
		# 残り時間
		tint(1,1,1)
		text('残り時間=' + str(int(self.game.time)), x=10, y=scr_size.y-48, alignment=6)
		
		# 風
		tint(1,1,1)
		text('風=' + str(self.game.stage.wind), x=10, y=scr_size.y-64, alignment=6)	
		
		# ヒット演出中
		if self.game.is_hit():
			tint(1,0,0)
			text('HIT!', font_size=32, x=scr_size.x/2, y=scr_size.y/2, alignment=5)
			
		# 時間切れ
		if self.game.is_timeup():	
			tint(1,0,0)
			text('終了!', font_size=32, x=scr_size.x/2, y=scr_size.y/2, alignment=5)
		
	# リザルト画面の描画
	def draw_result(self):
		scr_size = get_screen_size()
		# 背景
		fill(0,0,0)
		rect(0,0, scr_size.x,scr_size.y)
		
		# スコア見出し
		tint(1,1,1)
		text('スコア', font_size=32, x=scr_size.x/2, y=scr_size.y/2+16, alignment=5)
		
		# スコア本体
		text(str(int(self.game.score)), font_size=32, x=scr_size.x/2, y=scr_size.y/2-16, alignment=5)
		
	# タッチ
	def touch_began(self, touch):
		self.game.decide = True
# インポート時に関数を呼ばない対応
if __name__ == '__main__':
	run(Game(), PORTRAIT)

次はブラウザゲーム化!

ひとまず、Pythonista上で動くところまではできました。

次は、Replit上でPyGameを使って作っていきます!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメント一覧 (2件)

コメントする

目次