Building Games with jQuery: Advanced Techniques for Perspective, Levels, Multiplayer, and Social Integration

Chapter 5: Perspective Rendering

Top-down perspective (also called bird's-eye view) is one of the most popular rendering techniques for browser-based games. This approach enables a wide variety of game genres:

  • Battle royale-style action games
  • Shooter games featuring alien creatures
  • RPGs inspired by classic titles like The Legend of Zelda
  • City-building simulations
  • Strategy games similar to Civilization or Warcraft

These games utilize orthographic projection, which can be efficiently rendered using tile maps. In this chapter, we'll create a RPG-style game reminiscent of the Super Nintendo era.

Tilemap Optimization for Top-Down Games

The tilemap implementation from the previous chapter works excellently for side-scrolling games. These games typically use sparse matrices to define levels—a 100-tile by 7-tile level might contain far fewer than 700 actual tiles, allowing us to generate all tiles at game startup.

Top-down games present a different challenge. To render the map effectively, we must define every possible tile in the tilemap. For a similar-sized level, we'd have at least 700 tiles. With multiple layers, the situation worsens dramatically.

To optimize performance, we employ a visibility-based approach: generate only tiles visible at startup, then track and update as the view moves. When tiles scroll out of view, we remove them; when new tiles scroll into view, we create them.

This approach involves trade-offs. Adding and removing tiles consumes processing time and can potentially slow the game. Conversely, maintaining a large number of tiles in the scene increases rendering overhead.

The optimal strategy requires testing both approaches on your target platform. For games where players move at moderate speeds through reasonably-sized worlds, visible-tile-only generation performs well.

Identifying Visible Tiles

We already have much of the infrastructure needed to identify visible tiles. Our collision-detection function returns tiles intersecting a given bounding box. To find the visible area, we simply define this box as the game screen.

// Calculate visible viewport
var offset = gf.offset(parent);
var visible = gf.tilemapBox(options, {
    x: -options.x - offset.x,
    y: -options.y - offset.y,
    width: gf.baseDiv.width(),
    height: gf.baseDiv.height()
});

The offset function accounts for the tilemap potentially being nested within one or more moved groups. It traverses the element hierarchy, accumulating position offsets until reaching the base game container.

gf.offset = function(div) {
    var options = div.data("gf");
    var x = options.x;
    var y = options.y;

    var parent = $(div.parent());
    options = parent.data("gf");

    while (!parent.is(gf.baseDiv) && options !== undefined) {
        x += options.x;
        y += options.y;
        parent = $(parent.parent());
        options = parent.data("gf");
    }
    return { x: x, y: y };
};

The addTilemap function requires modification to store the visible region and render only those tiles:

gf.addTilemap = function(parent, divId, options) {
    var options = $.extend({
        x: 0,
        // ... other defaults
    }, options);


    var offset = gf.offset(parent);
    var visible = gf.tilemapBox(options, {
        x: -options.x - offset.x,
        y: -options.y - offset.y,
        width: gf.baseDiv.width(),
        height: gf.baseDiv.height()
    });
    options.visible = visible;

    var tilemap = gf.tilemapFragment.clone()
        .attr("id", divId)
        .data("gf", options);


    for (var i = visible.y1; i < visible.y2; i++) {
        for (var j = visible.x1; j < visible.x2; j++) {
            var animationIndex = options.map[i][j];
            // Create tile logic...
        }
    }
    parent.append(tilemap);
    return tilemap;
};

Dynamically Updating Tilemaps

When the tilemap or any parent element moves, we must update the visible region. We modify the movement functions to trigger visibility updates.

gf.x = function(div, position) {
    if (position !== undefined) {
        div.css("left", position);
        div.data("gf").x = position;

        if (div.find(".gf_tilemap").size() > 0) {
            div.find(".gf_tilemap").each(function() {
                gf.updateVisibility($(this));
            });
        }
        if (div.hasClass("gf_tilemap")) {
            gf.updateVisibility($(div));
        }
    } else {
        return div.data("gf").x;
    }
};

The updateVisibility function compares the new visible region with the previous one and adjusts tiles accordingly:

gf.updateVisibility = function(div) {
    var options = div.data("gf");
    var oldVisibility = options.visible;
    var parent = div.parent();

    var offset = gf.offset(div);


    var newVisibility = gf.tilemapBox(options, {
        x: -offset.x,
        y: -offset.y,
        width: gf.baseDiv.width(),
        height: gf.baseDiv.height()
    });

    if (oldVisibility.x1 !== newVisibility.x1 ||
        oldVisibility.x2 !== newVisibility.x2 ||
        oldVisibility.y1 !== newVisibility.y1 ||
        oldVisibility.y2 !== newVisibility.y2) {

        div.detach();

        // Remove tiles no longer visible (four directional loops)
        for (var i = oldVisibility.y1; i < newVisibility.y1; i++) {
            for (var j = oldVisibility.x1; j < oldVisibility.x2; j++) {
                div.find(".gf_line_" + i + ".gf_column_" + j).remove();
            }
        }
        // ... similar loops for other directions

        // Add newly visible tiles
        for (var i = oldVisibility.y2; i < newVisibility.y2; i++) {
            for (var j = oldVisibility.x1; j < newVisibility.x2; j++) {
                createTile(div, i, j, options);
            }
        }
        // ... similar loops for remaining directions

        div.appendTo(parent);
    }
    options.visible = newVisibility;
};

The tile creation function checks for existing tiles to prevent duplication:

var createTile = function(div, row, col, options) {
    var animationIndex = options.map[row][col];
    if (animationIndex > 0 && div.find(".gf_line_" + row + ".gf_column_" + col).size() === 0) {
        var tileOptions = {
            x: options.x + col * options.tileWidth,
            y: options.y + row * options.tileHeight,
            width: options.tileWidth,
            height: options.tileHeight
        };
        var tile = gf.spriteFragment.clone().css({
            left: tileOptions.x,
            top: tileOptions.y,
            width: tileOptions.width,
            height: tileOptions.height
        }).addClass("gf_line_" + row)
          .addClass("gf_column_" + col)
          .data("gf", tileOptions);

        gf.setAnimation(tile, options.animations[animationIndex - 1]);
        div.append(tile);
    }
};

Occlusion and Depth Management

Layer-Based Occlusion

With top-down view, two scenarios emerge: the camera looks directly downward or at a slight angle.

For direct overhead views, elements only occlude others directly above them. This is straightforward—we use separate groups for each depth level and place sprites in the appropriate group.

Consider a level with trees and bridges where players walk underneath:

// Organization by depth layers
var groundLayer = gf.addGroup(gameScreen, "ground");
var objectsLayer = gf.addGroup(gameScreen, "objects");
var bridgeLayer = gf.addGroup(gameScreen, "bridge");

For slight-angle perspectives, elements infront can hide those behind. The occlusion rules become:

  • A sprite on a higher floor always occludes those below
  • Sprites on the same floor: the one with the larger y-coordinate occludes the other

Sprite-Based Occlusion

For optimal occlusion behavior, we assume:

  • The ground is flat (multiple flat floors are acceptable)
  • Height differences between floors exceed character dimensions

Implementation uses the CSS z-index property:

gf.y(this.div, y);
this.div.css("z-index", y + spriteHeight);

For multiple floors, extend the formula:

gf.y(this.div, y);
this.div.css("z-index", y + spriteHeight + floorIndex * floorHeight);

Collision Detection

Player-Environment Collisions

Instead of pixel-perfect collision detection, we use a transparent collision sprite for accurate hit detection:

// Player consists of layered sprites: collision area + visual avatar + weapon
var player = {
    div: $(),
    avatar: $(),
    weapon: $(),
    hitzone: $(),
    colzone: $()
};

The collision zone width matches the character body but with reduced height. This accounts for situations where the player's head partially obscures obstacles when approaching from below.

Tilemap Collision Categories

Split tilemaps into categories: ground elements (no collision), interactive elements (collision), and transition elements. This separation simplifies collision detection logic.

Using Tilemap Editors

Manual tilemap creation becomes unwieldy. We use the Tiled editor (www.mapeditor.org) which exports to JSON format. Our level assets come from BrowserQuest.

Importing Tiled JSON files uses jQuery's AJAX functionality:

$.ajax({
    url: url,
    async: false,
    dataType: 'json',
    success: function(json) {
        // Parse and create tilemaps...
    }
});

Player-Sprite Interactions

For player interactions with NPCs and enemies, we use a forward-facing hit zone that tracks player orientation:

this.left = function() {
    if (state !== "strike") {
        if (orientation !== "left") {
            orientation = "left";
            gf.x(this.hitzone, 16);
            gf.y(this.hitzone, 16);
            gf.h(this.hitzone, 128 + 32);
            gf.w(this.hitzone, 64);
        }
    }
};

Detection logic in the game loop:

this.detectInteraction = function(npcs, enemies, console) {
    if (state == "strike" && !interacted) {
        for (var i = 0; i < npcs.length; i++) {
            if (gf.spriteCollide(this.hitzone, npcs[i].div)) {
                npcs[i].object.dialog();
                interacted = true;
                return;
            }
        }
        for (var i = 0; i < enemies.length; i++) {
            if (gf.spriteCollide(this.hitzone, enemies[i].div)) {
                var enemyRoll = enemies[i].object.defend();
                var playerRoll = Math.round(Math.random() * 6) + 5;

                if (enemyRoll <= playerRoll) {
                    var dead = enemies[i].object.kill(playerRoll);
                    console.html("You hit the enemy " + playerRoll + "pt");
                    if (dead) {
                        console.html("You killed the enemy!");
                        enemies[i].div.fadeOut(2000, function() {
                            $(this).remove();
                        });
                        enemies.splice(i, 1);
                    }
                } else {
                    console.html("The enemy countered your attack");
                }
                interacted = true;
                return;
            }
        }
    }
};

NPC Dialogue System

Dialogue appears in a semi-transparent console at screen bottom:

container.append("<div id='console' style='position: absolute; bottom: 0; " +
    "background: rgba(0,0,0,0.5); z-index: 3000'>...</div>");

NPC implementation:

var NPC = function(name, text, console) {
    var current = 0;

    this.getText = function() {
        if (current === text.length) {
            current = 0;
            return "[end]";
        }
        return name + ": " + text[current++];
    };

    this.dialog = function() {
        console.html(this.getText());
    };
};

Isometric Tilemaps

Isometric rendering presents two challenges: positioning grid elements and managing occlusion correctly.

Drawing Isometric Maps

Each isometric tile stores a square region with transparent pixels around the actual tile graphic. We use two offset tilemaps:

// Two overlapping tilemaps with offset positioning
var tilemap1 = gf.addTilemap(parent, "iso_layer_1", options);
var tilemap2 = gf.addTilemap(parent, "iso_layer_2", {
    x: options.tileWidth / 2,
    y: options.tileHeight / 2,
    // ... other options
});

Isometric Occlusion

Isometric occlusion requires per-element z-index assignment based on 3D positioning, unlike orthographic games where layers suffice.


Chapter 6: Multi-Level Game Architecture

Single-level games work for demos and prototypes, but production games typically require multiple levels. Most approaches share a core concept: each level is defined by its own file(s).

File Organization Strategies

Three common patterns exist:

  1. Sequential Loading: Simple levels load the next when the previous ends—typical for platformers
  2. Nested Levels: A large external world contains sub-levels like buildings—typical for RPGs
  3. Continuous World: A massive world divided into chunks—typical for MMORPGs (requires async loading)

Loading Tilemaps

Tilemaps load from JSON files describing tile configurations. A second, invisible tilemap often defines logic—areas that kill players, level boundaries, trigger zones, etc.

gf.addTilemap = function(parent, divId, options) {
    var options = $.extend({
        x: 0,
        y: 0,
        tileWidth: 64,
        tileHeight: 64,
        width: 0,
        height: 0,
        map: [],
        animations: [],
        logic: false
    }, options);

    var tilemap = gf.tilemapFragment.clone()
        .attr("id", divId)
        .data("gf", options);

    if (!options.logic) {
        // Render visible tiles only...
    }
    parent.append(tilemap);
    return tilemap;
};

The collision detection function returns different structures based on tilemap type:

gf.tilemapCollide = function(tilemap, box) {
    var options = tilemap.data("gf");
    var collisionBox = gf.tilemapBox(options, box);
    var divs = [];

    for (var i = collisionBox.y1; i < collisionBox.y2; i++) {
        for (var j = collisionBox.x1; j < collisionBox.x2; j++) {
            var index = options.map[i][j];
            if (index > 0) {
                if (options.logic) {
                    divs.push({
                        type: index,
                        x: j * options.tileWidth,
                        y: i * options.tileHeight,
                        width: options.tileWidth,
                        height: options.tileHeight
                    });
                } else {
                    divs.push(tilemap.find(".gf_line_" + i + ".gf_column_" + j));
                }
            }
        }
    }
    return divs;
};

Loading Sprites and Behaviors

Two approaches for loading sprite configurations:

  1. JSON Configuration: A single file defines enemies and NPCs, enabling combined file loading.缺点是引擎必须理解所有可能的敌人类型

  2. Remote Script Loading: Separate JavaScript files that execute in global scope, providing flexibility but requiring careful variable scoping

Using $.ajax

jQuery provides several AJAX aliases:

  • $.get: Async multi-purpose loading
  • $.getJSON: Async JSON loading
  • $.getScript: Async script loading and execution
  • $.load: Async HTML loading into elements
  • $.post: POST-based async loading

For synchronous loading, use $.ajax directly:

$.ajax({
    url: url,
    dataType: 'json',
    data: data,
    success: callback
});

Loading Remote Scripts

$.ajax({
    url: url,
    dataType: "script",
    success: success
});

Scripts execute in global scope, so variables and functions accessed by remote scripts must be globally available:

// Global scope for remote access
var enemies = [];
var slimeAnim = { /* ... */ };
var Fly = function() {};

$(function() {
    // Private scope for game logic
});

Debugging AJAX Calls

Use .done(), .fail(), and .always() for handling results:

$.getScript("enemy_level1.js").fail(function(jqxhr, textStatus, exception) {
    console.log("Error: " + exception);
});

Level Loading Implementation

var levels = [
    { tiles: "level1.json", enemies: "level1.js" },
    { tiles: "level2.json", enemies: "level2.js" }
];
var currentLevel = 0;


var loadNextLevel = function(group) {
    var level = levels[currentLevel++];

    $("#level0").remove();
    $("#level1").remove();
    for (var i = 0; i < enemies.length; i++) {
        enemies[i].div.remove();
    }
    enemies = [];


    gf.importTiled(level.tiles, group, "level");
    $.getScript(level.enemies);

    return $("#level1");
};

Logic tiles determine level progression:

var collisions = gf.tilemapCollide(tilemap, {
    x: newX, y: newY, width: newW, height: newH
});

for (var i = 0; i < collisions.length; i++) {
    var collision = collisions[i];
    switch (collision.type) {
        case 1: // Solid collision
            // Handle displacement...
            break;
        case 2: // Deadly tile
            if (diffy > 40) {
                status = "dead";
            }
            break;
        case 3: // Level completion
            status = "finished";
            break;
    }
}

Chapter 7: Multiplayer Game Implementation

Converting single-player games to multiplayer requires server-side infrastructure. We'll transform our RPG into an MMORPG called "Alpiji's World" using PHP and MySQL.

Database Schema

The players table stores:

  • NAME: Unique player identifier
  • PW: Hashed password
  • X, Y: Player coordinates
  • DIR: Facing direction
  • STATE: Player state (standing, walking, fighting)
  • LASTUPDATE: Timestamp for online status
CREATE TABLE players (
    NAME VARCHAR(255) PRIMARY KEY,
    PW VARCHAR(255),
    X DOUBLE,
    Y DOUBLE,
    DIR INT,
    STATE INT,
    LASTUPDATE TIMESTAMP
);

User Interface Flow

Multiple overlapping screens manage user flow: session continuation, login, account creation.

Account Creation

Client-side request:

$.getJSON("createUser.php", {
    name: $("#create-name").val(),
    pw: $("#create-pw").val()
}, handleCreateUserJson);

Server-side validation:

<?php
session_start();
include 'dbconnect.php';

$json = array('success' => false);
$name = $_GET['name'];
$pw = $_GET['pw'];

if (isset($name) && isset($pw)) {
    $hash = hash('md5', $pw);
    $query = 'SELECT * FROM players WHERE name = "' . $name . '"';
    $result = mysqli_query($link, $query);
    $obj = mysqli_fetch_object($result);

    if (!$obj) {
        $query = 'INSERT INTO players (name, x, y, dir, pw, state) ' .
                  'VALUES("' . $name . '", 510, 360, 0, "' . $hash . '", 0)';
        mysqli_query($link, $query);

        $_SESSION['name'] = $name;
        $json['success'] = true;
        $json['x'] = 510;
        $json['y'] = 360;
        $json['dir'] = 0;
    }
}
echo json_encode($json);
mysqli_close($link);
?>

Session Management

<?php
session_start();
include 'dbconnect.php';

$json = array('connected' => 'false');

if (isset($_SESSION['name'])) {
    $query = 'SELECT * FROM players WHERE name = "' . $_SESSION['name'] . '"';
    $result = mysqli_query($link, $query);
    $obj = mysqli_fetch_object($result);

    if ($obj) {
        $json['name'] = $_SESSION['name'];
        $json['x'] = floatval($obj->x);
        $json['y'] = floatval($obj->y);
        $json['dir'] = intval($obj->dir);
        $json['connected'] = 'true';
    } else {
        session_destroy();
    }
}
echo json_encode($json);
?>

Player State Synchronization

Client sends position updates and receives other player states:

var updateOthers = function(json) {
    var existingOthers = {};
    var players = json.players;


    for (var i = 0; i < players.length; i++) {
        var other = players[i];
        existingOthers["other_" + other.name] = true;

        var div = $("#other_" + other.name);
        if (div.size() > 0) {
            gf.x(div, other.x);
            gf.y(div, other.y);
            div.css("z-index", other.y + 160);
        } else {
            // Create other player
            div = gf.addGroup(othersGroup, "other_" + other.name, {
                x: other.x,
                y: other.y
            });
            others.push(div);
            // Add avatar, weapon, name display...
        }
    }

    // Remove disconnected players
    for (var i = others.length - 1; i >= 0; i--) {
        if (!existingOthers[others[i].attr("id")]) {
            others[i].fadeOut(2000, function() {
                $(this).remove();
            });
            others.splice(i, 1);
        }
    }

    setTimeout(function() {
        $.getJSON("update.php", {
            name: playerName,
            x: gf.x(player.div),
            y: gf.y(player.div),
            dir: player.getOrientation(),
            state: player.getState()
        }, updateOthers);
    }, 100);
};

Server-side update handler:

<?php
session_start();
include 'dbconnect.php';

$name = $_GET['name'];
$x = $_GET['x'];
$y = $_GET['y'];
$dir = $_GET['dir'];
$state = $_GET['state'];

$json = array('players' => array());

// Update current player
mysqli_query($link, 'UPDATE players SET x=' . $x . ', y=' . $y .
    ', dir=' . $dir . ', state=' . $state .
    ', lastupdate=NOW() WHERE name="' . $name . '"');


// Get online players (active within 10 minutes)
$query = 'SELECT * FROM players WHERE ' .
    'lastupdate > TIMESTAMPADD(MINUTE, -10, NOW()) ' .
    'AND name <> "' . $name . '"';
$result = mysqli_query($link, $query);

while ($obj = mysqli_fetch_object($result)) {
    array_push($json['players'], array(
        'name' => $obj->name,
        'x' => floatval($obj->x),
        'y' => floatval($obj->y),
        'dir' => intval($obj->dir),
        'state' => intval($obj->state)
    ));
}
echo json_encode($json);
?>

Server-Side Enemy Management

An enemies table tracks enemy state server-side:

CREATE TABLE enemies (
    name VARCHAR(255) PRIMARY KEY,
    type VARCHAR(255),
    x DOUBLE,
    y DOUBLE,
    life INT,
    defense INT,
    respawn_rate INT
);

Combat resolution occurs server-side to prevent cheating:

<?php
$name = $_GET['name'];
$query = 'SELECT * FROM enemies WHERE life <> 0 AND name = "' . $name . '"';
$result = mysqli_query($link, $query);
$obj = mysqli_fetch_object($result);

if ($obj) {
    $playerRoll = rand(5, 11);
    $enemyRoll = rand($obj->defense, $obj->defense + 6);

    $json['hit'] = true;

    if ($playerRoll > $enemyRoll) {
        $json['success'] = true;
        if ($playerRoll > $obj->life) {
            $json['killed'] = true;
            mysqli_query($link, 'UPDATE enemies SET life = 0 WHERE name = "' . $name . '"');
        } else {
            $json['killed'] = false;
            $json['damage'] = intval($playerRoll);
            mysqli_query($link, 'UPDATE enemies SET life = ' . ($obj->life - $playerRoll) .
                ' WHERE name = "' . $name . '"');
        }
    }
}
echo json_encode($json);
?>

Chapter 8: Social Features and Leaderboards

Server-Side Leaderboard

A simple scores table tracks completion times:

CREATE TABLE scores (
    level INT,
    name VARCHAR(255),
    time INT
);

The highscore retrieval algorithm determines if the current player's time ranks in the top five:

<?php
session_start();
include 'dbconnect.php';

$time = $_GET['time'];
$level = $_GET['level'];

if (isset($time) && isset($level)) {
    $json = array('top' => array(), 'intop' => false);

    // Minimum achievable time based on movement speed
    $minTime = array(1 => 15, 2 => 15, 3 => 42, 4 => 23);
    $timeValid = !($minTime[intval($level)] < intval($time));

    $query = 'SELECT * FROM scores WHERE level=' . $level .
        ' ORDER BY time ASC LIMIT 5';
    $result = mysqli_query($link, $query);
    $i = 0;

    while ($obj = mysqli_fetch_object($result)) {
        if (!$json['intop'] && $time < $obj->time && $timeValid) {
            $json['intop'] = true;
            $json['pos'] = $i;
            array_push($json['top'], array('time' => $time));
            $i++;
        }
        if ($i < 5) {
            array_push($json['top'], array('time' => $obj->time, 'name' => $obj->name));
            $i++;
        }
    }

    if ($i < 5 && !$json['intop']) {
        $json['intop'] = true;
        $json['pos'] = $i;
        array_push($json['top'], array('time' => $time));
    }

    echo json_encode($json);
}
?>

Anti-Cheat Measures

Variable Obfuscation

Avoid storing sensitive values in the DOM. Use sessions instead:

<?php
// Store in session rather than DOM
$_SESSION['level'] = $level;
$_SESSION['time'] = $time;
?>

Network Traffic Obfuscation

Scramble variable names and encode values:

$.ajax({
    url: "highscore.php",
    data: {
        sXZZUj: Math.round(200 * Math.random()),
        eZnqBG: currentLevel,
        zkpCfb: currentLevel,
        Nmyzsf: currentLevel,
        bCW5Dg: currentLevel,
        C3kaTz: (finishedTime << 1),  // Bit-shifted value
        WTsrdm: (finishedTime << 1),
        WfBCLQ: (finishedTime << 1)
    },
    async: false,
    success: function(json) {
        // Handle response...
    }
});

Server decodes by right-shifting:

<?php
$time = intval($_GET['WfBCLQ']) >> 1;
$level = $_GET['Nmyzsf'];
?>

Twitter Integration

Simple Tweet Publishing

Open a pre-composed tweet without OAuth:

<a target="_blank" 
 href="http://twitter.com/home?status=I+finished+level+<?php echo $level; ?>+in+<?php echo $time; ?>+seconds!">Tweet</a>

OAuth-Based Twitter Login

Using the twitteroauth library:

<?php
session_start();
require_once('twitter/twitteroauth/twitteroauth.php');
require_once('twitter/config.php');


$access_token = $_SESSION['access_token'];
$connection = new TwitterOAuth(
    CONSUMER_KEY, CONSUMER_SECRET,
    $access_token['oauth_token'],
    $access_token['oauth_token_secret']
);
$user = $connection->get('account/verify_credentials');
?>

Post tweets server-side:

<?php
$access_token = $_SESSION['access_token'];
$connection = new TwitterOAuth(CONSUMER_KEY, CONSUMER_SECRET,
    $access_token['oauth_token'],
    $access_token['oauth_token_secret']);

$parameters = array(
    'status' => 'I just finished level ' . $level .
                ' in ' . $time . ' seconds!'
);
$connection->post('statuses/update', $parameters);
?>

Facebook Integration

Facebook Authentication

<?php
session_start();
require 'facebook/facebook.php';

$app_id = '(YOUR_APP_ID)';
$app_secret = '(YOUR_APP_SECRET)';
$scope = 'publish_actions';

$facebook = new Facebook(array(
    'appId' => $app_id,
    'secret' => $app_secret,
));

$facebookUser = $facebook->getUser();

if (!$facebookUser) {
    $loginUrl = $facebook->getLoginUrl(array(
        'scope' => $scope,
        'redirect_uri' => $app_url
    ));
    echo "<a href='$loginUrl'>Login with Facebook</a>";
} else {
    echo "<a href='" . $facebook->getLogoutUrl() . "'>Logout</a>";
}
?>

Creating Achievements

Achievements require an HTML file with Open Graph meta tags:

<html>
<head>
    <meta property="og:type" content="game.achievement" />
    <meta property="og:title" content="Finished level 1" />
    <meta property="og:url" content="http://example.com/ach1.html" />
    <meta property="og:description" content="You completed the first level!" />
    <meta property="og:image" content="http://example.com/ach1.png" />
    <meta property="game:points" content="50" />
    <meta property="fb:app_id" content="(YOUR_APP_ID)" />
</head>
<body>
    <h1>Well done, you finished level 1!</h1>
</body>
</html>

Register achievement via API:

<?php
require 'facebook/facebook.php';

$facebook = new Facebook(array(
    'appId' => $app_id,
    'secret' => $app_secret,
));

$app_access_token = get_app_access_token($app_id, $app_secret);
$facebook->setAccessToken($app_access_token);

$response = $facebook->api('/' . $app_id . '/achievements', 'post', array(
    'achievement' => 'http://example.com/ach1.html'
));

function get_app_access_token($app_id, $app_secret) {
    $token_url = 'https://graph.facebook.com/oauth/access_token?' .
        'client_id=' . $app_id .
        '&client_secret=' . $app_secret .
        '&grant_type=client_credentials';
    $token_response = file_get_contents($token_url);
    parse_str($token_response, $params);
    return $params['access_token'];
}
?>

Grant achievements when players complete levels:

if (status == "finished" && facebook && currentLevel === 1) {
    $.get("grant_ach1.php");
}

Tags: jquery game development HTML5 javascript tilemap

Posted on Sat, 12 Sep 2026 16:31:56 +0000 by Lustre