Tuesday, January 5, 2010

Light Racer 2.0 - Days 1 and 2 - Refactoring

The first two days of actual coding have involved mostly refactoring. If you're curious as to why I'm doing all of this refactoring, I will recap on the new requirements which involve multiple kinds of AI, a new game object that I call the "decimator" and real-time multiplayer functionality. Many would think that the only way to do that would be to rewrite the game to support this, so why am I opting for refactoring to start? Some would argue that it's semantics but for the most part, refactoring is taking existing code without changing functionality and rewriting is completely scrapping a functional piece of code and developing it from scratch. Lots of the code in Light Racer was really good but just not organized to allow for new features to be added in without too much headache.

What changed?

I first started by creating two new activites: One for the game and one for the instructions. Before it was just one activity that had a hacked up layout. That hacked layout had all 3 different layouts all layered on top of each other and the activity would switch which ones were visible depending on the state of the whole game. This was very hard to manage and work on and certainly wouldn't have worked out well to add in the new multiplayer menus. Now there are three distinct Activities. One for the main menu, one for the instructions and one for the game itself. In the future there will be more to handle more of the new functionality.

My next goal was to break out the existing AI code so that I could make it more modular. What I wanted to do was be able to just pop a new instance of a MediumAI and be able to set that on any player. Here's the idea:

public void setupGame() {
Player p1 = new Player();
p1.isHuman = true;
Player p2 = new Player();
p2.setAI(new EasyAI());
Player p3 = new Player();
p3.setAI(new MediumAI());
startGame();
}

Sounds easy enough, right? Well that interface is in place now so I can have code that works almost exactly like that, but the hard part was working it out so that the AI code could work correctly on its own, which means working without access to all of the fields of the game. This was achieved by doing two things. First, I created a "World" class that contains everything that matters to the game. I moved all of the Players and state info into the World. Then I took the old updateAI() method which used to exist in the big monolithic game class and moved it into the AI interface, which all AI implementations implement, and added it as a parameter, so that every time the AI is updated, it has access to the entire world. To translate - the AI code is handed the positions of every player and game object as well as the paths of light, just like you see on the screen. This is necessary because those things are all used in the calculations for the AI to determine where to move its player to next. The most used method in that calculation is collision detection, which is some code that simply checks to see if the racer is going to hit a wall. That collision detection code is needed by AI and by the update() method in the Player class so I took a standard OO approach and created a base class called PlayerPhysics and made EasyAI and Player both extend it, thus allowing for them to share that common collision detection code.

Does this sound like a big jumble? It's actually really clean now. Here's how it looks:

public interface AI {
public void update(LightRacerWorld world);
}

public abstract class PlayerPhysics {
public boolean checkCollision(LightRacerWorld world, int fromX, int fromY, int toX, int toY) {
... collision detection code
}
}

public class EasyAI implements AI extends PlayerPhysics {
public void update(LightRacerWorld world) {
... all the old AI code is in here now
}
}

public class Player extends PlayerPhysics {
... tons of fields for position, next move, animation, color, etc
public void setAI(AI ai) {
this.ai = ai;
}

public void updateAI(LightRacerWorld world) {
ai.update(world);
}
}

Since a good, safe refactoring job moves things in small steps and doesn't break anything, this should be considered a few steps taken but not a finished job. The design in the architecture document uses Receivers, which we don't have implemented yet. For now, this is an improvement and takes steps to get there.

The other thing that was done is that resources were moved into their own class, also following the architecture document. This was necessary to allow for GameObjects to be able to draw themselves, which was an ability they did not previously possess. Did I mention I added GameObjects?

So far so good on the development. Here are screenshots of where the game is at now. Much of this work won't have any visible artifact but soon we'll get there. In fact, all that has changed aesthetically is that the game is now full screen and there are placeholders up on top for the game difficulty and level.


Main Menu Dev Shot
Main Menu Dev Shot

Light Racer 2.0 Dev Shot Game 4-7-2009
Light Racer 2.0 Dev Shot Game 4-7-2009

No comments:

Post a Comment