6
\$\begingroup\$

After trying a couple different HTML5 frameworks, I've always found pure JS the most appealing. However, I am uncertain about the way I'm structuring it.

var titlescreen = new Titlescreen();
var fps = new FPSTracker();
var input = new Input();
var background = new Background();      
var shops = new Shops();
var chunkhandler = new ChunkHandler();
var explosionhandler = new ExplosionHandler();
var ui = new UI();
var world = new World();
var player = new Player(); 
var joystick = new Joystick();

var old_time = 0;
var dt = 0;

load();

function init(){    // called once resources loaded
    // ...
    requestAnimationFrame(loop);
}

function update(){
    player.update();
}

function draw(){
    background.draw();
    world.draw();
    shops.draw();
    player.draw();
    chunkhandler.draw();
    explosionhandler.draw();
    ui.draw();
    fps.draw();
}

function loop(time){
    dt = Math.min(0.04, (time - old_time) / 1000);

    if(!game_paused){
        update();
        draw();
    }

    old_time = time;
    requestAnimationFrame(loop);
}

Some objects listed here must communicate information between each other. For example, the Player class accesses some of world's functions by simply stating world.functionINeedToUse();, using the global variable's name. Coming from a Java background, this gives me a very bad vibe as it depends on the declaration of this global variable with the specific name. Though I've found JS to be a more forgiving language, this still does not feel right.

\$\endgroup\$

2 Answers 2

4
\$\begingroup\$

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.

\$\endgroup\$
1
  • \$\begingroup\$ Very useful information, looks very clean. I will definitely apply this! Thank you. dt stands for delta-time. I think it's a pretty known term when creating games, and considering I have to use it a decent amount when calculating frame changes, I like to keep verbosity a bit lower. \$\endgroup\$
    – John
    Commented Oct 20, 2014 at 0:49
4
\$\begingroup\$

In order to prevent objects from depending on global variables (and in JavaScript almost every identifier is actually a variable), you can use the principle of dependency injection.

When an object needs to interact with another object, it should receive that object, either when it is initialized, through a setter or as a method parameter.

When your player requires the world, you could pass it when you create it and store it in a variable in local scope:

var player = new Player(world);

However, when your game has multiple worlds and the player moves from one to the other, it would be good to be able to change the world. In that case you could have a setter:

var player = new Player();
player.setWorld(world);

When the world is only required for a single method, you can also decide to pass the object to that method as a parameter:

player.checkCollisions(world);
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.