2
\$\begingroup\$

I am writing a JavaScript game that provides a top down view of the player, to eventually create a ray caster that can render the view in faux 3D (the top down view will end up as the mini-map).

A while ago, I asked the question Should actors in a game be responsible for drawing themselves?, to which I received the suggestion of having my game objects have a draw descriptor which provides the renderer with the information required to draw it.

I have a world object and a renderer object. I am trying to do this the right way, so I gave my world object a getDrawDescription() method which returns information required to render itself.

However, it's not quite as simple as a game actor, which may return an x and y offset and an image resource. My world's data is a matrix of information about cells, each with a number that relates to a different cell/texture. It may be best to show the implementation of my two objects (and some code to work with them)...

var world = {
    cells: [
        [/* ... */]
    ],

    getDimensions: function() {
        return { width: this.cells[0].length, height: this.cells.length };
    },       

    getDrawDescription: function() {
        return {
            type: 'cells',
            cellSize: 16,
            data: this.cells
        };
    }
};

var renderer = {
    objects: [],
    width: 0,
    height: 0,
    ctx: null,
    canvas: null,
    addObj: function(obj) {
       this.objects.push(obj);
    },
    setDimensions: function(width, height) {
        this.width = width;
        this.height = height;
    },
    setCanvas: function(canvas) {
        this.canvas = canvas;
        canvas.width = this.width;
        canvas.height = this.height;
        this.ctx = canvas.getContext("2d");
    },
    draw: function() {
        var ctx = this.ctx;
        this.objects.forEach(function(obj) {

            var drawingDescription = obj.getDrawDescription();

            switch(drawingDescription.type) {
                case 'cells':
                    var cellSize = drawingDescription.cellSize;

                    drawingDescription.data.forEach(function(row, y) {

                        row.forEach(function(cell, x) {

                            // Obviously, this is a major violation of having
                            // the object provide all the drawing info.
                            var fillStyles = {
                                0: "#ccc",
                                1: "#000",
                                2: "#776",
                                3: "#fr2",
                                4: "#f0f"
                            };

                            ctx.fillStyle = fillStyles[cell];

                            ctx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize); 

                        });

                    });

                    break;
            }
        });
    }
};

var game = {
    loop: function() {

        renderer.draw();

        setTimeout(this.loop.bind(this), 500);
    },

    init: function() {

        renderer.addObj(world);
        this.loop();

    }
};

document.addEventListener("DOMContentLoaded", function() {
    var dimensions = world.getDimensions();
    var description = world.getDrawDescription();

    renderer.setDimensions(dimensions.width * description.cellSize, dimensions.height * description.cellSize);
    renderer.setCanvas(document.getElementsByTagName("canvas")[0]);

    game.init();
});

As you can see, I wasn't sure how to provide the render with the information required to render the world. I could have a function such as getColorById(), but it would require the renderer to know about the world's implementation (possibly OK?) and would require a lot of function calls (maybe a function with returns an object which provides the mappings would be OK).

So, how is this typically achieved in game development? It may be a case of YAGNI, but I figure if it provides a good separation of concerns, it's a win (I'm unlikely to switch the renderer from canvas, though I could prove its usefulness by having it write elements with a background-color, for example).

I'm still fairly new to game development, so help me out if I'm doing anything wrong on a more fundamental level.

\$\endgroup\$

2 Answers 2

2
\$\begingroup\$

It just so happens that I'm working on something roguelike-esque in JavaScript, and it looks like we're approaching the problem in similar ways. Here's what I'm doing:

  • GameMap. This object knows what the entire map looks like. It's basically an array of arrays of tiles. Each cell in the array is a number, with -1 being a sentinel for "no room here". Those numbers might graduate to objects, but I think I won't need too many of them -- there are only so many possible types of tiles, never more than about 50 in a map.
  • TileMap. This object knows about the viewport (which tiles are visible in the canvas at this moment based on the player's position in the world) and is responsible for making the actual draw calls. It knows how to draw a tile based on the index in the GameMap (it's the View to the GameMap's Model). It is also responsible for transforming click coordinates on the canvas into worldspace coordinates (canvas position [250, 300] might actually be world position [11, 8] depending on where the viewport is as the player walks around).
  • Canvas. A wrapper around canvas code that knows a little bit about my drawing optimizations. Don't know that you'll need it, but it's involved in this little dance so I thought I'd include it in my explanation.

Here's how they're connected: the GameMap is synced from the server. The TileMap has an instance of GameMap and knows when the tiles are update (thanks, KnockoutJS!). It knows that it has to draw itself. The TileMap uses its instance of the Canvas object to draw the map. And voila: a drawn world.

I think you're basically on the right track, but I might make some suggestions:

  • See that switch statement in your renderer? That's a sign that you might be missing some objects. Right now you might have "sprites" and "cells", but what about random particle effects, or GUI elements? Do each of those get a case statement? That'll make the switch statement kind of big. What if you separated out "the thing that knows how to draw my world" from the renderer? Then the renderer could call that thing. Even better, the draw descriptor could return the object that knows how to render it to the renderer. Make sense?
  • Don't use setTimeout for your render loop, use requestAnimationFrame. That way, the player can navigate away from the window of your game and the browser will know to slow the animations down.
\$\endgroup\$
1
\$\begingroup\$

Normally the world renderer will get the list of tiles from the world. Then, for each tile, you can get the method or details for rendering that tile. That looks pretty close to what you have.

Renderers are expected to know about the implementation of what they're rendering. It's the other way around, where the logic of the game knows about the renderer, that is usually a bad idea.

(As for "how is this typically achieved in game development", there is no typical method.)

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .