From 3639cbf64fb5d54061d20b9b072bfcee8a26436d Mon Sep 17 00:00:00 2001 From: Joe Wreschnig Date: Wed, 21 Apr 2010 19:45:41 -0700 Subject: [PATCH 01/16] bulletml-runner: Construct bullets from the document. --- bulletml-runner | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/bulletml-runner b/bulletml-runner index b98e901..d2d9ce0 100755 --- a/bulletml-runner +++ b/bulletml-runner @@ -78,10 +78,8 @@ def main(argv): elif event.key == pygame.K_RETURN: newfile = True elif event.key == pygame.K_s: - actions = [act([], 0.5) for act in doc.actions] - source = bulletml.Bullet( - x=150, y=150, target=target, - actions=actions, rank=0.5) + source = bulletml.Bullet.FromDocument( + doc, x=150, y=150, target=target, rank=0.5) source.vanished = True active.add(source) target.x, target.y = pygame.mouse.get_pos() -- 2.20.1 From 175db2e60dd44cb95cd5e162ec134488439df26d Mon Sep 17 00:00:00 2001 From: Joe Wreschnig Date: Wed, 21 Apr 2010 20:04:58 -0700 Subject: [PATCH 02/16] Action, Bullet: Minor optimizations. --- bulletml/impl.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/bulletml/impl.py b/bulletml/impl.py index ea4893f..dad5601 100644 --- a/bulletml/impl.py +++ b/bulletml/impl.py @@ -4,7 +4,7 @@ from __future__ import division from math import atan2, sin, cos -from bulletml import parser +from bulletml.parser import ActionDef, ActionRef __all__ = ["Action", "Bullet"] @@ -74,8 +74,6 @@ class Action(object): def step(self, owner, created): """Advance by one frame.""" - s_params = self.params - rank = owner.rank if self.speed_frames > 0: self.speed_frames -= 1 @@ -101,6 +99,9 @@ class Action(object): self.wait_frames -= 1 return + s_params = self.params + rank = owner.rank + while True: self.pc += 1 @@ -119,7 +120,7 @@ class Action(object): self.pc = 0 action = self.actions[self.pc] - if isinstance(action, (parser.ActionDef, parser.ActionRef)): + if isinstance(action, (ActionDef, ActionRef)): child = self.Child(action, s_params, rank) owner.replace(self, child) child.step(owner, created) @@ -248,9 +249,11 @@ class Bullet(object): for action in self.actions: action.step(self, created) + speed = self.speed + direction = self.direction self.px = self.x self.py = self.y - self.x += self.mx + sin(self.direction) * self.speed - self.y += -self.my + cos(self.direction) * self.speed + self.x += self.mx + sin(direction) * speed + self.y += -self.my + cos(direction) * speed return created -- 2.20.1 From 67aa3f0b15d5f8122a8f99f5903ec2e2029937c4 Mon Sep 17 00:00:00 2001 From: Joe Wreschnig Date: Wed, 21 Apr 2010 22:26:33 -0700 Subject: [PATCH 03/16] Construct Actions before passing to Bullet. --- bulletml/impl.py | 14 +++++++------- bulletml/parser.py | 5 ++++- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/bulletml/impl.py b/bulletml/impl.py index dad5601..836fb41 100644 --- a/bulletml/impl.py +++ b/bulletml/impl.py @@ -153,8 +153,7 @@ class Bullet(object): """ def __init__(self, x=0, y=0, direction=0, speed=0, target=None, - actions=(), rank=0.5, tags=(), appearance=None, - Action=Action): + actions=(), rank=0.5, tags=(), appearance=None): self.x = self.px = x self.y = self.py = y self.mx = 0 @@ -166,17 +165,18 @@ class Bullet(object): self.rank = rank self.tags = set(tags) self.appearance = appearance - # New bullets reset the parent hierarchy. - self.actions = [Action(None, action, params, rank) - for action, params in actions] + self.actions = list(actions) @classmethod def FromDocument(cls, doc, x=0, y=0, direction=0, speed=0, target=None, params=(), rank=0.5, Action=Action): """Construct a new Bullet from a loaded BulletML document.""" - actions = [a(params, rank) for a in doc.actions] + actions = [action(params, rank) for action in doc.actions] + # New bullets reset the parent hierarchy. + actions = [Action(None, action, params, rank) + for action, params in actions] return cls(x=x, y=y, direction=direction, speed=speed, - target=target, actions=actions, rank=rank, Action=Action) + target=target, actions=actions, rank=rank) def __repr__(self): return ("%s(%r, %r, accel=%r, direction=%r, speed=%r, " diff --git a/bulletml/parser.py b/bulletml/parser.py index aee4f90..8bc23c0 100644 --- a/bulletml/parser.py +++ b/bulletml/parser.py @@ -861,10 +861,13 @@ class FireDef(object): if appearance is None: appearance = owner.appearance + Action = action.__class__ + actions = [Action(None, action, params, rank) + for action, params in actions] bullet = owner.__class__( x=x, y=y, direction=direction, speed=speed, target=owner.target, actions=actions, rank=rank, - appearance=appearance, tags=tags, Action=action.__class__) + appearance=appearance, tags=tags) created.append(bullet) def __repr__(self): -- 2.20.1 From ed0fa877fb0b846eb6227b0e6e4b88ca6e330ef8 Mon Sep 17 00:00:00 2001 From: Joe Wreschnig Date: Thu, 22 Apr 2010 21:45:08 -0700 Subject: [PATCH 04/16] bulletml-runner: Give 120Hz figures to match the FAQ. --- bulletml-runner | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bulletml-runner b/bulletml-runner index d2d9ce0..af79add 100755 --- a/bulletml-runner +++ b/bulletml-runner @@ -110,8 +110,8 @@ def main(argv): if elapsed: seconds_per_bullet = elapsed / count bullets_per_second = count / elapsed - print " %g seconds per bullet (60Hz max: %g)." % ( - seconds_per_bullet, bullets_per_second / 60) + print " %g seconds per bullet (120Hz max: %g)." % ( + seconds_per_bullet, bullets_per_second / 120) screen.fill([0, 0, 0]) for obj in active: -- 2.20.1 From e0a48bf90fd856cb7009eb2140187145c7d0ebaf Mon Sep 17 00:00:00 2001 From: Joe Wreschnig Date: Thu, 22 Apr 2010 21:47:08 -0700 Subject: [PATCH 05/16] Action.step: Remove special-case dependency on ActionDef / ActionRef. --- bulletml/impl.py | 22 ++++------------------ bulletml/parser.py | 41 +++++++++++++++++++++-------------------- 2 files changed, 25 insertions(+), 38 deletions(-) diff --git a/bulletml/impl.py b/bulletml/impl.py index 836fb41..8ef78ab 100644 --- a/bulletml/impl.py +++ b/bulletml/impl.py @@ -4,8 +4,6 @@ from __future__ import division from math import atan2, sin, cos -from bulletml.parser import ActionDef, ActionRef - __all__ = ["Action", "Bullet"] class Action(object): @@ -48,10 +46,6 @@ class Action(object): return "%s(pc=%r, actions=%r)" % ( type(self).__name__, self.pc, self.actions) - def Child(self, action, params, rank, repeat=1): - actions, params = action(params, rank) - return type(self)(self, actions, params, rank, repeat) - def vanish(self): """End this action and its parents.""" if self.parent: @@ -80,8 +74,8 @@ class Action(object): owner.speed += self.speed if self.direction_frames > 0: - # I'm still not sure what the aim check is supposed to do. self.direction_frames -= 1 + # I'm still not sure what the aim check is supposed to do. if self.aiming and self.direction_frames <= 0: owner.direction += owner.aim else: @@ -120,13 +114,7 @@ class Action(object): self.pc = 0 action = self.actions[self.pc] - if isinstance(action, (ActionDef, ActionRef)): - child = self.Child(action, s_params, rank) - owner.replace(self, child) - child.step(owner, created) - break - - elif action(owner, self, s_params, rank, created): + if action(owner, self, s_params, rank, created): break class Bullet(object): @@ -171,10 +159,8 @@ class Bullet(object): def FromDocument(cls, doc, x=0, y=0, direction=0, speed=0, target=None, params=(), rank=0.5, Action=Action): """Construct a new Bullet from a loaded BulletML document.""" - actions = [action(params, rank) for action in doc.actions] - # New bullets reset the parent hierarchy. - actions = [Action(None, action, params, rank) - for action, params in actions] + actions = [action(None, Action, params, rank) + for action in doc.actions] return cls(x=x, y=y, direction=direction, speed=speed, target=target, actions=actions, rank=rank) diff --git a/bulletml/parser.py b/bulletml/parser.py index 8bc23c0..150ca45 100644 --- a/bulletml/parser.py +++ b/bulletml/parser.py @@ -367,10 +367,7 @@ class Repeat(object): def __call__(self, owner, action, params, rank, created): repeat = self.times(params, rank) - child = action.Child(self.action, params, rank, repeat) - owner.replace(action, child) - child.step(owner, created) - return True + return self.action(owner, action, params, rank, created, repeat) def __repr__(self): return "%s(%r, %r)" % (type(self).__name__, self.times, self.action) @@ -421,10 +418,7 @@ class If(object): branch = self.else_ if branch: - child = action.Child(branch, params, rank) - owner.replace(action, child) - child.step(owner, created) - return True + return branch(owner, action, params, rank, created) def __repr__(self): if self.else_: @@ -566,8 +560,9 @@ class BulletDef(object): doc._bullets[element.get("label")] = dfn return dfn - def __call__(self, params, rank): - actions = [action(params, rank) for action in self.actions] + def __call__(self, owner, action, params, rank, created): + actions = [a(None, action, params, rank, created) + for a in self.actions] return ( self.direction and self.direction(params, rank), self.speed and self.speed(params, rank), @@ -607,8 +602,9 @@ class BulletRef(object): doc._bullet_refs.append(bullet) return bullet - def __call__(self, params, rank): - return self.bullet(self.params(params, rank), rank) + def __call__(self, owner, action, params, rank, created): + params = self.params(params, rank) + return self.bullet(owner, action, params, rank, created) def __repr__(self): return "%s(params=%r, bullet=%r)" % ( @@ -652,8 +648,14 @@ class ActionDef(object): doc._actions[element.get("label")] = dfn return dfn - def __call__(self, params, rank): - return self.actions, params + def __call__(self, owner, action, params, rank, created=(), repeat=1): + Action = action if isinstance(action, type) else type(action) + parent = None if owner is None else action + child = Action(parent, self.actions, params, rank, repeat) + if owner is not None: + owner.replace(parent, child) + child.step(owner, created) + return child def __repr__(self): return "%s(%r)" % (type(self).__name__, self.actions) @@ -686,8 +688,9 @@ class ActionRef(object): doc._action_refs.append(action) return action - def __call__(self, params, rank): - return self.action(self.params(params, rank), rank) + def __call__(self, owner, action, params, rank, created=(), repeat=1): + params = self.params(params, rank) + return self.action(owner, action, params, rank, created, repeat) def __repr__(self): return "%s(params=%r, action=%r)" % ( @@ -811,7 +814,8 @@ class FireDef(object): return fire def __call__(self, owner, action, params, rank, created): - direction, speed, tags, appearance, actions = self.bullet(params, rank) + direction, speed, tags, appearance, actions = self.bullet( + owner, action, params, rank, created) if self.direction: direction = self.direction(params, rank) if self.speed: @@ -861,9 +865,6 @@ class FireDef(object): if appearance is None: appearance = owner.appearance - Action = action.__class__ - actions = [Action(None, action, params, rank) - for action, params in actions] bullet = owner.__class__( x=x, y=y, direction=direction, speed=speed, target=owner.target, actions=actions, rank=rank, -- 2.20.1 From 382c381aa58db92e81145d49f193f79fdfa72450 Mon Sep 17 00:00:00 2001 From: Joe Wreschnig Date: Fri, 23 Apr 2010 01:25:20 -0700 Subject: [PATCH 06/16] bulletml-runner: Flash the screen when the mouse hits a bullet. --- bulletml-runner | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/bulletml-runner b/bulletml-runner index af79add..f361d75 100755 --- a/bulletml-runner +++ b/bulletml-runner @@ -86,7 +86,10 @@ def main(argv): target.x /= 2 target.y /= 2 target.y = 300 - target.y + target.px = target.x + target.py = target.y + collides = False if not paused or go: lactive = list(active) start = time.time() @@ -100,7 +103,7 @@ def main(argv): or not (-50 < obj.y < 350)): active.remove(obj) if lactive: - collides_all(lactive[0], lactive) + collides = collides_all(target, lactive) elapsed = time.time() - start frames += 1 @@ -113,7 +116,7 @@ def main(argv): print " %g seconds per bullet (120Hz max: %g)." % ( seconds_per_bullet, bullets_per_second / 120) - screen.fill([0, 0, 0]) + screen.fill([0, 0, 64] if collides else [0, 0, 0] ) for obj in active: try: x, y = obj.x, obj.y -- 2.20.1 From eed0de88f98b23ecb25507b47a507e2791861334 Mon Sep 17 00:00:00 2001 From: Joe Wreschnig Date: Fri, 23 Apr 2010 01:26:33 -0700 Subject: [PATCH 07/16] Rewrite collision extension to not require pyrex. Aside from removing the dependency collision checking is >10% faster in simple benchmarks. --- bulletml/__init__.py | 6 +- bulletml/_collision.c | 241 ++++++++++++++++++++++++++++++++++++++++ bulletml/_collision.pyx | 178 ----------------------------- setup.py | 18 +-- 4 files changed, 249 insertions(+), 194 deletions(-) create mode 100644 bulletml/_collision.c delete mode 100644 bulletml/_collision.pyx diff --git a/bulletml/__init__.py b/bulletml/__init__.py index c31dce7..0d4cd94 100644 --- a/bulletml/__init__.py +++ b/bulletml/__init__.py @@ -36,11 +36,11 @@ attributes that can be used to influence it. from bulletml.parser import BulletML from bulletml.impl import Bullet -from bulletml.collision import overlaps, collides +from bulletml.collision import overlaps, collides, collides_all -VERSION = (1,) +VERSION = (2,) VERSION_STRING = ".".join(map(str, VERSION)) __all__ = ["VERSION", "VERSION_STRING", "Bullet", "BulletML", - "overlaps", "collides"] + "overlaps", "collides", "collides_all"] diff --git a/bulletml/_collision.c b/bulletml/_collision.c new file mode 100644 index 0000000..9b220a4 --- /dev/null +++ b/bulletml/_collision.c @@ -0,0 +1,241 @@ +#include "Python.h" + +#define STR_AND_SIZE(s) s, sizeof(s) - 1 +#define DOT(x1, y1, x2, y2) ((x1) * (x2) + (y1) * (y2)) +#define NEARZERO(d) ((d) < 0.0001 && (d) > -0.0001) + +static const char *s_pchModDoc = "Optimized collision detection functions."; + +static PyObject *s_ppykX; +static PyObject *s_ppykY; +static PyObject *s_ppykPX; +static PyObject *s_ppykPY; +static PyObject *s_ppykRadius; + +static const char s_achOverlapsDoc[] = ( + "Return true if two circles are overlapping.\n\n" + "Usually, you\'ll want to use the \'collides\' method instead, but\n" + "this one can be useful for just checking to see if the player has\n" + "entered an area or hit a stationary oject.\n\n" + "(This function is optimized.)\n\n"); + +static const char s_achCollidesDoc[] = ( + "Return true if the two moving circles collide.\n\n" + "The circles should have the following attributes:\n\n" + " x, y - required, current position\n" + " px, py - not required, defaults to x, y, previous frame position\n" + " radius - not required, defaults to 0.5\n\n" + "(This function is optimized.)\n\n"); + +static const char s_achCollidesAllDoc[] = ( + "Filter the second argument to those that collide with the first.\n\n" + "This is equivalent to filter(lambda o: collides(a, o), others),\n" + "but is much faster when the compiled extension is available (which\n" + "it is currently).\n\n"); + +// Get the attributes from a Python moving circle object. +static int GetCircle(PyObject *ppy, double *pdX, double *pdY, + double *pdPX, double *pdPY, double *pdR) +{ + PyObject *ppyf; + + if (!ppy) + return 0; + + ppyf = PyObject_GetAttr(ppy, s_ppykX); + if (ppyf) + { + *pdX = PyFloat_AsDouble(ppyf); + Py_DECREF(ppyf); + } + + ppyf = PyObject_GetAttr(ppy, s_ppykY); + if (ppyf) + { + *pdY = PyFloat_AsDouble(ppyf); + Py_DECREF(ppyf); + } + + // Catch X or Y or failure to convert, any one of the four cases + // is equally fatal. We don't need to check after each one. + if (PyErr_Occurred()) + return 0; + + ppyf = PyObject_GetAttr(ppy, s_ppykPX); + if (ppyf) + { + *pdPX = PyFloat_AsDouble(ppyf); + Py_DECREF(ppyf); + if (PyErr_Occurred()) + return 0; + } + else + { + PyErr_Clear(); + *pdPX = *pdX; + } + + ppyf = PyObject_GetAttr(ppy, s_ppykPY); + if (ppyf) + { + *pdPY = PyFloat_AsDouble(ppyf); + Py_DECREF(ppyf); + if (PyErr_Occurred()) + return 0; + } + else + { + PyErr_Clear(); + *pdPY = *pdY; + } + + ppyf = PyObject_GetAttr(ppy, s_ppykRadius); + if (ppyf) + { + *pdR = PyFloat_AsDouble(ppyf); + Py_DECREF(ppyf); + if (PyErr_Occurred()) + return 0; + } + else + { + PyErr_Clear(); + *pdR = 0.5; + } + + return 1; +} + +static int Collides(double dXA, double dXB, double dYA, double dYB, + double dPXA, double dPXB, double dPYA, double dPYB, + double dRA, double dRB) +{ + // Translate B's position to be relative to A's start. + double dDirX = dPXA + (dXB - dXA) - dPXB; + double dDirY = dPYA + (dYB - dYA) - dPYB; + // Now A doesn't move. Treat B as a point by summing the radii. + double dR = dRA + dRB; + // Now the problem is just circle/line collision. + + double dDiffX = dPXA - dPXB; + double dDiffY = dPYA - dPYB; + + // B didn't move relative to A, so early-out by doing point/circle. + if (NEARZERO(dDirX) && NEARZERO(dDirY)) + return dDiffX * dDiffX + dDiffY * dDiffY <= dR * dR; + else + { + double dT = (DOT(dDiffX, dDiffY, dDirX, dDirY) + / DOT(dDirX, dDirY, dDirX, dDirY)); + double dDistX; + double dDistY; + if (dT < 0.0) dT = 0.0; + else if (dT > 1.0) dT = 1.0; + + dDistX = dPXA - (dPXB + dDirX * dT); + dDistY = dPYA - (dPYB + dDirY * dT); + + return dDistX * dDistX + dDistY * dDistY <= dR * dR; + } +} + +static PyObject *py_overlaps(PyObject *ppySelf, PyObject *ppyArgs) { + double dXA, dYA, dPXA, dPYA, dRA; + double dXB, dYB, dPXB, dPYB, dRB; + PyObject *ppyA, *ppyB; + if (PyArg_ParseTuple(ppyArgs, "OO", &ppyA, &ppyB) + && GetCircle(ppyA, &dXA, &dYA, &dPXA, &dPYA, &dRA) + && GetCircle(ppyB, &dXB, &dYB, &dPXB, &dPYB, &dRB)) + { + double dX = dXA - dXB; + double dY = dYA - dYB; + double dR = dRA + dRB; + + if (dX * dX + dY * dY <= dR * dR) + { + Py_RETURN_TRUE; + } + else + { + Py_RETURN_FALSE; + } + } + else + return NULL; +} + +static PyObject *py_collides(PyObject *ppySelf, PyObject *ppyArgs) +{ + double dXA, dYA, dPXA, dPYA, dRA; + double dXB, dYB, dPXB, dPYB, dRB; + PyObject *ppyA, *ppyB; + if (PyArg_ParseTuple(ppyArgs, "OO", &ppyA, &ppyB) + && GetCircle(ppyA, &dXA, &dYA, &dPXA, &dPYA, &dRA) + && GetCircle(ppyB, &dXB, &dYB, &dPXB, &dPYB, &dRB)) + { + if (Collides(dXA, dXB, dYA, dYB, dPXA, dPXB, dPYA, dPYB, dRA, dRB)) + { + Py_RETURN_TRUE; + } + else + { + Py_RETURN_FALSE; + } + } + else + return NULL; +} + +static PyObject *py_collides_all(PyObject *ppySelf, PyObject *ppyArgs) +{ + double dXA, dYA, dPXA, dPYA, dRA; + PyObject *ppyA, *ppyOthers; + if (PyArg_ParseTuple(ppyArgs, "OO", &ppyA, &ppyOthers) + && GetCircle(ppyA, &dXA, &dYA, &dPXA, &dPYA, &dRA)) + { + PyObject *ppyRet = PyList_New(0); + Py_ssize_t pyszLen = ppyRet ? PySequence_Length(ppyOthers) : -1; + if (pyszLen >= 0) + { + Py_ssize_t sz; + for (sz = 0; sz < pyszLen; sz++) + { + double dXB, dYB, dPXB, dPYB, dRB; + PyObject *ppyB = PySequence_GetItem(ppyOthers, sz); + if (!GetCircle(ppyB, &dXB, &dYB, &dPXB, &dPYB, &dRB)) + { + Py_XDECREF(ppyB); + return NULL; + } + else if (Collides(dXA, dXB, dYA, dYB, dPXA, dPXB, dPYA, dPYB, + dRA, dRB)) + PyList_Append(ppyRet, ppyB); + Py_DECREF(ppyB); + } + return ppyRet; + } + else + return NULL; + } + else + return NULL; +} + +static struct PyMethodDef s_apymeth[] = { + {"overlaps", py_overlaps, METH_VARARGS, s_achOverlapsDoc }, + {"collides", py_collides, METH_VARARGS, s_achCollidesDoc }, + {"collides_all", py_collides_all, METH_VARARGS, s_achCollidesAllDoc }, + {NULL, NULL, 0, NULL} +}; + +PyMODINIT_FUNC init_collision(void) +{ + s_ppykX = PyString_FromStringAndSize(STR_AND_SIZE("x")); + s_ppykY = PyString_FromStringAndSize(STR_AND_SIZE("y")); + s_ppykPX = PyString_FromStringAndSize(STR_AND_SIZE("px")); + s_ppykPY = PyString_FromStringAndSize(STR_AND_SIZE("py")); + s_ppykRadius = PyString_FromStringAndSize(STR_AND_SIZE("radius")); + + if (s_ppykX && s_ppykY && s_ppykPX && s_ppykPY && s_ppykRadius) + Py_InitModule3("bulletml._collision", s_apymeth, s_pchModDoc); +} diff --git a/bulletml/_collision.pyx b/bulletml/_collision.pyx deleted file mode 100644 index b057022..0000000 --- a/bulletml/_collision.pyx +++ /dev/null @@ -1,178 +0,0 @@ -"""Optimized collision detection functions.""" - -def overlaps(a, b): - """Return true if two circles are overlapping. - - Usually, you'll want to use the 'collides' method instead, but - this one can be useful for just checking to see if the player has - entered an area or hit a stationary oject. - - (This function is optimized.) - - """ - - cdef float ax - cdef float bx - cdef float ay - cdef float by - cdef float dx - cdef float dy - cdef float radius_a - cdef float radius_b - cdef float radius - - ax = a.x - bx = b.x - ay = a.y - by = b.y - - dx = ax - bx - dy = ay - by - - radius_a = getattr3(a, 'radius', 0.5) - radius_b = getattr3(b, 'radius', 0.5) - radius = radius_a + radius_b - - return dx * dx + dy * dy <= radius * radius - - -cdef int _collides(float xa, float xb, float ya, float yb, - float pxa, float pxb, float pya, float pyb, - float radius_a, float radius_b): - - """Check collision for two moving circles.""" - - cdef float dir_x - cdef float dir_y - - cdef float diff_x - cdef float diff_y - cdef float dist_x - cdef float dist_y - - cdef float dx - cdef float dy - cdef float t - - cdef float radius - - radius = radius_a + radius_b - - # Translate b's final position to be relative to a's start. - # And now, circle/line collision. - dir_x = pxa + (xb - xa) - pxb - dir_y = pya + (yb - ya) - pyb - - if (dir_x < 0.0001 and dir_x > -0.0001 - and dir_y < 0.0001 and dir_y > -0.0001): - # b did not move relative to a, so do point/circle. - dx = pxb - pxa - dy = pyb - pya - return dx * dx + dy * dy < radius * radius - - diff_x = pxa - pxb - diff_y = pya - pyb - - # dot(diff, dir) / dot(dir, dir) - t = (diff_x * dir_x + diff_y * dir_y) / (dir_x * dir_x + dir_y * dir_y) - if t < 0: - t = 0 - elif t > 1: - t = 1 - - dist_x = pxa - (pxb + dir_x * t) - dist_y = pya - (pyb + dir_y * t) - - # dist_sq < radius_sq - return dist_x * dist_x + dist_y * dist_y <= radius * radius - -def collides(a, b): - """Return true if the two moving circles collide. - - a and b should have the following attributes: - - x, y - required, current position - px, py - not required, defaults to x, y, previous frame position - radius - not required, defaults to 0.5 - - (This function is optimized.) - - """ - cdef float xa - cdef float xb - cdef float ya - cdef float yb - - cdef float pxa - cdef float pya - cdef float pxb - cdef float pyb - - cdef float radius_a - cdef float radius_b - - xa = a.x - xb = b.x - ya = a.y - yb = b.y - - radius_a = getattr3(a, 'radius', 0.5) - radius_b = getattr3(b, 'radius', 0.5) - - pxa = getattr3(a, 'px', xa) - pya = getattr3(a, 'py', ya) - pxb = getattr3(b, 'px', xb) - pyb = getattr3(b, 'py', yb) - - return _collides(xa, xb, ya, yb, pxa, pxb, pya, pyb, radius_a, radius_b) - -def collides_all(a, others): - """Filter the second argument to those that collide with the first. - - This is equivalent to filter(lambda o: collides(a, o), others), - but is much faster when the compiled extension is available (which - it is currently). - - """ - cdef float xa - cdef float xb - cdef float ya - cdef float yb - - cdef float pxa - cdef float pya - cdef float pxb - cdef float pyb - - cdef float radius_a - cdef float radius_b - - cdef list bs - cdef int length - - cdef list colliding - - cdef int coll - - colliding = [] - - xa = a.x - ya = a.y - radius_a = getattr3(a, 'radius', 0.5) - pxa = getattr3(a, 'px', xa) - pya = getattr3(a, 'py', ya) - - bs = list(others) - length = len(bs) - - for 0 <= i < length: - b = others[i] - xb = b.x - yb = b.y - radius_b = getattr3(b, 'radius', 0.5) - pxb = getattr3(b, 'px', xb) - pyb = getattr3(b, 'py', yb) - - if _collides(xa, xb, ya, yb, pxa, pxb, pya, pyb, radius_a, radius_b): - colliding.append(b) - return colliding diff --git a/setup.py b/setup.py index 54a86d1..fb4b2e3 100755 --- a/setup.py +++ b/setup.py @@ -6,16 +6,6 @@ import shutil import sys from distutils.core import setup, Command, Extension - -try: - from Pyrex.Distutils import build_ext -except ImportError: - from distutils.command.build_ext import build_ext - ext_modules = [] -else: - ext_modules = [Extension( - 'bulletml._collision', [os.path.join('bulletml', '_collision.pyx')])] - from distutils.command.clean import clean as distutils_clean from distutils.command.sdist import sdist as distutils_sdist @@ -114,8 +104,8 @@ class test_cmd(Command): if __name__ == "__main__": setup(cmdclass=dict(clean=clean, test=test_cmd, coverage=coverage_cmd, - sdist=sdist, build_ext=build_ext), - name="python-bulletml", version="1", + sdist=sdist), + name="python-bulletml", version="2", url="http://code.google.com/p/python-bulletml/", description="parse and run BulletML scripts", author="Joe Wreschnig", @@ -124,7 +114,9 @@ if __name__ == "__main__": packages=["bulletml"], data_files=glob.glob("examples/*/*.xml") + ["examples/template.xml"], scripts=["bulletml-runner", "bulletml-to-bulletyaml"], - ext_modules=ext_modules, + ext_modules=[Extension( + 'bulletml._collision', + [os.path.join('bulletml', '_collision.c')])], long_description="""\ BulletML is the Bullet Markup Language. BulletML can describe the barrage of bullets in shooting games. (For example Progear, Psyvariar, -- 2.20.1 From 303a38b637b4e0013c442bc78c63df51880ceaeb Mon Sep 17 00:00:00 2001 From: Joe Wreschnig Date: Fri, 23 Apr 2010 01:39:35 -0700 Subject: [PATCH 08/16] Better conditional / appearance test. --- examples/popcorn/towards.xml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/examples/popcorn/towards.xml b/examples/popcorn/towards.xml index da70f59..1db6dd2 100644 --- a/examples/popcorn/towards.xml +++ b/examples/popcorn/towards.xml @@ -3,6 +3,14 @@ - + $rand > 0.7 + red + + $rand > 0.5 + blue + green + + + -- 2.20.1 From dfa19460a1f1a27431ed5a4a86ebe0334ece15cf Mon Sep 17 00:00:00 2001 From: Joe Wreschnig Date: Fri, 23 Apr 2010 01:46:56 -0700 Subject: [PATCH 09/16] Fix some example validity issues. --- examples/boss/template.xml | 8 -------- examples/popcorn/towards.xml | 21 +++++++++++---------- 2 files changed, 11 insertions(+), 18 deletions(-) delete mode 100644 examples/boss/template.xml diff --git a/examples/boss/template.xml b/examples/boss/template.xml deleted file mode 100644 index 7065d9b..0000000 --- a/examples/boss/template.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - diff --git a/examples/popcorn/towards.xml b/examples/popcorn/towards.xml index 1db6dd2..58ad5fb 100644 --- a/examples/popcorn/towards.xml +++ b/examples/popcorn/towards.xml @@ -1,16 +1,17 @@ - + - $rand > 0.7 - red - - $rand > 0.5 - blue - green - - - + $rand > 0.7 + red + + $rand > 0.5 + blue + green + + + -- 2.20.1 From ee9429c2c319d5d794e49da7b0fe13fca9945194 Mon Sep 17 00:00:00 2001 From: Joe Wreschnig Date: Fri, 23 Apr 2010 01:56:30 -0700 Subject: [PATCH 10/16] collision: Docstring should not mention pyrex. --- NEWS.txt | 5 +++++ bulletml/collision.py | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/NEWS.txt b/NEWS.txt index 70d15d1..08c7617 100644 --- a/NEWS.txt +++ b/NEWS.txt @@ -1,2 +1,7 @@ +2 - 2010.04.?? + * Easier to extend Action with new behaviors. + * Conditional // elements. + * Faster Pyrex-less collision extension. + 1 - 2010.04.10 * Initial release. diff --git a/bulletml/collision.py b/bulletml/collision.py index 0394ed3..1683e58 100644 --- a/bulletml/collision.py +++ b/bulletml/collision.py @@ -4,8 +4,9 @@ This module provides simple collision checking appropriate for shmups. It provides a routine to check whether two moving circles collided during the past frame. -If Pyrex was available when installing, this will used optimized -versions of the functions. +An equivalent C-based version will be used automatically if it was +compiled and installed with the module. If available, it will be noted +in the docstrings for the functions. Basic Usage: -- 2.20.1 From 4c9526a453f57e54c603dcad10d2346d06b69fe4 Mon Sep 17 00:00:00 2001 From: Joe Wreschnig Date: Fri, 23 Apr 2010 01:57:40 -0700 Subject: [PATCH 11/16] Bullet.__repr__: Fix format string. --- bulletml/impl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bulletml/impl.py b/bulletml/impl.py index 8ef78ab..f7edd5a 100644 --- a/bulletml/impl.py +++ b/bulletml/impl.py @@ -166,7 +166,7 @@ class Bullet(object): def __repr__(self): return ("%s(%r, %r, accel=%r, direction=%r, speed=%r, " - "actions=%r, target=%r, appearance=vanished=%r)") % ( + "actions=%r, target=%r, appearance=%r, vanished=%r)") % ( type(self).__name__, self.x, self.y, (self.mx, self.my), self.direction, self.speed, self.actions, self.target, self.appearance, self.vanished) -- 2.20.1 From 75cf6c9dd1fa5aef18f11e08bcc3728c1175b1c4 Mon Sep 17 00:00:00 2001 From: Joe Wreschnig Date: Sat, 24 Apr 2010 01:23:48 -0700 Subject: [PATCH 12/16] Give bullets a radius by default. Use try/except rather than getattr during collision, as Bullet has all of those by default. Remove abs calls. --- bulletml/bulletyaml.py | 2 +- bulletml/collision.py | 32 ++++++++++++++++++++------------ bulletml/impl.py | 7 +++++-- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/bulletml/bulletyaml.py b/bulletml/bulletyaml.py index 2503c9d..2ec2f08 100644 --- a/bulletml/bulletyaml.py +++ b/bulletml/bulletyaml.py @@ -30,7 +30,7 @@ def register(Loader=None, Dumper=None): parser.Repeat, parser.Accel, parser.BulletDef, parser.BulletRef, parser.ActionDef, parser.ActionRef, parser.FireDef, parser.FireRef, parser.Offset, - parser.BulletML]: + parser.Appearance, parser.If, parser.BulletML]: def add(cls, loader, dumper): """Register a class in a new variable scope.""" diff --git a/bulletml/collision.py b/bulletml/collision.py index 1683e58..8aa17d3 100644 --- a/bulletml/collision.py +++ b/bulletml/collision.py @@ -30,7 +30,10 @@ def overlaps(a, b): dx = a.x - b.x dy = a.y - b.y - radius = getattr(a, 'radius', 0.5) + getattr(b, 'radius', 0.5) + try: + radius = a.radius + b.radius + except AttributeError: + radius = getattr(a, 'radius', 0.5) + getattr(b, 'radius', 0.5) return dx * dx + dy * dy <= radius * radius @@ -53,27 +56,32 @@ def collides(a, b): yb = b.y # Treat b as a point, we only need one radius. - radius = getattr(a, 'radius', 0.5) + getattr(b, 'radius', 0.5) + try: + radius = a.radius + b.radius + except AttributeError: + radius = getattr(a, 'radius', 0.5) + getattr(b, 'radius', 0.5) # Previous frame locations. - pxa = getattr(a, 'px', xa) - pya = getattr(a, 'py', ya) - pxb = getattr(b, 'px', xb) - pyb = getattr(b, 'py', yb) + try: pxa = a.px + except KeyError: pxa = xa + try: pya = a.py + except KeyError: pya = ya + try: pxb = b.px + except KeyError: pxb = xb + try: pyb = b.py + except KeyError: pyb = yb # Translate b's final position to be relative to a's start. # And now, circle/line collision. dir_x = pxa + (xb - xa) - pxb dir_y = pya + (yb - ya) - pyb - if abs(dir_x) < 0.0001 and abs(dir_y) < 0.0001: - # b did not move relative to a, so do point/circle. - dx = pxb - pxa - dy = pyb - pya - return dx * dx + dy * dy < radius * radius - diff_x = pxa - pxb diff_y = pya - pyb + if (dir_x < 0.0001 and dir_x > -0.0001 + and dir_y < 0.0001 and dir_y > -0.0001): + # b did not move relative to a, so do point/circle. + return diff_x * diff_x + diff_y * diff_y < radius * radius # dot(diff, dir) / dot(dir, dir) t = (diff_x * dir_x + diff_y * dir_y) / (dir_x * dir_x + dir_y * dir_y) diff --git a/bulletml/impl.py b/bulletml/impl.py index f7edd5a..4785ef2 100644 --- a/bulletml/impl.py +++ b/bulletml/impl.py @@ -131,9 +131,10 @@ class Bullet(object): rank - game difficulty, 0 to 1, default 0.5 tags - string tags set by the running actions appearance - string used to set bullet appearance + radius - radius for collision Contructor Arguments: - x, y, direction, speed, target, rank, tags, appearance + x, y, direction, speed, target, rank, tags, appearance, radius - same as the above attributes actions - internal action list Action - custom Action constructor @@ -141,9 +142,11 @@ class Bullet(object): """ def __init__(self, x=0, y=0, direction=0, speed=0, target=None, - actions=(), rank=0.5, tags=(), appearance=None): + actions=(), rank=0.5, tags=(), appearance=None, + radius=0.5): self.x = self.px = x self.y = self.py = y + self.radius = radius self.mx = 0 self.my = 0 self.direction = direction -- 2.20.1 From 261aefba910dfd4511284c48e5e0b136a1f7f32a Mon Sep 17 00:00:00 2001 From: Joe Wreschnig Date: Sat, 24 Apr 2010 01:28:10 -0700 Subject: [PATCH 13/16] Bullet.step: Manage finished state here. Although this increases the time spent in bullet.step, all calls to that will be followed by checks of bullet.finished anyway, and a real attribute is cheaper than the callable. --- bulletml/impl.py | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/bulletml/impl.py b/bulletml/impl.py index 4785ef2..a2e8dec 100644 --- a/bulletml/impl.py +++ b/bulletml/impl.py @@ -132,6 +132,7 @@ class Bullet(object): tags - string tags set by the running actions appearance - string used to set bullet appearance radius - radius for collision + finished - true if all actions are finished and the bullet vanished Contructor Arguments: x, y, direction, speed, target, rank, tags, appearance, radius @@ -152,6 +153,7 @@ class Bullet(object): self.direction = direction self.speed = speed self.vanished = False + self.finished = False self.target = target self.rank = rank self.tags = set(tags) @@ -188,24 +190,6 @@ class Bullet(object): else: return atan2(target_x - self.x, target_y - self.y) - @property - def finished(self): - """Check if this bullet is finished running. - - A bullet is finished when it has vanished, and all its - actions have finished. - - If this is true, the bullet should be removed from the screen. - (You will probably want to cull it under other circumstances - as well). - """ - if not self.vanished: - return False - for action in self.actions: - if not action.finished: - return False - return True - def vanish(self): """Vanish this bullet and stop all actions.""" self.vanished = True @@ -235,8 +219,11 @@ class Bullet(object): """ created = [] + finished = self.vanished for action in self.actions: action.step(self, created) + finished = finished and action.finished + self.finished = finished speed = self.speed direction = self.direction -- 2.20.1 From 9da06c24edb2148cf6b773b58d280a1db407c5ce Mon Sep 17 00:00:00 2001 From: Joe Wreschnig Date: Sat, 24 Apr 2010 01:37:02 -0700 Subject: [PATCH 14/16] Correct bad docstrings. --- bulletml/__init__.py | 6 ++++-- bulletml/collision.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/bulletml/__init__.py b/bulletml/__init__.py index 0d4cd94..62a1d04 100644 --- a/bulletml/__init__.py +++ b/bulletml/__init__.py @@ -9,8 +9,10 @@ renderer-agnostic. In addition to the standard BulletML XML format, this module supports an equivalent YAML format. See bulletml.bulletyaml for more details. -Finally, two simple collision routines are provided, bulletml.overlaps -for stationary circles and bulletml.collides for moving circles. +Finally, three simple collision routines are provided: +bulletml.overlaps for stationary circles, bulletml.collides for moving +circles, and bulletml.collides_all for one moving circle against many +moving circles. More information is available at the BulletML homepage, http://www.asahi-net.or.jp/~cs8k-cyu/bulletml/index_e.html, or the diff --git a/bulletml/collision.py b/bulletml/collision.py index 8aa17d3..2e88b3c 100644 --- a/bulletml/collision.py +++ b/bulletml/collision.py @@ -1,7 +1,7 @@ """Simple collision check. This module provides simple collision checking appropriate for -shmups. It provides a routine to check whether two moving circles +shmups. It provides routines to check whether two moving circles collided during the past frame. An equivalent C-based version will be used automatically if it was -- 2.20.1 From 4c11ea4b779470d67cad2f6a060a51424625cff9 Mon Sep 17 00:00:00 2001 From: Joe Wreschnig Date: Sat, 24 Apr 2010 01:43:07 -0700 Subject: [PATCH 15/16] FireDef.__call__: Minor optimizations. --- bulletml/parser.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/bulletml/parser.py b/bulletml/parser.py index 150ca45..1470ed3 100644 --- a/bulletml/parser.py +++ b/bulletml/parser.py @@ -816,15 +816,15 @@ class FireDef(object): def __call__(self, owner, action, params, rank, created): direction, speed, tags, appearance, actions = self.bullet( owner, action, params, rank, created) - if self.direction: + if self.direction is not None: direction = self.direction(params, rank) - if self.speed: + if self.speed is not None: speed = self.speed(params, rank) tags = tags.union(self.tags) - if self.appearance: + if self.appearance is not None: appearance = self.appearance - if direction: + if direction is not None: direction, type = direction if type == "aim" or type is None: direction += owner.aim @@ -836,7 +836,7 @@ class FireDef(object): direction = owner.aim action.previous_fire_direction = direction - if speed: + if speed is not None: speed, type = speed if type == "sequence": speed += action.previous_fire_speed @@ -851,8 +851,9 @@ class FireDef(object): speed = 1 action.previous_fire_speed = speed - x, y = owner.x, owner.y - if self.offset: + x = owner.x + y = owner.y + if self.offset is not None: off_x, off_y = self.offset(params, rank) if self.offset.type == "relative": s = sin(direction) -- 2.20.1 From 62705a2613f321c698ba7194325a53d1dd0dc27a Mon Sep 17 00:00:00 2001 From: Joe Wreschnig Date: Sat, 24 Apr 2010 01:43:22 -0700 Subject: [PATCH 16/16] expr: Improve documentation. --- bulletml/expr.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bulletml/expr.py b/bulletml/expr.py index a00c99e..385b31b 100644 --- a/bulletml/expr.py +++ b/bulletml/expr.py @@ -11,6 +11,8 @@ import re from bulletml.errors import Error +__all__ = ["ExprError", "NumberDef", "INumberDef"] + class ExprError(Error): """Raised when an invalid expression is evaluated/compiled.""" pass @@ -19,7 +21,6 @@ class NumberDef(object): """BulletML numeric expression. This translates BulletML numeric expressions into Python expressions. - The Examples: 35 @@ -75,6 +76,7 @@ class INumberDef(NumberDef): self._value = int(round(self._value)) def __call__(self, params, rank): + # Avoid int(round(__call__())) overhead for constants. if self._value is not None: return self._value return int(round(super(INumberDef, self).__call__(params, rank))) -- 2.20.1