Hacking Thy Fearful Symmetry

Hacker, hacker coding bright
Powered by a Gamboling Beluga

Perl in Spaaace!

created: December 30, 2010

I like turn-based strategy games. They tend to agree with my... aah, sometimes less than real-time thinking process. I love even more full-fledged campaign turn-based strategy games, where I can let my megalomania run rampant. Games like the X-Com series, Battle for Wesnoth and MegaMek/MekWars.

Now, let's be honest: playing the game is fun, but writing the game is even more of a blast. So I've been toying with the idea of implementing one such a game for a long time, and for its interface been pondering about using a SVG-based web application, as this nicely solves cross-platform compatibilities. But in the past browsers' support of SVG, and mostly SVG animations, was a big disappointment, so I waited. Now, though, it seems that browsers are almost there, and with the little help of JavaScript libraries like Raphaël, should be in a usable state.

In all cases, since I'm vacation and have time to devote to new tech exploration, I've decided to go ahead and prototype around, just to see how far a few days could lead me.

For the game itself, I've decided to aim for a space battle tactical game. Mostly because it's probably the easiest environment to pitch battles in: backdrop is black, sprinkled with stars, and there no hindrace of line of sight or anything of the sort. As inspiration for the game mechanics, I'm looking at the table-top game Full Thrust and its play-by-email adaptation ftjava.

First thing: let there be display!

Of course, there would not be much of a game if we couldn't show a map with all the ships on it. So let's begin with that.

As mentioned above, I've decided to use SVG for the graphical displays and, for now, Raphaël as its API. Using it, let's get ourselves a simple, star-studded map:

<html>
<head> 
<style type="text/css">
div.space_viewport {
    background-color: black;
    background: url(/images/starfield.jpg);
    width: 800px;
    height: 400px;
}
</style>

<script src="/javascripts/raphael-min.js"></script>
<script src="/javascripts/raphael-zpd.js"></script>
<script src="/javascripts/jquery.js"></script>
</head>
<body>

    <div class="space_viewport" id="space_viewport"></div>

<script>
    var paper = Raphael(document.getElementById("space_viewport"), 800, 400);

    var zpf = new RaphaelZPD( paper, { zoom: true, pan: true, drag: false } );

    paper.ZPDPanTo(0,0)

    var ships = new Object(); // will contain the ships present on the map
</script>
</body>
</html>

We can't see because there's nothing on the map, but thanks to Raphael-ZPD we can zoom and pan in our new viewport with the mouse without having to add a line of code. This is very nice.

Space shouldn't be that empty: adding a ship

Now the fun stuff truly begin. We want to add ships (well, to start off, a single ship), and we want to add then dynamically.

To do that, we'll need a web application to feed data to the webpage. So I quickly create a Dancer application. /radar is its first action, which will return a JSON representation of what should be on the map:

ajax  '/radar' => sub {
    return to_json ( {
        ships => [
            {
                id => $ship->id,
                location => {
                    x => $ship->x,
                    y => $ship->y,
                },
                heading => $ship->heading,
            },
        ],
    } );
};

From the webpage side, we'll now summon the power of jQuery to slurp the information from the web app and display the ship when the page load:

<script>
    var traj;

    jQuery.getJSON( '/radar', function(data){

        for each ( s in data.ships ) {
            var ship = paper.image( 
                "/images/ship.gif", 
                s.location.x-100,
                s.location.y-100, 
                200, 200          ).rotate(s.heading);

            ships[s.id] = ship;
            ship.node.id = s.id;

            var trajectory = paper.path( "M0,0L0,0" );
            trajectory.insertBefore(ship);
            trajectory.attr({ stroke: "white" });
            trajectory.node.id = "trajectory-" + s.id;
            traj = trajectory;
        }
    });
</script>

If we try it, we get:

The ship is a little big, but we can zoom out with the scroll-wheel and see that we can also drag the mouse and pan around. Yay!

To boldly go... that way!

Next on the list, I want to give a course to the ship, and I want the resulting trajectory to be outlined on the screen.

For the command itself, I'll use the Full Thrust system, which is text-based. I'll not go into details here; suffice to say that the commands will look like 2P-2 (which, in this case, means "turn 60 degrees port (2 times 30 degrees) and decrease velocity of 2 units).

On the web application side, I add the action /plot_course, which takes that command, computes if it makes senses and spits back any comments from the ship's pilot along the plotted trajectory:

ajax '/plot_course' => sub {
    my @log = $ship->set_course( params->{course} );

    return to_json({
        log => \@log,
        trajectory => $ship->trajectory,
    });
};

We can see a pattern here. The application's controller is as dumb as possible, and let the Ship model take care of all the logistic.

On the webpage, we add the input field, and the required ajax magic:

<input name="course" id="course" />

<input type="submit" 
    value="plot course" 
    onclick="plot_course()" />

<div class="intercom">
    <p><b>intercom</b></p>
    <div class="log"> </div>
</div>

<script>

function plot_course() {
    var course = $('#course').val();
    var data = new Object();
    data["course"] = course;
    jQuery.ajax({
        url: '/plot_course',
        data: data,
        dataType: 'json',
        type: "POST",
        success: function(data) {
        $('.log').html();
        for each ( var line in data.log ) {
            $('.log').append( '<p>' + line + '</p>' );
        }

        var x = traj.attr( "path", data.trajectory );
        traj.show();
    } });
    }
    
</script>

We reload the application and... the ship can now plot its course. The funny semi-arc, by the by, is expected and is the way the Full Thrust system work. Trust me. :-)

"Ensign... engage!"

Our course is plotted. By now, I'm all eager to fire up the ship's main reactors. Of course, I could go the easy route and just show the ship at its new location for the next round, but to see it move around would be much cooler. Well, Raphaël supports animation, so let's try it.

On the server side, we're adding the action /move, which pretty much does the same thing as /set_course, but also acts on it:

ajax '/move' => sub {
    my @log = $ship->set_course( params->{course} );

    push @log, $ship->move;

    return to_json({
        log => \@log,
        id => $ship->id,
        trajectory => $ship->trajectory,
        heading => 180 * $ship->heading / pi,
    });
};

For the webpage-side, we'll use the trajectory we already have as the animation path. Except for the 3 lines(!) pertaining to the animation, the resulting code is almost identical to the one to set the course, hinting that we could refactor the logic to something much more concise.


<input type="submit" 
    value="make it so"
    onclick="make_it_so()" />

<script>
function make_it_so () {
    var course = $('#course').val();
    var data = new Object();
    data["course"] = course;
    jQuery.ajax({
        url: '/move',
        data: data,
        dataType: 'json',
        type: "POST",
        success: function(data) {
        $('.log').html();
        for each ( var line in data.log ) {
            $('.log').append( '<p>' + line + '</p>' );
        }

        var x = traj.attr( "path", data.trajectory );
        ships[data.id].rotate( data.heading, true );
        ships[data.id].animateAlong( traj, 5000, function(){
            traj.hide();
        });
    } });
}
</script>

And now if we click "make it so", we... well, I can see the ship move. If you want to see it move too, the code is on GitHub. The animation is a little jerky, but that might be due to the less than muscular state of my computer. And while that jerkiness doesn't bode quite well for the animation of a full armada, it's still a not-so-bad first step.

Lessons learned

  • SVG animation in the browser is getting better. It's maybe not quite there yet, but at least there's hope.

  • Not only Raphaël and its plugins make SVG manipulations easier, but I've discovered that jQuery and FireBug are also quite happy to do their stuff with SVG as well as HTML. That's two additional mighty tools I was not expecting to have at my disposal.

  • Caveat to the bullet above. Raphaël and cohorts make SVG stuff easier, but it's not yet a walk in the park. There are things like manual zooming and grouping of elements that still require a lot of tuning and homebrew code.

  • For quick prototyping, Dancer is quite the bee's knee.

comments powered by Disqus