「Wio Terminal の CircuitPython に Stage ライブラリを追加する」にて Wio Terminal に Stage ライブラリを組み込みましたが、今回はそれを使って PyBadge 用のゲーム Vacuum Invaders を Wio Terminal に移植してみます。
※ オリジナルの PyBadge でのプレイの様子はここで見れます。
PyBadge の画面が 160x128 であるのに対して、Wio Terminal は 320x240 なので、そのままだと画面の左上 1/4 ぐらいのスペースで動くゲームになってしまうので、ゲーム画面を広げて、エイリアン数も増やしてみました。
PyBadge にはまがりなりにもスピーカーがついていて、Stageライブラリでは wav ファイルからの効果音を鳴らすことができるのですが、Wio Terminal にはブザーしかないので、今回はsimpleio を使って音を出しています。
※ simpleio は CircuitPython Librariesに含まれています。
結果としてはこんなゲームになりました。
元のゲームソースにある tiles.bmpと以下の code.py、加えて simpleio ライブラリを Wio Terminal に転送しておけば遊べるはずです。
- code.py
import time
import random
from micropython import const
from simpleio import tone
import board
import stage
import ugame
GAME_FPS = const(34) # フレームレート
STAGE_WIDTH = const(15) # ゲームステージの幅(タイル単位)
STAGE_HEIGHT = const(15) # ゲームステージの高さ(タイル単位)
ALIENS_WIDTH = const(12) # エイリアングリッドの幅(タイル単位)
ALIENS_HEIGHT = const(5) # エイリアングリッドの高さ(タイル単位)
TILE_WIDTH_PIXELS = const(16) # 1タイルの幅(ピクセル単位)
TILE_HEIGHT_PIXELS = const(16) # 1タイルの硬さ(ピクセル単位)
STAGE_WIDTH_PIXELS = const(240) # STAGE_WIDTH * TILE_WIDTH_PIXELS
STAGE_HEIGHT_PIXELS = const(240) # STAGE_HEIGHT * TILE_HEIGHT_PIXELS
ALIENS_WIDTH_PIXELS = const(192) # ALIENS_WIDTH * TILE_WIDTH_PIXELS
ALIENS_HEIGHT_PIXELS = const(80) # ALIENS_HEIGHT * TILE_HIGHT_PIXELS
DEAD_LINE = const(160) # (STAGE_HEIGHT - ALIENS_HEIGHT) * TILE_HEIGHT_PIXELS
TILE_SPACE = const(1) # Bank内の宇宙画像のインデックス
TILE_STAR1 = const(2) # Bank内の星1画像のインデックス
TILE_STAR2 = const(3) # Bank内の星2画像のインデックス
TILE_SHIP = const(4) # Bank内の宇宙船画像のインデックス
TILE_BOMB = const(6) # Bank内の爆破画像のインデックス
TILE_ALIEN = const(8) # Bank内のエイリアン画像のインデックス
TILE_SAUCER = const(9) # Bank内の円盤画像のインデックス
TILE_MISSILE = const(12) # Bank内のミサイル画像のインデックス
tiles = stage.Bank.from_bmp16('tiles.bmp') # タイルやスプライトのための画像読み込み
text_area = stage.Text(9, 1) # テキスト表示エリアの用意(横文字数、縦文字数)
text_area.move(80, 120) # テキストエリアの表示位置
class Ship(stage.Sprite):
'''宇宙船スプライト。
'''
def __init__(self):
'''初期化。
'''
super(Ship, self).__init__(tiles, TILE_SHIP, 110, 214)
self.dx = 0 # 移動増分
self.x = 110 # 初期 x 位置
self.tick = 0 #
self.dead = False # やられたか
def update(self):
'''表示の更新。
'''
super(Ship, self).update()
self.tick = not self.tick
keys = ugame.buttons.get_pressed()
self.set_frame(4, 0 if self.tick else 4)
if keys & ugame.K_RIGHT:
self.dx = min(self.dx + 1, 4)
self.set_frame(5, 0)
elif keys & ugame.K_LEFT:
self.dx = max(self.dx - 1, -4)
self.set_frame(5, 4)
else:
self.dx = self.dx // 2
if keys & ugame.K_SELECT:
if missile.y <= -TILE_HEIGHT_PIXELS:
missile.move(self.x, self.y)
tone(board.BUZZER, 523.25, 0.005)
tone(board.BUZZER, 783.99, 0.005)
elif missile1.y <= TILE_HEIGHT_PIXELS:
missile1.move(self.x, self.y)
tone(board.BUZZER, 523.25, 0.005)
tone(board.BUZZER, 783.99, 0.005)
elif missile2.y <= TILE_HEIGHT_PIXELS:
missile2.move(self.x, self.y)
tone(board.BUZZER, 523.25, 0.005)
tone(board.BUZZER, 783.99, 0.005)
if keys & ugame.K_O:
pause(' Pause...')
self.x = max(min(self.x + self.dx, 224), 0)
self.move(self.x, self.y)
class Saucer(stage.Sprite):
'''円盤スプライト。
'''
def __init__(self):
'''初期化。
'''
super(Saucer, self).__init__(tiles, 9, 0, 0)
self.tick = 0
self.dx = 4
def update(self):
'''表示の更新。
'''
super(Saucer, self).update()
self.tick = (self.tick + 1) % 6
self.layer.frame(TILE_SAUCER, 0 if self.tick >= 3 else 4)
if self.x >= 240 or self.x <= -TILE_WIDTH_PIXELS:
self.dx = -self.dx
self.move(self.x + self.dx, self.y)
if abs(self.x - ship.x) < 4 and bomb.y >= 240:
bomb.move(self.x, self.y)
class Bomb(stage.Sprite):
'''爆破スプライト。
'''
def __init__(self):
'''初期化。
'''
super(Bomb, self).__init__(tiles, TILE_BOMB, 0, 240)
self.boom = 0
def update(self):
'''表示の更新。
'''
super(Bomb, self).update()
if self.y >= STAGE_HEIGHT_PIXELS:
return
if self.boom:
if self.boom == 1:
tone(board.BUZZER, 493.88, 0.005)
tone(board.BUZZER, 349.23, 0.005)
self.set_frame(12 + self.boom, 0)
self.boom += 1
if self.boom > 4:
self.boom = 0
ship.dead = True
self.move(self.x, 240)
return
self.move(self.x, self.y + 8)
self.set_frame(6, 0 if self.y % TILE_HEIGHT_PIXELS else 4)
if stage.collide(self.x + 4, self.y + 4, self.x + 12, self.y + 12,
ship.x + 4, ship.y + 4, ship.x + 12, ship.y + 12):
self.boom = 1
class Missile(stage.Sprite):
'''ミサイルスプライト。
'''
def __init__(self, power):
'''初期化。
'''
super(Missile, self).__init__(tiles, TILE_MISSILE, 0, -32)
self.boom = 0
self.power = power
def update(self):
'''表示の更新。
'''
super(Missile, self).update()
if self.boom:
if self.boom == 1:
tone(board.BUZZER, 493.88, 0.005)
tone(board.BUZZER, 349.23, 0.005)
self.set_frame(TILE_MISSILE + self.boom)
self.boom += 1
if self.boom > 4:
self.boom = 0
self.kill()
aliens.tile(self.ax, self.ay, 0)
aliens.dirty = True
return
if self.y <= -32:
return
self.move(self.x, self.y - 8)
self.set_frame(TILE_MISSILE - self.power,
0 if self.y % TILE_HEIGHT_PIXELS == 6 else 4)
self.ax = (self.x + 8 - aliens.x) // TILE_WIDTH_PIXELS
self.ay = (self.y + 4 - aliens.y) // TILE_HEIGHT_PIXELS
if (aliens.tile(self.ax, self.ay)
and (self.x + 10 - aliens.x) % TILE_WIDTH_PIXELS > 4):
aliens.tile(self.ax, self.ay, 7)
self.move(self.x, self.y - 4)
self.boom = 1
def kill(self):
'''
'''
self.move(self.x, -32)
self.set_frame(12 - self.power)
class Aliens(stage.Grid):
'''エイリアングリッド。
'''
def __init__(self):
'''初期化。
'''
super(Aliens, self).__init__(tiles, ALIENS_WIDTH, ALIENS_HEIGHT)
for y in range(ALIENS_HEIGHT):
for x in range(ALIENS_WIDTH):
self.tile(x, y, TILE_ALIEN)
self.tick = 0
self.left = 0 # 左側エイリアン全滅数列ピクセル数
self.right = 0 # 右側エイリアン全滅列ピクセル数
self.descend = 0
self.dx = 2 # 水平方法移動単位ピクセル数
self.dirty = False
def update(self):
'''表示の更新。
'''
self.tick = (self.tick + 1) % 4
self.layer.frame(0, 0 if self.tick >= 2 else 4)
if self.tick in (0, 2):
if (self.x >= (TILE_WIDTH_PIXELS * (STAGE_WIDTH - ALIENS_WIDTH)
- self.dx + self.right)
or self.x <= 2 - self.left):
self.y += 1
self.descend += 1
if self.descend >= 4:
self.descend = 0
self.dx = -self.dx
self.x += self.dx
else:
self.x += self.dx
self.move(self.x, self.y)
def reform(self):
'''エイリアングリッドの左右の端に関する情報を更新する。
'''
self.left = TILE_WIDTH_PIXELS * (ALIENS_WIDTH - 1)
self.right = TILE_HEIGHT_PIXELS * (ALIENS_WIDTH - 1)
for x in range(ALIENS_WIDTH):
for y in range(ALIENS_HEIGHT):
if self.tile(x, y): # 指定座標のエイリアンが生きている
self.left = min(TILE_WIDTH_PIXELS * x, self.left)
self.right = min((ALIENS_WIDTH - 1) * TILE_WIDTH_PIXELS
- TILE_WIDTH_PIXELS * x, self.right)
self.dirty = False
def pause(info):
'''ポーズ。
:param info: ポーズ中に表示するテキスト。
'''
while ugame.buttons.get_pressed() & ugame.K_O:
time.sleep(0.25)
text_area.cursor(0, 0)
text_area.text(info)
game.render_block()
while not ugame.buttons.get_pressed() & ugame.K_O:
time.sleep(0.25)
text_area.clear()
game.render_block()
while ugame.buttons.get_pressed() & ugame.K_O:
time.sleep(0.25)
while True:
space = stage.Grid(tiles, STAGE_WIDTH, STAGE_HEIGHT)
game = stage.Stage(ugame.display, GAME_FPS)
for y in range(STAGE_HEIGHT):
for x in range(STAGE_WIDTH):
space.tile(x, y, TILE_SPACE)
for i in range(15):
space.tile(random.randint(0, STAGE_WIDTH - 1),
random.randint(0, STAGE_HEIGHT - 1),
random.randint(TILE_STAR1, TILE_STAR2))
aliens = Aliens()
aliens.move(8, 17)
saucer = Saucer()
bomb = Bomb()
ship = Ship()
missile = Missile(0)
missile1 = Missile(1)
missile2 = Missile(2)
sprites = [saucer, bomb, ship, missile, missile1, missile2]
game.layers = [text_area] + sprites + [aliens, space]
game.render_block()
while (aliens.left + aliens.right < STAGE_WIDTH_PIXELS - TILE_WIDTH_PIXELS
and aliens.y < DEAD_LINE
and not ship.dead):
for sprite in sprites:
sprite.update()
aliens.update()
game.render_sprites(sprites)
game.render_block(aliens.x + aliens.left - 1,
aliens.y - 1,
aliens.x + ALIENS_WIDTH_PIXELS + 1 - aliens.right,
aliens.y + ALIENS_HEIGHT_PIXELS)
if aliens.dirty:
aliens.reform()
game.tick()
if ship.dead or aliens.y >= DEAD_LINE:
pause('Game Over')
else:
pause('You won!')