Dependency injection will help, but not solve all the architectural issues. Whenever I embark on a large new project, I first answer this question:
Where does my application start?
It's a simple question with a complex answer. Global variables are a code smell. Instead, create an Application class that ties all the components together into one cohesive unit. Plus, all of your classes should go in a "namespace" to help organize your code and further reduce the clutter in the global context.
Namespaces reduce global context clutter
First, let's give the game a namespace in which all the classes can live:
// Namespace in which all of your game's classes live
var CombatChuck = {};
The "application" class
Next, we need one class that coordinates all the others, which plays the role of the "application":
CombatChuck.Game = function(element) {
this.setElement(element);
this.onAnimate = this.onAnimate.bind(this);
this.titleScreen = new CombatChuck.TitleScreen();
this.tracker = new CombatChuck.FPSTracker();
this.input = new CombatChuck.Input();
this.background = new CombatChuck.Background();
this.shops = new CombatChuck.Shops();
this.chunkHandler = new CombatChuck.ChunkHandler();
this.explosionHandler = new CombatChuck.ExplosionHandler();
this.userInterface = new CombatChuck.UserInterface();
this.world = new CombatChuck.World();
this.player = new CombatChuck.Player();
this.joystick = new CombatChuck.Joystick();
};
CombatChuck.Game.prototype = {
background: null,
chunkHandler: null,
document: null,
dt: 0, // What is "dt"?
element: null,
explosionHandler: null,
input: null,
joystick: null,
oldTime: 0,
paused: false,
player: null,
shops: null,
titleScreen: null,
tracker: null,
userInterface: null,
window: null,
world: null,
constructor: CombatChuck.Game,
draw: function() {
this.background.draw();
this.world.draw();
this.shops.draw();
this.player.draw();
this.chunkHandler.draw();
this.explosionHandler.draw();
this.userInterface.draw();
this.tracker.draw();
},
onAnimate: function(time) {
this.dt = Math.min(0.04, (time - this.oldTime) / 1000);
if(!this.paused){
this.update();
this.draw();
}
this.oldTime = time;
this.window.requestAnimationFrame(this.onAnimate);
},
pause: function() {
this.paused = true;
// ...
},
setElement: function(element) {
this.element = element;
this.document = this.element.ownerDocument;
this.window = this.document.defaultView;
},
start: function() {
// ...
this.window.requestAnimationFrame(this.onAnimate);
},
unpause: function() {
this.paused = false;
// ...
},
update: function() {
// ...
}
};
I'd like to call special attention to the setElement
method. Through this method you get the DOM node in which your whole game lives, as well as the document
and window
object, making your game completely encapsulated. More on that later.
Individual component classes
Next, define all the other classes in the same namespace:
CombatChuck.FPSTracker = function() {
// ...
};
CombatChuck.FPSTracker.prototype = {
constructor: CombatChuck.FPSTracker
};
CombatChuck.TitleScreen = function() {
// ...
};
CombatChuck.TitleScreen.prototype = {
constructor: CombatChuck.TitleScreen
};
CombatChuck.Input = function() {
// ...
};
CombatChuck.Input.prototype = {
constructor: CombatChuck.Input
};
CombatChuck.Background = function() {
// ...
};
CombatChuck.Background.prototype = {
constructor: CombatChuck.Background
};
CombatChuck.Shops = function() {
// ...
};
CombatChuck.Shops.prototype = {
constructor: CombatChuck.Shops
};
CombatChuck.ChunkHandler = function() {
// ...
};
CombatChuck.ChunkHandler.prototype = {
constructor: CombatChuck.ChunkHandler
};
CombatChuck.ExplosionHandler = function() {
// ...
};
CombatChuck.ExplosionHandler.prototype = {
constructor: CombatChuck.ExplosionHandler
};
CombatChuck.UserInterface = function() {
// ...
};
CombatChuck.UserInterface.prototype = {
constructor: CombatChuck.UserInterface
};
CombatChuck.Player = function() {
// ...
};
CombatChuck.Player.prototype = {
constructor: CombatChuck.Player
};
CombatChuck.World = function() {
// ...
};
CombatChuck.World.prototype = {
constructor: CombatChuck.World
};
CombatChuck.Joystick = function() {
// ...
};
CombatChuck.Joystick.prototype = {
constructor: CombatChuck.Joystick
};
Bringing the game to life
Now you've got some architecture to deal with. Your code is organized and you've got one definitive place to start: CombatChuck.Game#start()
. This is where the whole process begins.
Next, a little HMTL and JavaScript to kick things off:
<script type="text/javascript">
var game = new CombatChuck.Game(document.documentElement);
window.onload = function() {
game.start();
};
</script>
<button onclick="game.pause();">Pause</button>
<button onclick="game.unpause();">Unpause</button>
You still have one global variable called game
, but in my opinion this is not a big deal. Your whole game only introduces two globals: game
and CombatChuck
. Much better than the 18 globals your current code introduces (including variables and functions).
Testing your game
Earlier I mentioned that the CombatChuck.Game
class is completely encapsulated because the root element gives you the document
and window
objects. This means your class doesn't have to rely on any globals. Your whole game is set up for unit testing now. I'll use Jasmine as an example:
describe("CombatChuck.Game", function() {
var element, doc, win, game;
beforeEach(function() {
doc = {
// stub out some methods that you need
};
win = {
requestAnimationFrame: function(callback) {}
};
element = document.createElement("div");
element.ownerDocument = doc;
doc.defaultView = win;
game = new CombatChuck.Game(element);
});
it("requests an animation frame", function() {
spyOn(win.requestAnimationFrame);
game.start();
expect(win.requestAnimationFrame).toHaveBeenCalledWith(game.onAnimate);
});
// more tests ...
});
Nitpicks
What is the dt
variable? Don't be afraid to use names that immediately make sense. Basically assume that the next person viewing your code could very well end up being me, who is completely unfamiliar with your game and your code. Does dt
mean "draw time?" If this is true, rename that variable drawTime
.