激ムズ。2014年頃に流行ったらしい。
成果物
コード
コード
#!/usr/bin/env python3 # coding: utf8 import os, enum, random, pyxel from abc import ABCMeta, abstractmethod class App: def __init__(self): self.__window = Window() globals()['Window'] = self.__window self.__scene = SceneManager() pyxel.run(self.update, self.draw) def update(self): self.__scene.update() def draw(self): self.__scene.draw() class Window: def __init__(self): pyxel.init(self.Width, self.Height, border_width=self.BorderWidth, caption=self.Caption, fps=60) @property def Width(self): return 64 @property def Height(self): return 96 @property def Caption(self): return "Ping Pong" @property def BorderWidth(self): return 0 def update(self): pass def draw(self): pyxel.cls(0) class SceneType(enum.IntEnum): Start = 0 Play = 1 Score = 2 class SceneManager: def __init__(self): self.__scenes = [StartScene(), PlayScene(), ScoreScene()] self.__now = SceneType.Start def init(self, *args, **kwargs): pass def update(self): next_scene = self.__scenes[self.__now].update() if isinstance(next_scene, SceneType): self.__now = next_scene self.__scenes[self.__now].init() elif isinstance(next_scene, tuple) and isinstance(next_scene[0], SceneType): self.__now = next_scene[0] if 2 <= len(next_scene): self.__scenes[self.__now].init(*next_scene[1]) elif 3 <= len(next_scene): self.__scenes[self.__now].init(*next_scene[1], **next_scene[2]) else: self.__scenes[self.__now].init() def draw(self): self.__scenes[self.__now].draw() class Scene(metaclass=ABCMeta): @abstractmethod def init(self, *args, **kwargs): pass @abstractmethod def update(self): pass @abstractmethod def draw(self): pass class StartScene(Scene): def __init__(self): self.__pc = PC() def init(self, *args, **kwargs): pass def update(self): if pyxel.btn(pyxel.KEY_SPACE): return SceneType.Play def draw(self): self.__pc.draw() pyxel.text(Window.Width // 2 - (4*16/2), Window.Height // 2 - (8*2), 'Push SPACE key !', 7) class BestScore: def __init__(self): self.__file_name = 'BEST' self.__file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), self.__file_name) self.__load() @property def Score(self): return self.__score @Score.setter def Score(self, value): if self.__score < value: self.__score = value self.__save() def __load(self): if os.path.isfile(self.__file_path): with open(self.__file_path, 'r') as f: self.__score = int(f.read()) else: self.__score = 0 def __save(self): with open(self.__file_path, 'w') as f: f.write(str(self.__score)) class ScoreScene(Scene): def __init__(self): self.__best = BestScore() self.__pc = PC() self.__now = 0 def init(self, *args, **kwargs): self.__now = args[0] self.__best.Score = self.__now def update(self): if pyxel.btn(pyxel.KEY_R): return SceneType.Play def draw(self): x = Window.Width // 2 - (4*16/2) + 2 y = Window.Height // 2 - (8/2) - (8 * 4 / 2) pyxel.rect(x, y, Window.Width, 8 * 4 + 2, 4) pyxel.text(x+2, y+2, 'SCORE', 7) pyxel.text(x+2+(6*4)+self.__right_align(), y+2, str(self.__now), 7) pyxel.text(x+2, y+2+8, 'BEST', 7) pyxel.text(x+2+(6*4), y+2+8, str(self.__best.Score), 7) pyxel.text(x+2, y+2+24, 'Push R key', 7) def __right_align(self): return ( len(str(self.__best.Score)) - len(str(self.__now)) ) * 4 class PlayScene(Scene): def __init__(self): self.init() def init(self, *args, **kwargs): self.__pc = PC() self.__blocks = [Block(self.__pc.countup), Block(self.__pc.countup)] self.__blocks[1].X = self.__blocks[0].X + (Window.Width // 2) + (self.__blocks[0].W // 2) self.__is_gameover = False def update(self): if self.__is_gameover: return SceneType.Score, [self.__pc.Count] self.__detect_collision() self.__pc.update() for b in self.__blocks: b.update() def draw(self): pyxel.cls(0) for b in self.__blocks: b.draw() self.__pc.draw() pyxel.text(Window.Width // 2 - (len(str(self.__pc.Count)) * 4 // 2), 8, str(self.__pc.Count), 3) if self.__is_gameover: pyxel.text(Window.Width // 2 - (4*9/2), Window.Height // 2, 'Game Over', 7) def __detect_collision(self): self.__detect_collision_window() self.__detect_collision_block() def __detect_collision_window(self): if self.__pc.Y < 0 or Window.Height < self.__pc.Y: self.__is_gameover = True def __detect_collision_block(self): for b in self.__blocks: if (b.X <= self.__pc.X and \ ((self.__pc.Y - self.__pc.R <= b.SafeY) or \ (b.SafeY + b.SafeH <= self.__pc.Y + self.__pc.R))): self.__is_gameover = True class SizeObject: def __init__(self): self.__w = 8 self.__h = 8 self.__x = self.W // 2 self.__y = Window.Height // 2 self.__color = 7 @property def X(self): return self.__x @property def Y(self): return self.__y @property def W(self): return self.__w @property def H(self): return self.__h @property def Color(self): return self.__color @X.setter def X(self, value): self.__x = value @Y.setter def Y(self, value): self.__y = value @W.setter def W(self, value): self.__w = value @H.setter def H(self, value): self.__h = value @Color.setter def Color(self, value): if -1 < value < 16: self.__color = value class PC(SizeObject): JumpV = -4 def __init__(self): super(self.__class__, self).__init__() self.W = 8 self.H = 8 self.X = self.W // 2 self.Y = Window.Height // 2 self.Color = 7 self.__r = 4 self.__vy = 0 self.__junpping = False self.__count = -2 @property def R(self): return self.__r @property def Count(self): return self.__count def update(self): self.__move() def __move(self): if 0 == pyxel.frame_count % 3: if pyxel.btn(pyxel.KEY_SPACE): self.__vy = self.__class__.JumpV self.__vy += 1 self.Y = self.Y + self.__vy def draw(self): pyxel.circ(self.X, self.Y, self.R, self.Color) def countup(self): self.__count += 1 class Block(SizeObject): def __init__(self, on_next): super(self.__class__, self).__init__() self.W = 12 self.H = Window.Height self.X = Window.Width self.Y = 0 self.Color = 10 self.__on_next = on_next self.__next() @property def SafeH(self): return self.__safe_h @property def SafeY(self): return self.__safe_y def update(self): if 0 == pyxel.frame_count % 3: self.X = self.X - 1 if self.X + self.W <= 0: self.__next() def __next(self): self.__on_next() self.X = Window.Width self.__safe_h = random.randint(0, abs(PC.JumpV) // 2) + abs(PC.JumpV*6) self.__safe_y = random.randint(0, (Window.Height - self.__safe_h)) def draw(self): pyxel.rect(self.X, self.Y, self.W, self.H, self.Color) pyxel.rect(self.X, self.__safe_y, self.W, self.__safe_h, 0) def set_event_on_next(self, func): self.__on_next = func App()
キモ
ジャンプ
ジャンプの放物線を描く動きをどうやって計算しようか悩んだ。
サイン波と同じなので計算できるはずだが、三角関数など難しそうなことはさっぱりわからない。そこで単純に考えてみた。
- Y座標で高さにおける現在位置を示す
- 1フレームあたりの移動量は変動する
- ジャンプし始めは大きく移動する
- 次第にゆるやかになる
- そして落下
- つまり座標
y
とは別に「移動量vy
」の変数を用意する
という発想になった。ようするに加速度。公式なんて知らなくても考えてみれば思いつく。
class PC(SizeObject): JumpV = -4 ... def __move(self): if 0 == pyxel.frame_count % 3: if pyxel.btn(pyxel.KEY_SPACE): self.__vy = self.__class__.JumpV self.__vy += 1 self.Y = self.Y + self.__vy
上記だけだとvy
は無限に増大するが問題ない。「画面下に衝突したら死亡」してゲーム終了するため。
ブロック
上下にブロックが2つある。1つのインスタンスで済ませたい。そこで「セーフゾーン」の概念を考えた。
「上下にブロックが2つ」ではなく「中央にセーフゾーンが1つ」という考え方。コペルニクス的発想の転換!
絵的にいえば以下。背景が黒だからできる技。
- 垂直の黄色いブロックを描く(画面高さまで)
- 1の上に黒いブロックを描く(キャラが通り抜ける高さ分だけ)
ゲーム調整(ジャンプ&セーフゾーン)
「ムリそうでイケる」バランス調整。
- ジャンプ量
- ブロックのセーフゾーンにおける高さ
class Block(SizeObject): ... def __next(self): self.__on_next() self.X = Window.Width self.__safe_h = random.randint(0, abs(PC.JumpV) // 2) + abs(PC.JumpV*6)
何度もプレイしてみて調整した。
課題
クオリティを上げたい。
- 効果音
- 成功:くぐりぬけた
- 失敗:ぶつかった
- 他:
- ジャンプで飛ぶ音
- 死んで落下する音
- 画像
- 背景
- 地面
- ブロック
- プレイヤキャラ
- ジャンプ(斜め上)
- 水平
- 落下(斜め下)
所感
ついついやってしまう中毒性がある。私は11
点が最高だった。
対象環境
- Raspbierry pi 4 Model B
- Raspbian buster 10.0 2019-09-26 ※
- bash 5.0.3(1)-release
- Python 3.7.3
- pyxel 1.3.1
$ uname -a Linux raspberrypi 4.19.97-v7l+ #1294 SMP Thu Jan 30 13:21:14 GMT 2020 armv7l GNU/Linux