PyBML

大往生3ボス?

BulletMLPython用に作ってみました。
 
ただしkenmoの力不足のため、本家の仕様と結構異なるところがあります。

  • actionRefが使えない
  • bulletの子要素に複数のactionを指定できない
  • bullet時にメインループに制御を返してしまう
  • fire時のspeedは親に依存
  • directionの値は-180〜180
  • パラメータは$1〜$9まで
  • vertical、horizonキーワード
  • その他、動きが若干おかしいところあり

などなど。(多すぎ?)
 
たぶん、白い弾幕君などを持ってきても動かない可能性が高いです(ぉぃ
 
ただ、独自拡張として、

  • $cntでループカウンタを取れる(ただし、リピート回数からのデクリメント)

と、式をeval()で評価しているので、

<direction>random.choice([0, 10, 20])</direction>

という奇妙なことができてしまったり。
 
特にライセンスなどの制約はありませんので、
自由に組み込んでみてください。
PythonBulletMLなんて、使う人が存在するのか疑問ですが…)

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import math
import random
from xml.dom.minidom import parse

# ユーティリティ関数
def adjustAbs180(deg):
	""" -180〜180になるよう調整 """
	if deg > 180:
		return deg - 360
	elif deg < -180:
		return deg + 360
	else:
		return deg
def getParams(node, params):
	""" パラメータの取得 """
	ret = []
	for n in node.childNodes:
		if isTagName(n, "param"):
			param = n.firstChild.data.strip()
			if param.startswith("$"):
				param = params[int(params[1:]) + 1]
			param = param.replace("$rand", "random.random()")
			param = param.replace("$rank", "1") # TODO: とりえあず1
			ret.append(param)
	if len(ret) == 0:
		return params
	return ret
def isTagName(node, tagName):
	""" 要素名が一致するかどうか """
	if node.nodeType == node.ELEMENT_NODE:
		if node.tagName == tagName:
			return True
	return False
def getChild(node, tagName):
	"""
	指定した子ノードを取得
	@return 見つからない場合、None
	"""
	for n in node.childNodes:
		if isTagName(n, tagName):
			return n
	return None

class Stack:
	""" スタッククラス """
	def __init__(self):
		self._data = []
	def size(self):
		return len(self._data)
	def push(self, var):
		self._data.append(var)
	def pop(self):
		if len(self._data) == 0:
			return None
		return self._data.pop()
	def top(self):
		""" 一番上の要素を取得 """
		if len(self._data) == 0:
			return None
		return self._data[len(self._data)-1]
	def __str__(self):
		return str(self._data)

class Vec2D:
	""" 2次元ベクトル """
	def __init__(self, x=0, y=0):
		self.x = x
		self.y = y
	def __add__(self, pos):
		return Vec2D(self.x + pos.x, self.y + pos.y)
	def __iadd__(self, pos):
		return Vec2D(self.x + pos.x, self.y + pos.y)
	def __sub__(self, pos):
		return Vec2D(self.x - pos.x, self.y - pos.y)
	def __str__(self):
		return "<x,y=(%f,%f)>"%(self.x, self.y)
	# コピーインスタンスを返す
	def clone(self):
		return Vec2D(self.x, self.y)
	# 正規化
	def normalize(self):
		d = math.sqrt(self.x ** 2 + self.y ** 2) * 1.0
		if(d == 0): d = 1
		return Vec2D(self.x / d, self.y / d)
	# スカラ
	def length(self):
		return math.sqrt(self.lengthSq())
	def lengthSq(self):
		return self.x*self.x + self.y*self.y
	def degrees(self):
		return math.degrees(math.atan2(-self.y, self.x))
	def tuple(self):
		return (self.x, self.y)
	def degToVec(deg):
		""" 角度に対応するベクトルを取得 """
		return Vec2D(
			math.cos(math.radians(deg)), 
			-math.sin(math.radians(deg)))
	degToVec = staticmethod(degToVec)

class Bullet:
	""" 弾定義 """
	def __init__(self, pos=Vec2D(0, 0)):
		""" コンストラクタ """
		self.pos   = pos        # 座標
		self.exist = True       # 存在フラグ
	def vanish(self):
		""" 消滅 """
		self.exist = False
	def isExist(self):
		""" 存在しているかどうか """
		return self.exist
	def setBullet(self, nBullet):
		self.setAction(getChild(nBullet, "action"))
	def setAction(self, nAction):
		self.action = Action(nAction, self)
	def update(self, vAim):
		""" 更新 """
		self.action.vAim = vAim
		ret = self.action.update()
		return ret
	def __str__(self):
		return "Bullet pos.x:%f pos.y:%f exist:%d"%(self.pos.x, self.pos.y, self.exist)

class Speed:
	""" 速度定義 """
	def __init__(self, speed=0, accel=0, timer=0):
		self.speed = speed # 速度
		self.accel = accel # 加速
		self.timer = timer # タイマ
	def update(self):
		if self.timer > 0:
			self.timer -= 1
			self.speed += self.accel
	def __str__(self):
		return "Speed speed:%f, accel:%f, timer:%d"%(self.speed, self.accel, self.timer)
class Accel:
	""" 加速定義 """
	def __init__(self, mx=0, my=0, ax=0, ay=0, timer=0):
		self.mx = mx # 移動量
		self.my = my
		self.ax = ax # 加速度
		self.ay = ay
		self.timer = timer
	def update(self):
		if self.timer > 0:
			self.timer -= 1
			self.mx += self.ax
			self.my += self.ay
	def __str__(self):
		return "Accel mx:%f, my:%f, , ax:%f, ay:%f, timer:%d"%(self.mx, self.my, self.ax, self.ay, self.timer)

class Direction:
	""" 角度定義 """
	def __init__(self, deg):
		deg = adjustAbs180(deg)
		self.deg   = deg  # 現在向いている角度
		self.prev  = deg  # 開始角度
		self.timer = 0    # タイマー
		self.mov   = 0    # タイマーに応じた移動角度
		self.aim   = True # 狙い撃ち弾かどうか
	def setDeg(self, deg):
		""" 角度の設定 """
		deg = adjustAbs180(deg)
		self.deg  = deg
		self.prev = deg
	def changeDirection(self, mov, timer):
		""" "changeDirection"の設定 """
		mov = adjustAbs180(mov)
		self.mov = mov
		self.timer = timer
	def update(self, vAim):
		""" 角度更新 """
		if self.timer > 0:
			self.timer -= 1
			self.deg += self.mov
			self.deg = adjustAbs180(self.deg)
	def __str__(self):
		return "Direction deg:%f, prev:%f, timer:%d, mov:%f, aim:%d"%(self.deg, self.prev, self.timer, self.mov, self.aim)
class Action:
	""" アクション定義 """
	# "type"属性のキーワード
	TYPE_AIM      = "aim"
	TYPE_SEQUENCE = "sequence"
	TYPE_ABSOLUTE = "absolute"
	TYPE_RELATIVE = "relative"
	def __init__(self, nAction, bullet):
		self.__updateHandler = self.__parse()
		self.bullet        = bullet          # 弾情報
		self.vAim          = Vec2D(320, 240) # 目標座標(ダミー)
		self.iWait         = 0               # "wait"タイマー
		self.iRepeat       = 1               # "repeat"回数
		self.direction     = Direction(self.getAimDegrees())
		self.speed         = Speed()
		self.accel         = Accel()
		self.nAction       = nAction # "action"ノード
		self.params        = []      # "param"リスト
	def evalExpression(self, expression, params):
		""" 式の評価 """
		e = expression.replace("$rand", "random.random()")
		e = e.replace("$cnt", str(self.repeats.top()))
		e = e.replace("$rank", "1") # TODO: とりあえず1
		if params is not None:
			# TODO: $1-$9まで
			for i, param in enumerate(params):
				e = e.replace("$%d"%(i+1), param)
		return eval(e)
	def getAimDegrees(self):
		""" 狙い撃ち角度の取得 """
		return (self.vAim-self.bullet.pos).degrees()
	def setAimVec2D(self, vAim):
		self.vAim = vAim
	def update(self):
		""" bulletとactionの更新 """
		ret = None
		if self.iWait > 0:
			self.iWait -= 1 # ウェイト
		else:
			if self.nAction is not None:
				# アクションノード解析・更新
				ret = self.__updateHandler.next()
		# 角度更新
		self.direction.update(self.vAim-self.bullet.pos)
		# 回転速度更新
		self.speed.update()
		# 加速更新
		self.accel.update()
		vDeg = Vec2D.degToVec(self.direction.deg)
		# 座標更新
		moveX = self.accel.mx + vDeg.x*self.speed.speed
		moveY = self.accel.my + vDeg.y*self.speed.speed
		self.bullet.pos.x += moveX
		self.bullet.pos.y += moveY
		return ret
	def __parse(self):
		"""
		BulletML解析マイクロスレッド
		@return 生成した弾
		"""
		skips   = Stack() # 子ノードを読み込んだ場所
		self.repeats = Stack() # 繰り返しスタック
		root    = self.nAction
		
		parent = root
		skip   = 0
		while True:
			for i, node in enumerate(parent.childNodes):
				# 兄弟の走査
				if skip > 0:
					# 走査済み
					skip -= 1
					if isTagName(node, "times"):
						r = self.repeats.top()
						assert r is not None
						self.iRepeat = r
					continue
				
				if node.nodeType == node.ELEMENT_NODE:
					# 要素
					self.repeats, bullet = self.parseTag(node, self.repeats) # 要素の解析
					tag = node.tagName
					if tag in [
							   "fire", "fireRef", "bullet", "bulletRef",
							   "wait", "accel", "changeSpeed", "changeDirection"]:
						yield bullet
						continue # 子を調べない
					elif not(self.bullet.isExist()):
						yield bullet
					if node.hasChildNodes():
						# 子を調べる
						self.iRepeat = 1
						skips.push(i+1)
						parent = node
						break
			else:
				# 走査完了
				if self.iRepeat > 1:
					# もう一回
					skip = 0
					self.repeats.pop()
					self.iRepeat -= 1
					self.repeats.push(self.iRepeat)
				else:
					# 親ノード取り出し
					skip = skips.pop()
					if skip is None:
						# 親なしなので最初に戻る
						parent     = root
						skip       = 0
						self.iWait = 60
						yield None
					else:
						if isTagName(parent, "repeat"):
							self.repeats.pop() # repeat回数取り出し
						parent = parent.parentNode
	def parseTag(self, node, repeats):
		"""
		要素の解析
		@param  node    要素ノード
		@param  repeats "repeat"スタック
		@return "repeat"スタック
		@return 生成した弾
		"""
		bullet = None
		tag = node.tagName
		if tag == "action" or tag == "actionRef":
			# "action"要素
			# TODO: ActionRefの実装
			if tag == "actionRef":
				raise "actionRef is not implement yet."
		elif tag == "repeat":
			# "repeat"要素
			n = getChild(node, "times")
			assert n is not None # times not exist.
			t = n.firstChild.data.strip()
			repeats.push(self.evalExpression(t, self.params))
		elif tag == "wait":
			# "wait"要素
			t = node.firstChild.data.strip()
			self.iWait = int(self.evalExpression(t, self.params)) - 1
		elif tag == "fire" or tag == "fireRef":
			# "fire"要素
			bullet = self.setFire(tag, node)
		elif tag == "speed":
			# "speed"要素
			s = node.firstChild.data.strip()
			self.speed.speed = self.evalExpression(s, self.params)
		elif tag == "accel":
			# "accel"要素
			nTerm = getChild(node, "term")
			term = self.evalExpression(nTerm.firstChild.data.strip(), self.params)
			self.accel.timer = term
			nVertical = getChild(node, "vertical")
			if nVertical is None:
				# 横加速
				nHorizon = getChild(node, "horizon")
				assert nHorizon is not None # accel has "vertial" or "horizon"
				e = nHorizon.firstChild.data.strip()
				a = self.evalExpression(e, self.params)
				type = nHorizon.getAttribute("type")
				if type == self.TYPE_SEQUENCE:
					self.accel.ax = a
				else:
					aimAx = a
					if type == self.TYPE_RELATIVE:
						aimAx += self.accel.mx
					self.accel.ax = (aimAx-self.accel.mx)/float(term)
			else:
				# 縦加速
				e = nVertical.firstChild.data.strip()
				a = self.evalExpression(e, self.params)
				type = nVertical.getAttribute("type")
				if type == self.TYPE_SEQUENCE:
					self.accel.ay = a
				else:
					aimAy = a
					if type == self.TYPE_RELATIVE:
						aimAy += self.accel.my
					self.accel.ay = (aimAy-self.accel.my)/float(term)
		elif tag == "changeDirection":
			# "changeDirection"要素
			# "direction"要素取得
			nDir = getChild(node, "direction")
			d = self.evalExpression(nDir.firstChild.data.strip(), self.params)
			type = nDir.getAttribute("type")
			nTerm = getChild(node, "term")
			term = self.evalExpression(nTerm.firstChild.data.strip(), self.params)
			# "type"属性に対応した移動角度を設定
			if type == self.TYPE_SEQUENCE:
				self.direction.aim = False
				mov = d
			else:
				if type == self.TYPE_ABSOLUTE:
					self.direction.aim = False
					mov = (d - self.direction.deg) % 360
				elif type == self.TYPE_RELATIVE:
					self.direction.aim = False
					mov = d % 360
				else:
					self.direction.aim = True
					mov = (d + self.getAimDegrees() - self.direction.deg) % 360
				mov = adjustAbs180(mov) / float(term)
			self.direction.changeDirection(mov, term)
			# "speed"属性取得
			nSpeed = getChild(node, "speed")
			if nSpeed is not None:
				s = nSpeed.firstChild.data.strip()
				if params is None:
					s = self.evalExpression(s, self.params)
				else:
					s = self.evalExpression(s, params)
				if (nSpeed.getAttribute("type")
				    	in [self.TYPE_RELATIVE, self.TYPE_SEQUENCE]):
					s += self.speed.speed
				self.speed.speed = s
		elif tag == "changeSpeed":
			# "changeSpeed"要素
			# "speed"要素取得
			nSpeed = getChild(node, "speed")
			assert nSpeed is not None # changeSpeed has "speed"Child
			s = self.evalExpression(nSpeed.firstChild.data.strip(), self.params)
			type = nSpeed.getAttribute("type")
			# "term"要素取得
			nTerm = getChild(node, "term")
			assert nTerm is not None # changeSpeed has "term"Child
			tSpeed = self.evalExpression(nTerm.firstChild.data.strip(), self.params)
			# "type"属性に対応した移動角度を設定
			if type == self.TYPE_SEQUENCE:
				accel = s
			else:
				aimSpeed = s
				if type in [self.TYPE_RELATIVE, self.TYPE_SEQUENCE]:
					aimSpeed += self.speed.speed
				accel = (aimSpeed - self.speed.speed) / float(tSpeed)
			self.speed.timer = tSpeed
			self.speed.accel = accel
		elif tag == "vanish":
			""" 消滅 """
			self.bullet.vanish()
		return repeats, bullet
	def setFire(self, tag, nFire):
		"""
		"fire"要素設定
		@param tag   要素名
		@param nFire "fire"要素
		@return 生成した弾
		"""
		# 弾インスタンス生成
		bullet = Bullet(self.bullet.pos.clone())
		params = None
		if tag == "fireRef":
			params = getParams(nFire, self.params)
			# "fireRef"から取り出し
			label = nFire.getAttribute("label")
			nFire  = LabelManager.getFire(label)
		# "bullet"要素取得
		bRef = getChild(nFire, "bulletRef")
		if bRef is None:
			nBullet = getChild(nFire, "bullet")
			if params is None:
				params  = list(self.params)
		else:
			# "bulletRef"要素でーす
			label = bRef.getAttribute("label")
			nBullet = LabelManager.getBullet(label)
			if params is None:
				params  = getParams(bRef, self.params)
		bullet.setBullet(nBullet)
		bullet.action.params = params
		# "direction"要素取得
		nDir = getChild(nFire, "direction")
		if nDir is None:
			# "direction"がないので、狙い撃ち弾
			d = self.getAimDegrees()
			bullet.action.direction.deg = d
		else:
			type = nDir.getAttribute("type")
			d = self.evalExpression(nDir.firstChild.data.strip(), params)
			# "type"属性に対応した角度を設定
			if type == self.TYPE_AIM:
				d += self.getAimDegrees()
			elif type == self.TYPE_SEQUENCE:
				d += self.direction.prev
			elif type == self.TYPE_RELATIVE:
				d += self.direction.deg
			self.direction.setDeg(d)
			bullet.action.direction.deg = self.direction.deg
		# "speed"属性取得
		nSpeed = getChild(nFire, "speed")
		if nSpeed is None:
			# "speed"がないので、親のspeedにする
			bullet.action.speed.speed = self.speed.speed
		else:
			s = nSpeed.firstChild.data.strip()
			if params is None:
				s = self.evalExpression(s, self.params)
			else:
				s = self.evalExpression(s, params)
			if (nSpeed.getAttribute("type")
			    	in [self.TYPE_RELATIVE, self.TYPE_SEQUENCE]):
				s += self.speed.speed
			bullet.action.speed.speed = s
		return bullet
		
class PyBulletML:
	""" BulletML管理クラス """
	def __init__(self, filename, pos=Vec2D(320, 160)):
		"""
		コンストラクタ
		@param filename BulletMLのファイル名
		@param pos      開始座標
		"""
		self.bulletList = [] # 弾リスト
		act = LabelManager.getTopAction(filename)
		self.topAction = Action(act, Bullet(pos=pos))
	def appendBullet(self, bullet):
		""" 弾の追加 """
		self.bulletList.append(bullet)
	def removeBullet(self, bullet):
		""" 弾の削除 """
		self.bulletList.remove(bullet)
	def update(self, vAim):
		"""
		topActionと発射した弾の更新
		@param vAim 目標座標
		"""
		self.topAction.setAimVec2D(vAim)
		bullet = self.topAction.update()
		if bullet is not None:
			self.appendBullet(bullet)
		for i, bullet in enumerate(self.bulletList[:]):
			ret = bullet.update(vAim)
			if ret is not None:
				self.appendBullet(ret)
			if 0 < bullet.pos.x < 640:
				if 0 < bullet.pos.y < 480:
					pass
				else:
					bullet.vanish()
			else:
				bullet.vanish()
			if not(bullet.isExist()):
				"*** BULLET Vanish... ***"
				self.removeBullet(bullet)

class LabelManager:
	""" ラベル管理クラス """
	Bullets = {} # 弾ノードテーブル
	Actions = {} # アクションノードテーブル
	Fires   = {} # Fireノードテーブル
	def __init__(self):
		pass
	def putBullet(label, bullet):
		""" 弾テーブルに登録 """
		print "putBullet ->", label, bullet
		LabelManager.Bullets[label] = bullet
	putBullet = staticmethod(putBullet)
	def getBullet(label):
		""" 弾ノード取得 """
		return LabelManager.Bullets[label]
	getBullet = staticmethod(getBullet)
	def putAction(label, action):
		""" アクションテーブルに登録 """
		print "putAction ->", label, action
		LabelManager.Actions[label] = action
	putAction = staticmethod(putAction)
	def getAction(label):
		""" アクションノード取得 """
		return LabelManager.Actions[label]
	getAction = staticmethod(getAction)
	def putFire(label, fire):
		""" Fireテーブル登録 """
		LabelManager.Fires[label] = fire
	putFire = staticmethod(putFire)
	def getFire(label):
		""" Fireノード取得 """
		return LabelManager.Fires[label]
	getFire = staticmethod(getFire)
	def getTopAction(filename):
		""" BulletML解析 """
		""" "top"アクションとテーブルの登録 """
		result = None
		skips  = Stack()         # 子ノードを読み込んだ場所
		root   = parse(filename) # XML解析
		
		parent = root
		skip   = 0
		while True:
			for i, node in enumerate(parent.childNodes):
				# 兄弟の走査
				if skip > 0:
					# 走査済み
					skip -= 1
					continue
				
				if node.nodeType == node.ELEMENT_NODE:
					# 要素
					if node.tagName == "action":
						label = node.getAttribute("label")
						if label == "top":
							result = node
						elif label != "":
							# 参照ラベルを登録
							LabelManager.putAction(label, node)
					elif node.tagName == "bullet":
						label = node.getAttribute("label")
						# 参照ラベルを登録
						LabelManager.putBullet(label, node)
					elif node.tagName == "fire":
						label = node.getAttribute("label")
						# 参照ラベルを登録
						LabelManager.putFire(label, node)
					if node.hasChildNodes():
						# 子を調べる
						skips.push(i+1)
						parent = node
						break
			else:
				# 走査完了
				# 親ノード取り出し
				skip = skips.pop()
				parent = parent.parentNode
				if parent is None:
					# 親なしなので終わり
					break
		if result is None:
			raise "<action label='top'> not found. "
		else:
			print "BulletML.getTopAction() done.\n"
			return result
	getTopAction = staticmethod(getTopAction)


from pygame.locals import *
import pygame
import sys

def main():
	pygame.init()
	pygame.display.set_mode((640, 480))
	screen = pygame.display.get_surface()
	
	bml = PyBulletML("test.xml") # BulletML読み込み
	while True:
		# ゲームループ
		for e in pygame.event.get():
			if e.type is pygame.QUIT :
				sys.exit()
		pygame.display.update()
		pygame.time.wait(10)
		screen.fill((0, 0, 0, 0))
		x, y = pygame.mouse.get_pos()
		
		bml.update(Vec2D(x, y)) # PyBulletML更新
		for bullet in bml.bulletList:
			# 弾を描画
			pygame.draw.circle(screen, (0, 0, 255, 255), bullet.pos.tuple(), 5)
if(__name__ == "__main__"):
	main()