レトロゲームエンジン「Pyxel」でシューティングを作ってみた
こんにちは。プログラマーの尾関です。
Pyxel というゲームエンジンがあります。
これは、スクリプト言語 Python を使ってレトロチックなドット絵でゲームが作れるというものです。
レトロゲーム大好きで Python が大好きな私にとっては夢のようなツールです。
詳細はこちら
→ GitHub – Pyxel
なんとゲームエンジン内に、画像エディタとサウンドエディタがあり、このツールだけでゲームに必要なリソースの作成ができてしまいます。
さらにマップエディタも付属しているので、アクションゲームもとても作りやすいと思います。
早速、私もゲームを作ってみました。シューティングゲームです。
「矩形描画とキーボードの入力ができるから、ゲームが作れる!」と意気込んで作りました。
(まったくゲームエンジンの機能を使えていません!)
ソースコードは以下のものとなります。
import pyxel
import math
import random
# ゲームオブジェクト
class GameObject:
def __init__(self):
self.exists = False
self.x = 0
self.y = 0
self.vx = 0
self.vy = 0
self.size = 6
self.hp = 1
def init(self, x, y, deg, speed):
self.x, self.y = x, y
rad = math.radians(deg)
self.setSpeed(rad, speed)
def move(self):
self.x += self.vx
self.y += self.vy
def setSpeed(self, rad, speed):
self.vx, self.vy = speed * math.cos(rad), speed * -math.sin(rad)
def drawSelf(self, palette):
r2 = self.size/2
pyxel.rect(self.x-r2, self.y-r2, self.x+r2, self.y+r2, palette)
def isOutSide(self):
r2 = self.size/2
return self.x < -r2 or self.y < -r2 or self.x > pyxel.width+r2 or self.y > pyxel.height+r2
def clipScreen(self):
r2 = self.size/2
self.x = r2 if self.x < r2 else self.x
self.y = r2 if self.y < r2 else self.y
self.x = pyxel.width-r2 if self.x > pyxel.width-r2 else self.x
self.y = pyxel.height-r2 if self.y > pyxel.height-r2 else self.y
def update(self):
self.move()
if self.isOutSide():
self.exists = False
def dead(self):
pass
def hurt(self, val=1):
if self.exists == False:
return
self.hp -= val
if self.hp <= 0:
self.exists = False
self.dead()
# ゲームオブジェクト管理
class GameObjectManager:
def __init__(self, num, obj):
self.pool = []
for i in range(0, num):
self.pool.append(obj())
def add(self):
for obj in self.pool:
if obj.exists == False:
obj.exists = True
return obj
return None
def update(self):
for obj in self.pool:
if obj.exists:
obj.update()
def draw(self):
for obj in self.pool:
if obj.exists:
obj.draw()
def vanish(self):
for obj in self.pool:
if obj.exists:
obj.hurt(999)
# 自機
class Player(GameObject):
def __init__(self):
super().__init__()
self.x = pyxel.width/2
self.y = pyxel.height*5/6
self.size = 6
self.exists = True
def update(self):
if self.exists == False:
return
if pyxel.btnp(pyxel.KEY_SPACE):
Shot.add(self.x, self.y, 90, 5) # 弾を撃つ
# Moving...
dx, dy = 0, 0
if pyxel.btn(pyxel.KEY_LEFT):
dx = -1
elif pyxel.btn(pyxel.KEY_RIGHT):
dx = 1
if pyxel.btn(pyxel.KEY_UP):
dy = -1
elif pyxel.btn(pyxel.KEY_DOWN):
dy = 1
if(dx == 0 and dy == 0):
return # 動いていない
rad = math.atan2(-dy, dx)
speed = 2
self.setSpeed(rad, speed)
self.move()
self.clipScreen()
def dead(self):
for i in range(32):
deg = random.randrange(0, 360);
speed = 0.1 + random.random() * 1.5
Particle.add(self.x, self.y, deg, speed, 7)
def draw(self):
if self.exists == False:
return
self.drawSelf(7)
# 自弾
class Shot(GameObject):
mgr = None
@classmethod
def add(cls, x, y, deg, speed):
obj = cls.mgr.add()
if obj != None:
obj.init(x, y, deg, speed)
def __init__(self):
super().__init__()
self.size = 4
self.exists = False
def dead(self):
pass
def draw(self):
self.drawSelf(6)
# 敵
class Enemy(GameObject):
mgr = None
target = None
loop = 0
@classmethod
def add(cls, eid, x, y, deg, speed):
obj = cls.mgr.add()
if obj != None:
obj.init(eid, x, y, deg, speed)
def __init__(self):
super().__init__()
self.size = 12
self.exists = False
def init(self, eid, x, y, deg, speed):
super().init(x, y, deg, speed)
self.timer = 0
self.eid = eid
# データ定義
dataTbl = [
# hp, size, destroy
[],
[5, 12, 240, self.update1], # eid:1
[1, 6, 240, self.update2], # eid:2
[2, 4, 480, self.update3], # eid:3
[5, 12, 240, self.update4], # eid:4
[5, 12, 480, self.update5], # eid:5
[5, 12, 240, self.update6], # eid:6
]
data = dataTbl[eid]
self.hp = data[0]
self.size = data[1]
self.tDestroy = data[2]
self.func = data[3]
self.aim = 0
def getAim(self):
dx = self.target.x - self.x
dy = self.target.y - self.y
return math.degrees(math.atan2(-dy, dx))
def bullet(self, deg, speed):
# 弾を撃つ
speed += Enemy.loop * 0.5 # ループするほど敵弾の速度が上がる
Bullet.add(self.x, self.y, deg, speed)
def bulletAim(self, ofs, speed):
# 狙い撃ち弾
aim = self.getAim()
self.bullet(aim+ofs, speed)
def dead(self):
for i in range(8):
deg = random.randrange(0, 360);
speed = 1
Particle.add(self.x, self.y, deg, speed, 11)
def update(self):
super().update()
self.func()
self.timer += 1
if self.timer >= self.tDestroy:
# 自爆
self.hurt(999)
def update1(self):
self.vx *= 0.95
self.vy *= 0.95
for i in range(5):
if self.timer%60 == (i*3)+40:
if i == 0:
self.aim = self.getAim()
self.bullet(self.aim, 4)
def update2(self):
self.vx *= 0.97
self.vy *= 0.97
t = self.timer%120
if t == 60 or t == 80 or t == 100:
# for i in range(3):
for i in range(1):
aim = self.getAim() - 2 + 2*i
self.bullet(aim, 0.5)
def update3(self):
self.vx *= 0.97
self.vy *= 0.97
if self.timer < 60:
return
if self.timer%75 == 0:
self.bulletAim(0, 4)
def update4(self):
self.vx *= 0.95
self.vy *= 0.95
if self.timer < 60:
return
ofs = 20 * math.sin(math.radians(self.timer*2))
self.bullet(270+ofs, 5)
def update5(self):
self.vx *= 0.95
self.vy *= 0.95
if self.timer < 60:
return
for i in range(10):
if self.timer%60 == (i*3):
if i == 0:
self.aim = self.getAim()
for i in range(3):
self.bullet(self.aim-25+25*i, 3)
def update6(self):
self.vx *= 0.95
self.vy *= 0.95
if self.timer < 60:
return
self.bullet(120 + self.timer*7, 2)
def draw(self):
self.drawSelf(11)
# 敵弾
class Bullet(GameObject):
mgr = None
@classmethod
def add(cls, x, y, deg, speed):
obj = cls.mgr.add()
if obj != None:
obj.init(x, y, deg, speed)
def __init__(self):
super().__init__()
self.size = 4
def dead(self):
pass
def draw(self):
self.drawSelf(8)
# パーティクル
class Particle(GameObject):
mgr = None
@classmethod
def add(cls, x, y, deg, speed, palette):
obj = cls.mgr.add()
if obj != None:
obj.init(x, y, deg, speed, palette)
def __init__(self):
super().__init__()
self.size = 1
def init(self, x, y, deg, speed, palette):
super().init(x, y, deg, speed)
self.palette = palette
self.timer = 0
def update(self):
super().update()
self.vx *= 0.97
self.vy *= 0.97
self.timer += 1
if self.timer > 60:
self.hurt(999)
def dead(self):
pass
def draw(self):
self.drawSelf(self.palette)
# ボス
class Boss(GameObject):
def __init__(self):
super().__init__()
self.size = 32
self.timer = 0
self.init(pyxel.width/2, 30, 0, 0)
self.exists = True
self.hp = 75
def spawn(self, eid, deg, speed):
Enemy.add(eid, self.x, self.y, deg, speed)
def update(self):
if self.exists == False:
return;
self.timer += 1
t = self.timer
if t == 60:
self.spawn(1, 225, 2)
self.spawn(1, 315, 2)
if t%240 == 150:
self.spawn(2, random.randrange(0, 360), 1)
if t == 300:
for i in range(20):
self.spawn(3, i * 360/20, 1);
if t == 440:
self.spawn(4, 0, 2)
self.spawn(4, 180, 2)
if t == 600:
self.spawn(5, 15, 2)
self.spawn(5, 165, 2)
if t == 760:
self.spawn(6, 30, 2)
self.spawn(6, 210, 2)
if t == 900:
# 最初に戻る
self.timer = 0
Enemy.loop += 1 # ループ回数をカウントアップ
def dead(self):
for i in range(32):
deg = random.randrange(0, 360);
speed = 0.1 + random.random() * 1.5
Particle.add(self.x, self.y, deg, speed, 9)
def draw(self):
if self.exists == False:
return;
pyxel.text(self.x+24, self.y, "HP:%d"%self.hp, 7)
self.drawSelf(9)
# 衝突判定
def overlaped(obj1, obj2):
r1, r2 = obj1.size/2, obj2.size/2
dx = abs(obj1.x - obj2.x)
dy = abs(obj1.y - obj2.y)
return dx < (r1 + r2) and dy < (r1 + r2)
class App:
def __init__(self):
pyxel.init(160, 240, caption="Test", fps=60)
self.init()
pyxel.run(self.update, self.draw)
def init(self):
self.player = Player()
Shot.mgr = GameObjectManager(32, Shot)
Enemy.mgr = GameObjectManager(32, Enemy)
Enemy.target = self.player
self.boss = Boss()
Bullet.mgr = GameObjectManager(256, Bullet)
Particle.mgr = GameObjectManager(256, Particle)
Enemy.loop = 0
def update(self):
if pyxel.btnp(pyxel.KEY_1):
pyxel.quit()
if pyxel.btnp(pyxel.KEY_R):
self.init()
# 各種オブジェクトの更新
self.player.update()
Shot.mgr.update()
Enemy.mgr.update()
self.boss.update()
Bullet.mgr.update()
Particle.mgr.update()
if self.boss.exists == False:
return
# 自弾と敵との当たり判定
for s in Shot.mgr.pool:
if s.exists == False:
continue
for e in Enemy.mgr.pool:
if e.exists == False:
continue
if overlaped(s, e):
s.hurt()
e.hurt()
break
if overlaped(s, self.boss):
s.hurt()
self.boss.hurt()
if self.boss.exists == False:
# 敵と敵弾を全て消す
Enemy.mgr.vanish()
Bullet.mgr.vanish()
# 自機と敵弾との当たり判定
if self.boss.exists == True:
for b in Bullet.mgr.pool:
if overlaped(self.player, b):
self.player.hurt()
def draw(self):
pyxel.cls(0)
self.player.draw()
Shot.mgr.draw()
Enemy.mgr.draw()
self.boss.draw()
Bullet.mgr.draw()
Particle.mgr.draw()
if self.player.exists == False:
pyxel.text(64, 100, "GAME OVER", 7)
pyxel.text(48, 120, "Press 'R' to Restart", 7)
if self.boss.exists == False:
pyxel.text(48, 100, "MISSIN COMPLETE", 7)
pyxel.text(40, 120, "Congratulations!!!", 7)
App()
■操作方法
・カーソルキー:移動
・SPACEキー:弾を撃つ
・Rキー:リトライ
・1キー:終了
わずか400行のコードでこれだけのゲームが作れてしまいます!
Python最高!
Pyxelブラボー!
ファビュラスマックス!
Python好きでレトロゲーム好きな方はぜひ使ってみてください!!
以上、尾関でした。