Python シューティングプロトタイプ
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
シューティングプロトタイプ v0.00.01
作者 :めがねん(aminoパレットネーム:HK)
URI :http://megane0303.html.xdomain.jp/meganeGames/
amino project
http://web1.kcn.jp/as-studio/amino/
"""
"""
各ファイルのインポートの説明
pygame.locals 全てのライブラリをインポート
必須ではないが重要なものが含まれるらしいので全て入れておく
sys 必須
math 数学関数に必要。今回は三角関数を利用
csv csv処理に必要
"""
import pygame
from pygame.locals import *
import sys
import math
import csv
# 画面サイズ
# Rectは短形を保持するオブジェクト
# 上下の幅を簡単に記憶できる
SCR_RECT = Rect(0,0,640,480)
# ゲーム画面サイズ
SCR_GAME = Rect(0, 0, 410, 460)
# 敵クラス
# 各タイプは未実装なものも含むが、今後の拡張のため値は保存している
class Enemy(pygame.sprite.Sprite):
# 種類、x,y,移動パターン、発射パターン、出現、停止、発射、帰還(それぞれフレーム数)
# 弾速度、移動速度、ライフ、落とすアイテム
"""
発射パターン
今回のプロトタイプでは直線発射、プレイヤーを狙う弾、3way弾、全方向弾、ランダムway弾がある
時間について
各時間はメインフレーム数(frame)を基準に設定している
たとえば出現時間が120であれば、ゲーム開始2秒後に行動を開始する
同じく発射時間が180なら3秒後に弾を発射し、帰還時間360なら6秒後に帰還する
moveframeは移動ルーチンにのみ利用する個別フレーム
最大で2秒(120frame)まで記憶
"""
# 『=』はデフォルト引数。
def __init__(self, enemytype=0, x=0, y=0, movep=1, shotp=1, showt=120, stopt=240, shott=360, byet=420, shotspd=2, movespd=1, life=1, item=1, score=10):
pygame.sprite.Sprite.__init__(self, self.containers)
self.frame = 0
self.moveframe=0
self.shotflag=1
# 種類に応じて画像変更
if enemytype == 1:
self.tmpimg = "enemy1.png"
elif enemytype == 2:
self.tmpimg = "enemy2.png"
elif enemytype == 3:
self.tmpimg = "enemy3.png"
elif enemytype == 4:
self.tmpimg = "enemy4.png"
elif enemytype == 5:
self.tmpimg = "enemy5.png"
elif enemytype == 100:
self.tmpimg = "enemy100.png"
else:
self.tmpimg = "enemy1.png"
self.image = pygame.image.load(self.tmpimg).convert_alpha()
width = self.image.get_width()
height= self.image.get_height()
self.rect = Rect(x, y, width, height)
# self.rect.center = pos
self.type = enemytype
self.move = movep
self.shottype = shotp
self.stoptime = stopt
self.showtime = showt
self.shottime = shott
self.byetime = byet
self.shotspd = shotspd
self.movespd = movespd
self.life = life
self.item = item
self.score = score
self.vx = 0
self.vy = 0
# 移動パターンから移動値を設定する
def moveset(self):
if (frame < self.showtime):
return
# パターン1:出現して停止後、帰って行く
if (self.move == 1):
if(frame < self.stoptime):
self.vy=self.movespd
elif(frame > self.byetime):
self.vy=-self.movespd
# パターン2:そのまま下へ下がっていくだけ
elif(self.move == 2):
self.vy=self.movespd
# パターン3:左右にジグザグ移動
elif(self.move == 3):
if ((self.moveframe < 60) == 0):
self.vx=-self.movespd
elif ((self.frame < 120) == 0):
self.vx=self.movespd
self.vy=self.movespd
if(frame > self.byetime):
self.vy=-self.movespd
# パターン4:右下へ移動
elif(self.move == 4):
self.vy=self.movespd
self.vx=self.movespd
# パターン5:左下へ移動
elif(self.move == 5):
self.vy=self.movespd
self.vx=-self.movespd
# パターン100:中央固定
elif(self.move == 6):
self.vy=self.movespd
self.vx=-self.movespd
else:
self.vx=0
self.vy=0
if (self.moveframe > 120):
self.moveframe = 0
def update(self):
self.frame += 1
self.moveframe += 1
self.moveset()
self.rect.move_ip(self.vx, self.vy)
self.shot()
# ボムによる消滅
if (self.my.bomflag):
if (self.showtime <= frame):
global score
score += self.score
self.kill()
if not self.rect.colliderect(SCR_GAME):
# 停止時刻に達していなければ消滅しない
if(frame > self.stoptime):
self.kill()
self.vx = 0
self.vy = 0
def shot(self):
# 弾発射
if (self.shottime == frame):
self.count = 0
self.rad = math.atan2(self.my.rect.centery - self.rect.centery, self.my.rect.centerx - self.rect.centerx)
# 真下に発射
if (self.shottype == 1):
emyShot(self.rect.center, math.pi/2, self.shotspd, "emyshot1.png")
# プレイヤーを狙う発射
elif (self.shottype == 2):
emyShot(self.rect.center, self.rad, self.shotspd, "emyshot2.png")
self.count = self.count + 1
def draw(self, screen):
screen.blit(self.image, self.rect)
""" 自機クラス
スプライトクラスを継承する
"""
# このように、pythonではクラス名に括弧をつけてスーパークラスを指定する
class Player(pygame.sprite.Sprite):
# コンストラクタ
# 第一引数は自分自身を指すselfを指定
# @@param fname 画像ファイル名
# @@param x,y 座標(X,Y)
# @@param spd 移動速度
def __init__(self, fname, x, y, spd):
# スーパークラスのコンストラクタを呼ぶ
pygame.sprite.Sprite.__init__(self)
# 透明度を維持して画像を読み込む
# このほか、透明にしたい色座標を指定する方法もある
self.image = pygame.image.load(fname).convert_alpha()
width = self.image.get_width()
height= self.image.get_height()
# このメソッドを呼べば、自身の短形を簡単に保存可能
self.rect = Rect(x, y, width, height)
self.spd = spd;
# 一時速度
# 速度を変更すると速度が変わるので、本来の速度を記憶させておく
self.spd2 = spd
# vx,vyは実際に移動する移動量
# 値を指定すると、update時に実際に移動するため、通常は0にする
# 逆に言えば、vx,vyに数値を入れるだけで、毎フレームその数値分自動移動する
self.vx = 0
self.vy = 0
# このオブジェクトが作られてからのフレームカウント
# 60fpsでは1秒間に60がカウントされる。つまりframe=1/60である
self.frame = 0
"""
以下、基本能力 ライフ、ボム、火力
"""
self.life = 3
self.bomcount = 3
# ボム設定
self.bomflag = 0
self.bomframe = 0
# 周期処理
# メインフレームから呼ばれる周期処理
# 敵弾との当たり判定、ショットなどを記入
def update(self):
self.frame += 1
# ボムの効果時間
if (self.bomflag):
self.bomframe += 1
if (self.bomframe > 120):
self.bomflag = 0
self.bomframe= 0
# キー検出
# キーを押し続ける状態を検出するなら、このメソッドで。
press_key = pygame.key.get_pressed()
# ショット Zキーでショットする
if press_key[K_z]:
# 経過フレームが12の倍数ならショット可能
# 60fpsの場合1秒間に5発の弾が撃てることになる
if ((self.frame % 12) == 0):
# これは左シフトをおされた時。
# 右シフトはK_RSHIFT を指定するが、ゲームでは左シフト使用が一般的
if press_key[K_LSHIFT]:
# 各数値は角度(ラジアン)を指定。-パイ÷2が真上なので、それを基準に3way
# 値はかなりアバウト
myShot(self.rect.center, -math.pi/2.5, "myshot_normal2.png")
myShot(self.rect.center, -math.pi/2)
myShot(self.rect.center, -math.pi/1.65, "myshot_normal2.png")
else:
# シフトが押されてなければノーマルショット
myShot(self.rect.center, -math.pi/2)
# Cキーでボム
elif press_key[K_c]:
if (self.bomflag == 0):
if (self.bomcount > 0):
self.bomcount -= 1
self.bomflag = 1
# シフトが押されていれば速度を半減、つまり低速移動
# 弾幕シューティングにおいて、低速移動は必須といえる動作である
if press_key[K_LSHIFT]:
self.spd = self.spd/2
else:
# 押されてなければ通常速度に戻す
# これを忘れると、どんどん速度が低下する
self.spd = self.spd2
"""
移動関連の解説
xは左右、yは上下を指す
左上を0,0とするので、xが負なら左へ、正なら右へ進む
同じ考え方でyも負なら上へ、正なら下へ進む
self.vx -= 1
であれば、毎フレーム左へ1ずつ進む
※移動量は『毎フレーム』であって毎秒ではない
上の指定では一秒に60進むことになる
"""
# 斜め移動を考慮
if press_key[K_LEFT]:
if press_key[K_UP] or press_key[K_DOWN]:
self.vx = -self.spd *0.71
else:
self.vx = -self.spd
if press_key[K_RIGHT]:
if press_key[K_UP] or press_key[K_DOWN]:
self.vx = self.spd *0.71
else:
self.vx = self.spd
if press_key[K_UP]:
if press_key[K_LEFT] or press_key[K_RIGHT]:
self.vy = -self.spd *0.71
else:
self.vy = -self.spd
if press_key[K_DOWN]:
if press_key[K_LEFT] or press_key[K_RIGHT]:
self.vy = self.spd *0.71
else:
self.vy = self.spd
# pygameでは、このメソッドを使うだけで移動可能(便利!)
self.rect.move_ip(self.vx, self.vy)
# これは、ゲーム画面のRectからはみ出た場合、戻すメソッド
# SCR_RECTを指定すると右側のステータスまではみ出るので注意
self.rect = self.rect.clamp(SCR_GAME)
# 速度を戻す
# 念のため、メソッドを抜ける前に速度を強制的に戻す
# これだけでも、くだらないバグ防止になる
self.spd = self.spd2
# 移動量を戻す
# これも同じく。忘れると何も押してないのに移動を続けたりする
self.vx = 0
self.vy = 0
# 画面描画メソッド
# これが呼ばれることで初めて画面上に描画される
def draw(self, screen):
screen.blit(self.image, self.rect)
# 背景スクロール用クラス
class backGraund(pygame.sprite.Sprite):
def __init__(self, second=0):
pygame.sprite.Sprite.__init__(self, self.containers)
self.image = pygame.image.load("game_frame.jpg").convert_alpha()
self.vy=1
width = self.image.get_width()
height= self.image.get_height()
if (second == 1):
self.top = 0
else:
self.top = -460
self.rect = Rect(0, self.top, width, height)
def update(self):
self.rect.move_ip(0, self.vy)
if (self.rect.top > 460):
self.rect.top = -460
# 敵ショットクラス
class emyShot(pygame.sprite.Sprite):
def __init__(self, pos, rad, spd, img="emyshot1.png"):
pygame.sprite.Sprite.__init__(self, self.containers)
self.image = pygame.image.load(img).convert_alpha()
self.rect = self.image.get_rect()
self.rect.center = pos
self.rad = rad
self.spd = spd
def update(self):
self.rect.move_ip( math.cos(self.rad) * self.spd , math.sin(self.rad) * self.spd )
if not self.rect.colliderect(SCR_GAME):
self.kill()
def draw(self, screen):
rotateimg = pygame.transform.rotate(screen, self.rad)
w2, h2 = rotateimg.get_size() #回転後のサイズ
topleft = (self.rect.centerx - w2 / 2, self.rect.centery - h2 / 2)
screen.blit(rotateimg, topleft)
# 自機ショットクラス。スプライトを継承
# このクラスは弾の一発分のオブジェクト用
class myShot(pygame.sprite.Sprite):
spd = 10 # 弾速度
def __init__(self, pos, rad, img="myshot_normal1.png"):
pygame.sprite.Sprite.__init__(self, self.containers)
self.image = pygame.image.load(img).convert_alpha()
# スプライトクラスのget_rectメソッドを呼ぶだけで短形取得
self.rect = self.image.get_rect()
# 自身の短形の中央をposとする
self.rect.center = pos
# 自身の角度(方向)
self.rad = rad;
def update(self):
#self.rect.move_ip(0, -self.spd)
# 角度に応じて弾の発射方向を変える。
# 角度はラジアンで指定する。例えば真上の角度は『-パイ÷2』およそ-1.57になる
# 詳しい説明は数学の分野になるため、三角関数を勉強してください
# ここでは、角度のコサインに速度を掛けた値をx,サインをyとしている
self.rect.move_ip(math.cos(self.rad)*self.spd, math.sin(self.rad)*self.spd)
# 本来は角度が変われば画像を角度に応じて回転させるべきだが
# 今回は標準動作が目的なので、画像描画に関しては実装しない
# ちなみにpygameで回転縮小機能は標準で実装されているため、実現は容易である
# 自身の消滅。
# ゲーム画面(SCR_GAME)からはみ出たら消滅させる
# containsはrectのメソッド
# ゲーム画面上に弾があれば真なので、その否定を条件としている
# ※killはオブジェクトを消滅させるスプライトのメソッド
# if self.rect.top < 0 or self.rect.right > SCR_GAME.right or self.rect.left < SCR_GAME.left:
if not SCR_GAME.contains(self.rect):
self.kill()
# メイン関数
# クラスにしても良いが、今回はメイン関数をゲーム本体とした
def main():
# Pygameを初期化
pygame.init()
# SCREEN_SIZEの画面を作成
screen = pygame.display.set_mode(SCR_RECT.size)
# タイトルバーの文字列をセット
pygame.display.set_caption(u"シューティングプロトタイプ")
# 背景セット
# backImage = pygame.image.load("game_frame.jpg").convert()
# スプライトグループ作成
all = pygame.sprite.RenderUpdates()
enemy = pygame.sprite.Group()
mshots = pygame.sprite.Group()
eshots = pygame.sprite.Group()
backImages = pygame.sprite.Group()
backGraund.containers = backImages
myShot.containers = mshots,all
emyShot.containers = eshots,all
Enemy.containers = enemy,all
backImage = backGraund()
backImage2 = backGraund(1)
# 自機作成
# 画像ファイル名、XY座標、速度 の順番
my = Player("myChara0.png", 205, 420, 4)
emyShot.my = my
Enemy.my = my
# 敵の読み込みとオブジェクト作成
ef = open("enemy.csv", "r")
enemyData = csv.reader(ef)
# csvの一行目は項目の説明なので飛ばす
i = 0
for line in enemyData:
if (i ==0):
i+=1
else:
# 要素は文字列で作られているようなので、intメソッドで整数に変換した
# Enemyはスプライトグループ登録なので、あえて変数に代入する必要はない
Enemy(int(line[0]), int(line[1]), int(line[2]), int(line[3]), int(line[4]),
int(line[5]),int(line[6]), int(line[7]), int(line[8]), int(line[9]),
int(line[10]), int(line[11]), int(line[12]), int(line[13]))
# クロックオブジェクト。フレームカウントを利用するお約束として覚える
clock = pygame.time.Clock()
# ゲーム起動からのフレーム数
global frame
frame = 0
# ゲームステータス
global score
score = 0
fonts = pygame.font.SysFont(None, 24)
# ゲームループ
# ここからは毎フレーム処理になる
# 最低限更新が必要な処理だけを記述し、初期化は外に出す
while True:
# FPSを60に設定
clock.tick(60)
# ↓ この記述によって、全ての表示をリセットしている
screen.fill((0,0,0)) # 画面を黒色で塗りつぶす
# screen.blit(backImage , (0,0)) # 背景画像を上乗せ描画
# ↑ 全画面リセットは楽だが、処理に負荷がかかるため、
# 本来は部分的な更新が望ましい。が、今回はプロトタイプなので…(略)
frame += 1
# ゲームステータス更新
state_score_str = fonts.render("SCORE", True, (128, 128, 0))
state_life_str = fonts.render("LIFE", True, (128, 128, 0))
state_bom_str = fonts.render("BOM", True, (128, 128, 0))
state_graze_str = fonts.render("GRAZE", True, (128, 128, 0))
state_score = fonts.render(str(score), True, (0, 128, 128))
state_life = fonts.render(str(my.life), True, (0, 128, 128))
state_bom = fonts.render(str(my.bomcount), True, (0, 128, 128))
state_frame = fonts.render(str(frame/60), True, (0, 128, 128))
# 各ゲームステータスの描画
screen.blit(state_score_str, (420, 20))
screen.blit(state_score, (440, 40))
screen.blit(state_life_str, (420, 60))
screen.blit(state_life, (480, 60))
screen.blit(state_bom_str, (420, 80))
screen.blit(state_bom, (480, 80))
screen.blit(state_graze_str, (420, 120))
screen.blit(state_frame, (600, 450))
backImages.update()
backImages.draw(screen)
# スプライト更新と描画
my.update()
my.draw(screen)
# スプライトグループの更新
# グループを使っているため、本来はループ処理が必要なところ、たったこれだけ。
all.update()
all.draw(screen)
# 敵と弾の当たり判定
for e in pygame.sprite.groupcollide(enemy, mshots, 1, 1).keys():
if (e.showtime >= frame):
continue
e.life -= 1
if (e.life == 0):
score += e.score
e.kill()
# 最終的な画面更新。これで画面が書き換わる
# ここまでの処理を、今回は1秒に60回行う
# ↓ 画面更新
pygame.display.update()
# ↑ 画面更新終了
# イベント処理。イベントをキャッチする
# イベントは各オブジェクトが請け負うので
# ここでは終了メッセージが来たら終了させるにとどめる
# ※event.get()はメッセージが来た瞬間をキャッチできるため
# 毎フレーム、確実に処理を行える
# ※key.get_pressed()では更新のタイミングで押されていなければ処理されない
for event in pygame.event.get():
if event.type == QUIT:
sys.exit()
# ESCが押されたらゲーム終了
if event.type == KEYDOWN:
if event.key == K_ESCAPE:
sys.exit()
# おまじないみたいなものです。プログラムの起動でmain関数を呼ぶ
if __name__ == "__main__":
main()