PyBML
BulletMLをPython用に作ってみました。
ただしkenmoの力不足のため、本家の仕様と結構異なるところがあります。
- actionRefが使えない
- bulletの子要素に複数のactionを指定できない
- bullet時にメインループに制御を返してしまう
- fire時のspeedは親に依存
- directionの値は-180〜180
- パラメータは$1〜$9まで
- vertical、horizonキーワード
- その他、動きが若干おかしいところあり
などなど。(多すぎ?)
たぶん、白い弾幕君などを持ってきても動かない可能性が高いです(ぉぃ
ただ、独自拡張として、
- $cntでループカウンタを取れる(ただし、リピート回数からのデクリメント)
と、式をeval()で評価しているので、
<direction>random.choice([0, 10, 20])</direction>
という奇妙なことができてしまったり。
特にライセンスなどの制約はありませんので、
自由に組み込んでみてください。
(PythonでBulletMLなんて、使う人が存在するのか疑問ですが…)
#!/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()