Light Racer 2.0 - Days 12-14 - The Decimator NPC

NPC stands for Non-Player Character. It's anything in the game that acts upon the world but is never directly controller by a player. Light Racer 2.0's only NPC is the Decimator. The Decimator is a menacing machine that flies in when the round is taking too long and starts smashing things up. Since it's the first object that can travel any direction, the physics math was a little bit different than for all of the existing code. Also, it needed some unique collision detection because it hits an area instead of hitting a straight line like the racers do. While the Decimator is an ugly purple pentagon at the end of the day, I'm happy with how it feels within the game. I think that with a little graphics and sound TLC, it'll be an excellent addition.

Day 12 actually started with the addition of the Invincibility Item, which was designed the day before. I'm slightly behind schedule because I've been taking extra time to work the graphics out to where I'm happy with them before moving on. I don't want to end up with a week of extra graphics work at the end of the project. Anyway, the invincibility item was fairly easy to implement. I just added a flag to the Player called "isInvincible" and added a timer, which is updated on update(), and some drawing code to show the invincibility. This way the item just "tells" the player "be invincible for 2 seconds" and the Player abides. Nice!

Day 13 didn't have very many hours in it so I was only able to start implementing the Decimator. I got the basic skeleton for the NPC added and some drawing code to draw a stationary purple pentagon sprite.

Day 14 is where all of the fun happened. I have 2 videos that show the progress. At first I had some bugs in the state-logic for the Decimator. Here's how it works:

States: None, Target, Drop, Rise, Flee

The update() method runs the block of state-management code which switches from one state to the next, depending on the variables. Here's the update() method from the Decimator:

public void update(LightRacerWorld world) {
// if no action or action is target and target no longer valid, determine target.
// if not over target, go to target
// if over target and not dropping, start dropping.
// if dropping but not at 0, keep dropping
// if dropping and at or beyond 0, impact and start rising
// if rising but not at 100, keep rising
// if rising and at or over 100, stop rising and choose next action (flee)
// actions are TARGET, DROP, RISE, FLEE
if (world.gameState == STATE_RUNNING) {
if (action == ACTION_NONE || (action == ACTION_TARGET && !isTargetValid(world))) {
// cycle begin
determineTarget(world);
}
if (action == ACTION_TARGET) {
if (x != targetX || y != targetY) {
moveTowardsTarget(world);
} else {
action = ACTION_DROP;
}
if (LightRacerWorld.RNG.nextFloat() < .1) {
updateTarget(world);
}
}
if (action == ACTION_DROP) {
if (z > BOTTOM_Z) {
drop(world);
} else {
attack(world);
}
}
if (action == ACTION_RISE) {
if (z < TOP_Z) {
rise(world);
} else {
flee(world);
}
}
if (action == ACTION_FLEE) {
if (x != targetX || y != targetY) {
moveTowardsTarget(world);
} else {
// Completed cycle.
isActive = false;
}
}
}
}

There's a trick in that code. Did you see it? It's the part where the Decimator only updates its target (that is to say, it changes its target xy to match the current xy of the thing it's chasing) 10% of the time. This makes for some intelligent-looking pseudo-random behavior. I'm sure I'll pull that .1 out and make it a constant for tweaking later.

The thing that I got stumbled-up on when coding this was the movement code. Since everything moves direct along the X or Y axis but not both at the same time, I haven't had to do any geometry until now. The decimator flies so I had to take the interpolated distance and use it as a hypotenuse on a right triangle at the angle to the target to figure out how many pixels to move x and y per tick. Since I'm weak on math right now, it was a pain to remember Arc Tangent and Radians. I finally came to after about an hour of reading basic Trig again and got everything working. I'm wondering if there isn't a more efficient way to do this, but my way is to get the angle using the Arc tangent, then change the length of the hypotenuse to be the number of pixels to move this tick and multiply that by sine of theta and cosine of theta to get the y and x movement, respectively. It seems fast enough and works well.

Watch these videos to see how the Decimator will behave. Imagine that the graphics are good. That's what we developers do until they are!