Game graphics scaling

In game I would like every player to have more or less the same game experience. And, most importantly, I want players to have the same chance of winning, and not limiting players’ skills based on their devices etc. One of the things that need to be handled is visible game arena size. Ideally players with ultra HD screens and players with smaller laptop screens with 1600 over 900 for example should all see the same part of game arena, to not make players on bigger screens have it easier to spot enemies. This means there needs to be standard game size defined. But that would mean that some players would have only part of their screen space used for game, while others would have to scroll to see whole content – that makes terrible user experience. Scaling should solve the problem!

OK, first things first. I have to define base game arena size to server as reference. On screen with this resolution scaling factor should be equal to one. This could possibly be any set of two numbers (width and height), not corresponding to any particular screen size, but I’ve decided to base it on my full hd screen, or to be more precise, on my canvas size on Raim page – 1600 over 861 (with some space being taken over by address bar, developer tools, icons etc.).

var originalSize = { x: 1600, y: 861 };
var scale = 1;

Then, it is time to scale canvas on page when size of browser changes, so that resizing window will cause scale to change.

var resizeCanvas = function () {
    var arenaElement = document.getElementById(arenaHandler);
    var widthDiff = originalSize.x - arenaElement.offsetWidth;
    var heightDiff = originalSize.y - arenaElement.offsetHeight;

    var aspectRatio = originalSize.x / originalSize.y;
    var w, h;

    if (Math.abs(widthDiff) > Math.abs(heightDiff)) {
        w = arenaElement.offsetWidth;
        h = w / aspectRatio;
    } else {
        h = arenaElement.offsetHeight;
        w = h * aspectRatio;
    }

    canvas.width = w;
    canvas.height = h;

    scale = canvas.width / originalSize.x;
};

(function init() {
    ...

    var arenaElement = document.getElementById(arenaHandler);
    viewport.x = 0;
    viewport.y = arenaElement.offsetHeight;

    canvas = document.createElement("canvas");
    document.getElementById(arenaHandler).appendChild(canvas);
    resizeCanvas();

    window.addEventListener('resize', resizeCanvas);

    gfx = new raimGraphics({
        canvas: function () { return canvas; },
        viewport: function () { return viewport; },
        arena: function () { return arena; },
        scale: function () { return scale; }
    });

    ...
})();

Calculating the scale is not too hard. There is aspect ratio I want to hold (calculated as width divided by height). Given that, if I have new screen width, I can calculate screen height by dividing width by this ascpect ratio. Holding aspect ratio will ensure that graphics don’t get distorted in any axis (e.g. circles do not turn into elipses). The calculation formula is simply taken from proportion:

newWidth / newHeight = originalWidth / originalHeight
newHeight = newWidth / (originalWidth / originalHeight)

With new scale calculated, drawing graphics is as simple as multiplying every coordinate by this scale, for example:

...
x = player.Position.X + args.viewport().x;
y = player.Position.Y + args.viewport().y;
drawingContext.arc(x * scale, -y * scale, player.Size * scale, 0, 2 * Math.PI);

...

var x = points[0].X + args.viewport().x;
var y = -(points[0].Y + args.viewport().y);
drawingContext.moveTo(x * scale, y * scale);
for (var i = 1; i < points.length; i++) {
    x = points[i].X + args.viewport().x;
    y = -(points[i].Y + args.viewport().y);
    drawingContext.lineTo(x * scale, y * scale);
}

x = points[0].X + args.viewport().x;
y = -(points[0].Y + args.viewport().y);
drawingContext.lineTo(x * scale, y * scale);

Easy! Is that it? Well, no. There is also user input to be taken into account – mouse movement and mouse clicks are used in application and game required coordinates to be handled in game world coordinates, not screen coordinates. So what needs to happen is – mouse coordinates have to be scaled accordingly. Does this mean multiplying by game scale?
No. Since I’ve stretched game twice (for player with big screen) and user clicks in coordinate [10, 10] on the screen, it must be [5, 5] coordinate in game world (remember – game world got stretched two times). It makes sense – if I put stuff onto the screen, I multiply it by scale. If I get it from back the screen, I have to devede the value back by reversing the operations.

var inputChange = function (input) {
    var player = getCurrentPlayer();
    if (player == undefined) return;

    input.mouse.x /= scale;
    input.mouse.y /= scale;
    input.mouse.x = input.mouse.x - viewport.x;
    input.mouse.y = -input.mouse.y - viewport.y;
    ...
}

And just one more fix – in my calculations of viewport I was taking canvas size into account. Since canvas account is no longer a real game world size, I cannot take it into account. But thankfully there is game world size already there – originalSize, so that makes viewport calculation really easy:

viewport.x = originalSize.x / 2 - currentPlayer.Position.X;
viewport.y = -originalSize.y / 2 - currentPlayer.Position.Y;

And now game scales on every window sizes!

Overcoming obstacles, or more about collisions, pt 1

I don’t know if shooting game could be that much fun without something to hide behind and catch a breath. I will put some obstacles onto the map. What I imagine them to do is first, block all player movement through the obstacle, block all incoming bullets that hit obstacle, and, in future, block player’s view of the part of arena hidden behind obstacle.

I will start ith easy part first. I need obstacles to be visible on the screen. Invisible walls never really suited me, I’m shy guy after all. First, I extended my Arena class to hold list of obstacles. What is obstacle? Well, basicaly a thingy with list of points that define obstacle shape. It works for now, mybe will have something more in future to prove even more useful. But we like list of points, right?

public class Obstacle
{
    public Vector2d[] Points { get; set; }

    public Obstacle()
    {
        Points = new Vector2d[4]
        {
            new Vector2d(50, 100),
            new Vector2d(100, 75),
            new Vector2d(50, 0),
            new Vector2d(0, 25)
        };
    }
}

Since there are four points in this sample obstacle, you know it is going to be quadrilateral (yay for fancy words!, I’m not native speaker as you probably could’ve guessed ;)). I can tell you even more – it is going to be rectangle at an angle (see what I did here?).

And Arena looks pretty much the same but with a twist;

public class Arena
{
    ...
    public List<Obstacle> Obstacles = new List<Obstacle>() { new Obstacle() };
    ...
}

And this gets returned to the player once he signs into the game.

// RaimHub
public void Register(string name)
{
    name = HttpUtility.HtmlEncode(name);

    var player = arena.RegisterPlayer(name);
    players.Add(Context.ConnectionId, player);

    Clients.Caller.SignedIn(player.Id);
    Clients.Caller.SetupArena(arena);
    Clients.All.Registered(player);
    Clients.Caller.OtherPlayers(players.Values.Where(p => p.Name != name));
}

On a client this arena object is available for graphics “engine” to a) draw arena border, and b) draw all the obstacles. See the code, it is pretty simple:

var drawArena = function (gameObjects) {
    drawingContext.clearRect(0, 0, canvas.width, canvas.height);

    drawArenaBorders();
    drawObstacles();

    ...
};

function drawArenaBorders() {
    if (args.arena() == undefined) return;

    drawRectangle([
        { X: 0, Y: 0 },
        { X: 0, Y: args.arena().ArenaSize.Y },
        { X: args.arena().ArenaSize.X, Y: args.arena().ArenaSize.Y },
        { X: args.arena().ArenaSize.X, Y: 0 }]);
}

function drawObstacles() {
    if (args.arena() == undefined) return;

    var obstacles = args.arena().Obstacles;
    for (var i = 0; i < obstacles.length; i++) {
        drawRectangle(obstacles[i].Points);
    }
}

function drawRectangle(points) {
    if (points.length < 2) return;

    drawingContext.beginPath();

    drawingContext.moveTo(points[0].X + args.viewport().x, -(points[0].Y + args.viewport().y));
    for (var i = 1; i < points.length; i++) {
        drawingContext.lineTo(points[i].X + args.viewport().x, -(points[i].Y + args.viewport().y));
    }

    drawingContext.lineTo(points[0].X + args.viewport().x, -(points[0].Y + args.viewport().y));

    drawingContext.strokeStyle = "rgba(0, 0, 0, 1)";
    drawingContext.stroke();
    drawingContext.closePath();
}

The same function draws borders and obstacles. All are rectangles, just one limits you from escaping and the other blocks you from going in. Well, sort-of. It will in future. I promise.

We are counting scores here

One useful info in any game that promotes rivalry of any kind is score each player gets. How else do you decide who is actualy better? In Raim, stuff is pretty simple – you get one point for any other player you hit with your bullet. Clear rules are the best.

Actual implementation? Well, that’s a simple one this time:

private void HandleCollision(Player o1, Bullet o2)
{
    o1.IsDestroyed = true;
    o2.IsDestroyed = true;
    o2.KilledPlayer();
}
public void KilledPlayer()
{
    Player.KilledEnemy();
}
public void KilledEnemy()
{
    Score++;
}

Bullet knows what to do in case it hit the other player. This time it only notifies player it belongs to that something like that happened so player can update internal score. In future maybe this will do something more.

Score is property of player so it will notify all players when the next update from server will be sent to clients. And what clients should do is to update leaderboard.

var playerMoved = function (gameObjectsFromServer) {
    gameObjects = gameObjectsFromServer;
    players.updateLeaderboard(gameObjects);
};
var updateLeaderboard = function (gameObjects) {
    for (var i = 0; i < _players.length; i++) {
        _players[i] = gameObjects.find(function (g) { return g.Id == _players[i].Id; });
        var playersList = document.getElementById(playersListElementId);
        var playerListElement = playersList.getElementsByTagName("span")[i];
        playerListElement.textContent = _players[i].Name + " " + _players[i].Score;
        playerListElement.id = _players[i].Id;
    }

    for (var i = _player.length; i < playerListElements.length; i++) {
        playersList.removeChild(playerListElements[i]);
    }
};

That is one ugly piece of code, but it will do for now :)

You’re the center of my universe, or few words about viewport

The arena is big. Bigger than the screen. Well, at this point arena is infinitely big, but that’s gonna change at some point. It will be big though. You wanna know what is the problem with big arena? Player can get lost. More. Player can get out of screen. Try to aim when you can’t see your rifle! This is about to change.

Fortunately I already have viewport in place. Until now it served one purpose only – to flip around the Y axis and move the screen down so that player is visible, up is up and down is down. But in my mind I had already an idea of how it was supposed to work with scrolling visible game area so that player can see his or her representation at all times. The target is simple: I want player to always be in the center of the screen. It kind-of gives the impression that player does not move but rather all the world around him or her is being moved. And that’s fine – this way player is never lost, it is easy to rotate the player, mouse will always be rotated around center of the screen. The implementation is simple as well.

var processFrame = function (timestamp) {
    ...

    var currentPlayer = getCurrentPlayer();
    if (currentPlayer !== undefined) {
        viewport.x = canvas.width / 2 - currentPlayer.Position.X;
        viewport.y = -canvas.height / 2 - currentPlayer.Position.Y;
    }

    ...
}

Before each frame is being processed, I calculate new viewport size. In case of X axis it is straightforward – since I want player to be in the middle of the screen, I calculate how far of the screen center the player is (screen center being half the width of canvas; if I subtract player position from it I will get how many units I need to move my screen to get player centered).

For Y axis things are tiny bit different. Remember that Y axis is flipped (since Y in geometry goes up the higher we get, but on screen Y gets larger when we go down the screen), so I need to flip canvas height as well to get the correct number of units to correct my screen position.

Now it is time for graphics rendering:

function drawBullet(bullet) {
    drawingContext.beginPath();

    drawingContext.fillStyle = "rgba(0, 0, 0, 1)";
    x = bullet.Position.X + args.viewport().x;
    y = bullet.Position.Y + args.viewport().y;

    drawingContext.arc(x, -y, bullet.Size, 0, 2 * Math.PI);
    drawingContext.fill();
    drawingContext.closePath();
}

This is bullet drawing, but the same actions were completed in player rendering, skipped here for brevity. Since I have viewport which tells how far of the screen center player is, I need to add this value to all positions on the screen to translate those objects into correct position regarding to viewport. This is simple – just add viewport to position vector. Notice that y variable is still being flipped when being drawn – viewport does not change that, it only gets transition calculated in flipped units, but then the actual drawn point needs to still be flipped.

Is that all? No – there is one more place where viewport is being used. User input handling – where I calculate player mouse position in game arena coordinates:

function mouseMove(e) {
    var targetRect = document.getElementById("arena").children[0].getBoundingClientRect();
    mouseCoordinates = { x: e.clientX - targetRect.left, y: e.clientY - targetRect.top };
    mouseCoordinates.x = mouseCoordinates.x - args.viewport().x;
    mouseCoordinates.y = -mouseCoordinates.y - args.viewport().y;

    notifyKeysChanged();
}

Two things here. First – y axis mouse coordinates stay flipped, like when drawing. And second – here I do not add viewport but subtract it. This is simple – Since I’ve added it to move the world to screen coordinates, I need to reverse the operation to move screen coordinates into world coordinates.

And with those few simple changes player is now in the center of game and everything moves relative to him or her. And every player is certain he is the center of the universe. This proves Einstein’s theory that everything is relative nicely ;)

No, mr Bullet, I expect you to die

There are some objects that I want to live for some time and then disappear. Bullets would be one of them. They serve a purpose for a time, but once they (a) hit another object, or (b) reach their lifespan, they should disappear. My first idea was to store creation time, get actual time and check whether the lifespan of bullet is reached. What sounded like a good idea at first, and actually worked on server side, is quite hard to do on client side. Well, to do it is easy. The hard part is synchronizing client and server time. And this would be priority – getting time wrong would mean client side would render bullet for too long or too short. Either way it would be terrible experience.

And then, while cleaning my teeth, it hit me. I was looking at this completely wrong. I’m not interested in how much time has passed since object creation. What I want to know is how long object has to live. Basically Time To Live, similar to what you might know from pinging or other web protocols. And implementation is even simpler.

public void Update(DateTime updateTime)
{
    if (TimeToLive <= 0)
    {
        IsDestroyed = true;
        return;
    }

    var changeTime = updateTime;
    var timeBetweenEvents = changeTime - lastUpdate;

    Position.X += Speed.X * timeBetweenEvents.TotalSeconds;
    Position.Y += Speed.Y * timeBetweenEvents.TotalSeconds;

    TimeToLive -= (int)timeBetweenEvents.TotalMilliseconds;
    lastUpdate = changeTime;
}

And on client side as well:

var processFrame = function (timestamp) {
    if (!lastFrameTime)
        lastFrameTime = timestamp;

    var timeDiff = (timestamp - lastFrameTime) / 1000;

    for (var i = 0; i < gameObjects.length; i++) {
        var gameObject = gameObjects[i];

        if (!gameObject.IsDestroyed) {
            var directionPoint = { x: gameObject.Position.X + gameObject.FacingDirection.X, y: gameObject.Position.Y + gameObject.FacingDirection.Y };
            gameObject.Position.X += gameObject.Speed.X * timeDiff;
            gameObject.Position.Y += gameObject.Speed.Y * timeDiff;
            gameObject.FacingDirection = calculateFacingDirection(gameObject, directionPoint);

            if (!!gameObject.TimeToLive) {
                gameObject.TimeToLive -= timestamp - lastFrameTime;
                if (gameObject.TimeToLive <= 0)
                    gameObject.IsDestroyed = true;
            }
        }
    }

    gameObjects = gameObjects.filter(function (g) { return !g.IsDestroyed });

    gfx.drawArena(gameObjects);

    lastFrameTime = timestamp;
    requestAnimationFrame(processFrame);
};

Once again it has been proven that oral hygiene is goon not just for your teeth but your whole body!

Fire at will, pt 2

OK, time to do some changes. I would really hate it if I had to write different loops for every possible object type. One loop to rule them all. One loop to draw them.

I’ve already extracted IGameObject interface with some common stuff I (at this point) think all objects will use. That is:

public interface IGameObject
{
    Guid Id { get; }
    Vector2d Position { get; }
    Vector2d Speed { get; }
    Vector2d FacingDirection { get; }

    void Update(DateTime updateTime);
}

Then I changed how hub manages objects:

public class RaimHub : Hub
{
    private static Dictionary<string, Player> players = new Dictionary<string, Player>();
    private static List<IGameObject> gameObjects = new List<IGameObject>();

    public void Register(string name)
    {
        name = HttpUtility.HtmlEncode(name);
        var player = Player.Create(name, 250, 250);
        players.Add(Context.ConnectionId, player);
        gameObjects.Add(player);
        Clients.All.Registered(player);
        Clients.Caller.OtherPlayers(players.Values.Where(p => p.Name != name));

        UpdateGameState();
        Clients.All.PlayerMoved(gameObjects);
    }

    public void SignOff()
    {
        ...

        UpdateGameState();
        Clients.All.PlayerMoved(gameObjects);
    }

    public override Task OnDisconnected(bool stopCalled)
    {
        SignOff();
        return base.OnDisconnected(stopCalled);
    }

    public void PlayerMoving(PlayerInput input)
    {
        UpdateGameState();
        var player = players[Context.ConnectionId];
        var createdObjects = player.ProcessInput(input);
        gameObjects.AddRange(createdObjects);
        Clients.All.PlayerMoved(gameObjects);
    }

    private void UpdateGameState()
    {
        var updateTime = DateTime.Now;

        foreach (var player in gameObjects)
            player.Update(updateTime);
    }
}

So now any action that comes to server causes objects to be updated and sent back to all players to synchronise their status. This piece of code I’m positive will change, but servers its purpose for now.

It also requires some changes on client side. First at arena.js

var playerMoved = function (gameObjectsFromServer) {
    gameObjects = gameObjectsFromServer;
};

    var processFrame = function (timestamp) {
    if (!lastFrameTime)
        lastFrameTime = timestamp;

    var timeDiff = (timestamp - lastFrameTime) / 1000;

    for (var i = 0; i < gameObjects.length; i++) {
        var player = gameObjects[i];

        var directionPoint = { x: player.Position.X + player.FacingDirection.X, y: player.Position.Y + player.FacingDirection.Y };
        player.Position.X += player.Speed.X * timeDiff;
        player.Position.Y += player.Speed.Y * timeDiff;
        player.FacingDirection = calculateFacingDirection(player, directionPoint);
    }

    gfx.drawArena(gameObjects);

    lastFrameTime = timestamp;
    requestAnimationFrame(processFrame);
};

And then at raimGraphics.js

var drawArena = function (gameObjects) {
    drawingContext.clearRect(0, 0, canvas.width, canvas.height);

    for (var i = 0; i < gameObjects.length; i++) {
        var gameObject = gameObjects[i];
        if (gameObject.Name === undefined) {
            drawBullet(gameObject);
        } else {
            drawPlayer(gameObject);
        }
    }
};

Don’t shoot for this ugly if. Right now Name is only available for players, not for other objects so it can be used to differentiate between object types. But I will implement object types for sure!

There, now it makes things bit easier to handle on client and server.

Lets go back to handling user input. Last implementation of mouse click was working only when mouse was moving – that’s the only time mousemove is triggered of course. That’s not acceptable for shooting though. New, upgraded code looks like this:

function mouseDown(e) {
    if ((e.buttons && 1) && (keys.indexOf(1) === -1)) {
        keys.push(1);
    }
    notifyKeysChanged();
}

function mouseUp(e) {
    if (keys.indexOf(1) >= 0) {
        keys.splice(keys.indexOf(1), 1);
    }
    notifyKeysChanged();
}

(function () {
    document.addEventListener("keydown", keyDown);
    document.addEventListener("keyup", keyUp);
    document.addEventListener("mousemove", mouseMove);
    document.addEventListener("mousedown", mouseDown);
    document.addEventListener("mouseup", mouseUp);
})();

So mouse clicks are handled separately to all other moving logic, giving quick response and appropriate behavior.

Am I done with mouse input for now? Sure not. User can trigger so many shots one after another that he or she can fill up the screen with black dots. This is problem for game as well as for performance. I’ll need to handle that next.

Fire at will, pt. 1

With players moving and all, it is time to introduce some action! I hope to see some pixels flying all over the place, and see all those players respawning like crazy. But to get that, there needs to be a way to fire some projectiles, right?

function mouseMove(e) {
    var targetRect = document.getElementById("arena").children[0].getBoundingClientRect();
    mouseCoordinates = { x: e.clientX - targetRect.left, y: e.clientY - targetRect.top };
    mouseCoordinates.x = mouseCoordinates.x - args.viewport.x;
    mouseCoordinates.y = -(mouseCoordinates.y - args.viewport.y);
    console.log(e.buttons);
    if (e.buttons && 1) {
        keys.push(1);
    }
    else if (keys.indexOf(1) >= 0) {
        keys.splice(keys.indexOf(1), 1);
    }

    args.inputChanged({ direction: buildDirectionKey(), mouse: mouseCoordinates });
}

What has been added is handling mouse button pressed events. Well, to be precise – I get the info about mouse buttons state with mouse move event, which is handy. I use this to push the value 1 to list of keys pressed. This is safe since no keyboard keys can cause 1 to be added – this is not printable character. Simple, consistent. I’ve also added LeftMouse: 16 into my enum-like object holding keys map. This all get send to server. And what it does is:

in RaimHub.cs

public void PlayerMoving(PlayerInput input)
{
    UpdatePlayers();
    var player = players[Context.ConnectionId];
    var createdObjects = player.ProcessInput(input);
    gameObjects.AddRange(createdObjects);
    Clients.All.PlayerMoved(player, createdObjects);
}

in Player.cs

public IEnumerable<IGameObject> ProcessInput(PlayerInput input)
{
    ProcessDirection(input.KeysInput);
    FacingDirection = input.FacingDirection;

    if (input.KeysInput.HasFlag(KeysInput.MouseLeft))
    {
        var bullet = Bullet.Create(Position.X, Position.Y, FacingDirection);
        return new[] { bullet };
    }

    return Enumerable.Empty<IGameObject>();
}

And last, but not least, Bullet.cs

public class Bullet : IGameObject
{
    private DateTime lastUpdate = DateTime.Now;

    public Guid Id { get; private set; }
    public Vector2d Position { get; set; }
    public Vector2d Speed { get; set; }
    public Vector2d FacingDirection { get; set; }

    private Bullet() { }

    public static Bullet Create(double x, double y, Vector2d direction)
    {
        return new Bullet()
        {
            Id = Guid.NewGuid(),
            Position = new Vector2d(x, y),
            Speed = direction.Unit().Scale(10),
        };
    }

    public void Update(DateTime updateTime)
    {
        var changeTime = updateTime;
        var timeBetweenEvents = changeTime - lastUpdate;

        Position.X += Speed.X * timeBetweenEvents.TotalSeconds;
        Position.Y += Speed.Y * timeBetweenEvents.TotalSeconds;

        lastUpdate = changeTime;
    }
}

And also drawing on client side, we can’t forget drawing:

var drawArena = function () {
    drawingContext.clearRect(0, 0, canvas.width, canvas.height);

    for (var i = 0; i < players.count(); i++) {
        var gameObject = players.get(i);
        drawPlayer(gameObject);
    }

    for (var i = 0; i < gameObjects.length; i++) {
        var gameObject = gameObjects[i];
        drawingContext.beginPath();

        drawingContext.fillStyle = "rgba(0, 0, 0, 1)";
        x = gameObject.Position.X - viewport.x;
        y = gameObject.Position.Y - viewport.y;

        drawingContext.arc(x, -y, 2, 0, 2 * Math.PI);
        drawingContext.fill();
        drawingContext.closePath();
    }
};

Easy, right? I’m not entirely sure about my design decissions yet. Well, I’m pretty sure that this is not all OK and it will soon change, but for the moment it gets things done.

But what is so bad about it? Well, to start – there are separate collections of players and game objects, handled separately in drawing. And nobody likes special cases, right? I imagine this should be all handled the same way – just a list of objects that take action in game, possibly with each object knowing how to paint itself (or, more reasonably, specialized classes responsible for drawing all different projects).

Second thing is – my bullets don’t move. This is related to first thing. With separate collection of game objects, I did not write code that triggers bullets update.

Third – mouse clicks are only detected when mouse is moving. Take that campers! But no, for real that is an issue that needs to be handled.

In all honesty I’ve made a piece of code that makes player drop black dots when moving mouse. That’s not gonna sell. But I’m not finished yet.