As mentioned in the previous post, next on the todo list was the game logic and supporting functions required to have the player ship lift the orb using its tractor beam. I was able to spend a few hours almost every day of the previous week and made some nice progress, things are starting to come together nicely now.
A few things had to be sorted out before the orb-lifting game logic could
be implemented as part of the K14Ship step
method. Most notably, I needed
the K14Planet peek
method I already talked about way back when I designed the
first iteration of the
data model for the game. The peek
method should basically implement a planet
query, returning all entities visible from a certain viewpoint and direction. It
is used to check whether the orb is in range of the tractor beam when it is
activated.
In addition to the K14Planet peek
method, a few state tracking variables had
to be added to K14Ship
, indicating whether the tractor beam is active, whether
the orb is in attached (but not lifted) state, and whether it is being lifted. I
won’t go into much detail about these since they are pretty much self-explanatory,
instead I’ll dedicate this post to just the peek
method and the actual orb
lifting game logic.
The planet peek method
The problem to be solved by the peek
method can be formulated as ‘find all
entities (partially) inside the viewing cone defined by an origin (location),
depth (distance) and breadth (field-of-view angle). The following diagram
illustrates the basic idea:
The green squares represent entities that are considered ‘visible’, the red
square is an entity outside the field-of-view. The viewing cone can be extended
to a full 360 degree angle for an omnidirectional field of view. Translating the
relevant parameters for the peek operation, I ended up with the following K14Planet
selector:
-(NSArray ) peekFromPosition: (CGPoint) position
angle: (float) angle
fov: (float) fov
maxDistance: (float) distance;
Instead of thinking the problem through first, I immediately started out hammering in a function that would iterate all entities, and for each entity determine visibility by calculating a vector from the viewing cone origin to the entity center, then testing the angle between this vector and all corner points of the entity bounding box. If one or more corner points were within half of the view direction at less than the viewing distance from the viewing cone origin, the entity was considered ‘visible’. This seemed like a solid approach, but actually it wasn’t. Suppose for example that there’s a really wide entity somewhere very close to the origin of a very narrow, but very deep viewing cone, ie: a small field-of-view angle but a large viewing distance. The viewing cone could cross the entity, but all of the corner points of the entity bounding box could be far outside of it. This wasn’t going to work and I needed to come up with something different.
A quick search on Google revealed a solution so obvious I should have figured it out myself before wasting the better part of 2 hours on a defective solution. Especially considering my coworker Ton already mentioned it to me, having implemented a solution to a similar problem at work. The visibility test is basically a 2D convex polygon intersection (overlap) test between the entity countour (its translated, rotated bounding box), and a polygon representing the viewing cone. The polygon intersection test can be implemented by a very simple and elegant algorithm based on the separating axis theorem. The premise of this theorem is that two convex polygons do not intersect if you can draw a line that separates them, and that you can find such a line by projecting the vertices of both polygons on an orthogonal line (the separating axis) to get the projection intervals (‘shadow’) of both objects. If the intervals do not overlap, any line between them that is orthogonal to the separating axis, will separate the two shapes. The Wikipedia page does a great job explaining the theorem, so I’ll just refer to that for a more in-depth treatment.
The intersection algorithm itself is actually very easy to implement, it
took me only 30 minutes. I still had to add a contour
property to the
K14Entity
method to get the transformed bounding box for an entity, and
inside the K14Planet peek
method I needed to setup a polygon representing
the viewing cone. I chose to sample the arc at the end of the viewing
cone every 1/4⋅π radians (45 degrees), which should me more than sufficient
for visibility testing. The whole peek function now just came down to
iterating all planet entities, getting their contour polygon, and returning
all entities with a contour overlapping the viewing cone polygon.
For anyone interested, here’s the full source code for my 2D convex polygon-polygon intersection function that uses the separating axis theorem, maybe I’ll save someone some time having to implement this themselves. Feel free to use this snippet if you have a use for it:
BOOL polygonsIntersect(NSArray *polygon0, NSArray *polygon1)
{
// If either one of the polygons has less than 3 points (degenerate),
// the rest of this function makes no sense. Just return 'no intersection'.
if ((polygon0.count < 3) || (polygon1.count < 3))
{
return NO;
}
// Iterate edges of the first polygon, and test whether all points of the second
// polygon are on the same side of it. If they do, we found a separating axis,
// and the two polygons do not intersect.
for (int i = 0; i < polygon0.count; i++)
{
CGPoint v0 = [polygon0[i] CGPointValue];
CGPoint v1 = [polygon0[(i+1) % polygon0.count] CGPointValue];
// Create vector perpendicular to edge vector v1-v0
GLKVector2 pv = GLKVector2Normalize(GLKVector2Make(v1.y - v0.y, -(v1.x - v0.x)));
// Project polygon0 and polygon1 points onto p, tracking their projection interval
float t0_min = INFINITY;
float t0_max = -INFINITY;
float t1_min = INFINITY;
float t1_max = -INFINITY;
for (NSValue *p in polygon0)
{
float t = GLKVector2DotProduct(pv, GLKVector2Make(p.CGPointValue.x, p.CGPointValue.y));
t0_min = MIN(t0_min, t);
t0_max = MAX(t0_max, t);
}
for (NSValue *p in polygon1)
{
float t = GLKVector2DotProduct(pv, GLKVector2Make(p.CGPointValue.x, p.CGPointValue.y));
t1_min = MIN(t1_min, t);
t1_max = MAX(t1_max, t);
}
// If the projection intervals for both polygons do not overlap, the current polygon0
// edge is a separating axis, and the polygons do not overlap. We bias the interval
// check towards determining no overlap in case the projection intervals are touching.
if (((t0_min + COMPARE_EPSILON) > t1_max) || ((t0_max - COMPARE_EPSILON) < t1_min))
{
return NO;
}
}
// Tested all polygon1 points against all polygon0 edges, but found no separating
// axis. This means the polygons intersect.
return YES;
}
The orb-lifting game logic
With the peek
method in place, having added a tractor beam input action,
and K14Ship
flags to track whether the tractor beam is active and whether the
orb is attached or already lifted, the actual orb-lifting game logic was pretty
straightforward to implement. Whenever the tractor beam is activated while the
orb is within range, the orb is attached until the tractor beam is deactivated.
When during that time, the distance between the orb and the player ship increases
beyond the tractor beam range, the orb is lifted by creating a Box2D distance joint
that between the ship and the orb. After lifting the orb, it cannot be de-attached
anymore.
All of the orb-lifting game logic is implemented inside the K14Ship step
method
called at regular intervals. The idea is to eventually make this kind of logic
fully scriptable, which means it should only depend on high-level public interfaces.
At this point in time, this isn’t possible yet as I didn’t add abstractions and
interfaces for creating joints between entities, instead I’m calling Box2D directly
from inside the step
method. The following code snippet is taken verbatim from
the K14Ship step
method:
-(void) step: (NSTimeInterval) dt
// ...
// Handle tractor beam game logic
self.tractorBeamActive = [self.planet.game.inputs isInputActive:K14_INPUT_TRACTOR_BEAM];
if (self.tractorBeamActive)
{
if (!self.orbAttached && !self.orbLifted)
{
// Tractor beam active, and orb not attached or being lifted. Peek around
// to see whether the orb is within the tractor beam range. If so, attach it.
NSArray *entities = [self.planet peekFromPosition:self.position
angle:self.angle
fov:2.0f*M_PI
maxDistance:self.tractorBeamRange];
for (K14Entity *entity in entities)
{
if ([entity.entityClass isEqualToString:K14Orb.entityClass])
{
self.orbAttached = YES;
self.orb = (K14Orb *) entity;
break;
}
}
}
if (self.orbAttached && !self.orbLifted)
{
// Tractor beam active while orb attached but not lifted yet. Test whether
// the distance from ship to orb exceeds the tractor beam range. If it does,
// we lift the orb.
CGPoint pp = self.position;
CGPoint op = self.orb.position;
float d = GLKVector2Length(GLKVector2Make(op.x - pp.x, op.y - pp.y));
if (d >= TRACTOR_BEAM_RANGE)
{
// Orb lifted! Create a joint between the ship and orb entity
b2DistanceJointDef jd;
jd.bodyA = self.body;
jd.bodyB = self.orb.body;
jd.length = self.tractorBeamRange;
jd.localAnchorA = b2Vec2(0.0f, 0.0f);
jd.localAnchorB = b2Vec2(0.0f, 0.0f);
self.orbJoint = (b2DistanceJoint *) self.planet.world->CreateJoint(&jd);
self.orbLifted = YES;
}
}
}
else
{
// Tractor beam inactive
if (self.orbAttached && !self.orbLifted)
{
// In case the orb was attached but not yet being lifted, it isn't anymore now...
self.orbAttached = NO;
self.orb = nil;
}
}
// ...
Orb lifting in action
Here’s a video showing orb lifting in action. I simply tweaked the action replay
I’ve been using so far, adding simulated inputs to activate/deactivate the tractor
beam at the right moment. I also added some color-coding to the renderer for debugging
purposes, using a debug color that entities can set on themselves. The K14Ship
entity
sets this color to red when thrust is active, blue when the tractor beam is active, or
purple when both are active. Otherwise it’s set to yellow. To top it off, I also draw
a line between the orb while it is attached. Don’t mind the missing edges at the far
left and right of the planet surface by the way, they got lost as a side effect of
the refactored wraparound code, I didn’t care enough to modify the debug renderer to
render them again. None of this is supposed to be pretty yet! ;-)
Next steps
With world-queries implemented through the K14Planet peek
method, and inputs and
state tracking for the tractor beam, it should be trivial to implement the fuel pickup
logic. After that, it’s probably a good time to add the game logic to kill entities
or the player when collisions, and to implement game termination conditions. I’ll likely
want to add a round of cleanup and refactoring around that time.
Development scoreboard
About 9 hours were added to the development time tally, to get to a total of
about 36 hours. The 9 hours included about 2 hours wasted on my failed attempt
at the K14Planet peek
method, almost 3 hours writing a unit test that
simulates the orb pickup, and another hour or so spend cleaning up some cruft
accumulated in various parts of the code. The SLOC count increased by 81 lines
to 711. Not too bad considering the added functionality.