Pythonでブラウザゲームを作るために、まずは先日公開したenchant.js製ブラウザゲーム「斜方蹴射」をPythonで作り直してみました。
目次
Pythonistaを使用
開発に使用したアプリは「Pythonista 3」です。
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を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を使って作っていきます!
リンク
コメント
コメント一覧 (2件)
[…] […]
[…] 2021.12.25 2021.04.16 【Python】斜方蹴射をPythonistaで作ってみた(コードあり)Pythonでブラウザゲ… […]