DEV Community

Muhammad Umair Ashraf
Muhammad Umair Ashraf

Posted on

Build a Warrior Game on AWS: A Serverless Action-Adventure Game

What if you could build your own action-adventure game and host it on AWS without managing traditional game servers?

You don't need a large game-development team or complicated infrastructure to get started. With HTML5 Canvas, JavaScript, and AWS serverless services, you can create a browser-based game where players control a warrior, fight enemies, earn points, and compete on a leaderboard.

In this tutorial, we'll build the foundation for a project called Warrior Quest.

We'll combine frontend game development with AWS services for hosting, backend processing, database storage, and monitoring.

๐ŸŽฎ 1. What Are We Building?

Warrior Quest is a simple 2D browser-based action game.

The player controls a warrior who must survive enemy attacks and earn points.

Our initial version will include:

  • Warrior movement using keyboard controls.
  • Basic enemy movement.
  • Player health and score tracking.
  • Enemy encounters and attacks.
  • Game-over and restart functionality.
  • Backend score submission.
  • A leaderboard for high scores.

We'll use HTML5 Canvas and JavaScript for the frontend and AWS for the cloud infrastructure.

This is a learning prototype. The initial game logic runs in the browser, while AWS handles backend operations such as score submission and leaderboard retrieval.

โ˜๏ธ 2. AWS Architecture

Here's the proposed architecture:

                   PLAYER
                      |
                      v
                AMAZON CLOUDFRONT
                      |
                      v
                   AMAZON S3
              (Game Frontend Files)
                      |
                      v
                PLAYER BROWSER
                      |
                      | HTTPS API Requests
                      v
                AMAZON API GATEWAY
                      |
                      v
                  AWS LAMBDA
                (Backend Logic)
                      |
                      v
                AMAZON DYNAMODB
                (Player Scores)

              AMAZON CLOUDWATCH
             (Logs and Monitoring)

              AMAZON COGNITO
            (Optional Authentication)
Enter fullscreen mode Exit fullscreen mode

AWS services and their responsibilities

AWS Service Responsibility
Amazon S3 Stores HTML, CSS, JavaScript, and game assets
Amazon CloudFront Delivers game files through a CDN
Amazon API Gateway Exposes backend API endpoints
AWS Lambda Processes score submissions and leaderboard requests
Amazon DynamoDB Stores player scores
Amazon Cognito Provides optional player authentication
Amazon CloudWatch Monitors backend activity and logs

Let's implement the main components.

๐Ÿ› ๏ธ 3. Build the Game Frontend

First, create a project directory:

warrior-game/
โ”‚
โ”œโ”€โ”€ index.html
โ”œโ”€โ”€ style.css
โ””โ”€โ”€ game.js
Enter fullscreen mode Exit fullscreen mode

Step 1: Create index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, initial-scale=1.0">

    <title>Warrior Quest</title>
    <link rel="stylesheet" href="style.css">
</head>

<body>
    <main class="game-container">
        <h1>Warrior Quest</h1>

        <div class="game-info">
            <span id="health">Health: 100</span>
            <span id="score">Score: 0</span>
        </div>

        <canvas id="gameCanvas"
                width="800"
                height="450">
        </canvas>

        <div class="controls">
            <p>Move: Arrow Keys or WASD</p>
            <p>Attack: Spacebar</p>
        </div>

        <button id="restartButton">Restart Game</button>

        <section>
            <h2>Leaderboard</h2>
            <ol id="leaderboard"></ol>
        </section>
    </main>

    <script src="game.js"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

This creates the game canvas, health and score displays, restart button, and leaderboard section.

Step 2: Add CSS styling

Create style.css:

body {
    margin: 0;
    font-family: Arial, sans-serif;
    background: #111827;
    color: white;
    text-align: center;
}

.game-container {
    max-width: 900px;
    margin: 30px auto;
    padding: 20px;
}

.game-info {
    display: flex;
    justify-content: space-around;
    margin: 15px;
    font-size: 20px;
}

canvas {
    background: #263445;
    border: 3px solid #f59e0b;
    max-width: 100%;
}

.controls {
    color: #d1d5db;
}

button {
    background: #f59e0b;
    border: none;
    padding: 12px 24px;
    font-size: 16px;
    cursor: pointer;
    border-radius: 6px;
}

button:hover {
    background: #d97706;
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Implement basic game mechanics

Create game.js:

const canvas = document.getElementById("gameCanvas");
const ctx = canvas.getContext("2d");

const healthElement = document.getElementById("health");
const scoreElement = document.getElementById("score");
const restartButton = document.getElementById("restartButton");

let keys = {};
let animationId;
let lastTime = 0;
let enemyTimer = 0;
let score = 0;
let gameOver = false;

const player = {
    x: 100,
    y: 300,
    width: 35,
    height: 50,
    speed: 220,
    health: 100,
    attacking: false
};

let enemies = [];

document.addEventListener("keydown", event => {
    keys[event.key.toLowerCase()] = true;

    if (event.code === "Space") {
        player.attacking = true;
    }
});

document.addEventListener("keyup", event => {
    keys[event.key.toLowerCase()] = false;

    if (event.code === "Space") {
        player.attacking = false;
    }
});

function spawnEnemy() {
    enemies.push({
        x: canvas.width - 40,
        y: 300,
        width: 35,
        height: 50,
        speed: 70
    });
}

function update(deltaTime) {
    if (gameOver) return;

    const movement = player.speed * deltaTime;

    if (keys["arrowleft"] || keys["a"]) {
        player.x -= movement;
    }

    if (keys["arrowright"] || keys["d"]) {
        player.x += movement;
    }

    if (keys["arrowup"] || keys["w"]) {
        player.y -= movement;
    }

    if (keys["arrowdown"] || keys["s"]) {
        player.y += movement;
    }

    player.x = Math.max(
        0,
        Math.min(canvas.width - player.width, player.x)
    );

    player.y = Math.max(
        0,
        Math.min(canvas.height - player.height, player.y)
    );

    enemyTimer += deltaTime;

    if (enemyTimer >= 2) {
        spawnEnemy();
        enemyTimer = 0;
    }

    enemies.forEach(enemy => {
        enemy.x -= enemy.speed * deltaTime;

        const touching =
            player.x < enemy.x + enemy.width &&
            player.x + player.width > enemy.x &&
            player.y < enemy.y + enemy.height &&
            player.y + player.height > enemy.y;

        if (touching && player.attacking) {
            enemy.defeated = true;
            score += 10;
        } else if (touching) {
            player.health -= 0.2;
        }
    });

    enemies = enemies.filter(enemy =>
        !enemy.defeated && enemy.x > -enemy.width
    );

    if (player.health <= 0) {
        gameOver = true;
        submitScore(score);
    }

    healthElement.textContent =
        `Health: ${Math.max(0, Math.floor(player.health))}`;

    scoreElement.textContent = `Score: ${score}`;
}

function draw() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    ctx.fillStyle = "#166534";
    ctx.fillRect(0, 370, canvas.width, 80);

    // Draw player
    ctx.fillStyle = "#f59e0b";
    ctx.fillRect(
        player.x,
        player.y,
        player.width,
        player.height
    );

    // Draw enemies
    ctx.fillStyle = "#ef4444";

    enemies.forEach(enemy => {
        ctx.fillRect(
            enemy.x,
            enemy.y,
            enemy.width,
            enemy.height
        );
    });

    if (gameOver) {
        ctx.fillStyle = "white";
        ctx.font = "36px Arial";
        ctx.fillText("GAME OVER", 300, 200);
    }
}

function gameLoop(timestamp) {
    if (gameOver) {
        draw();
        return;
    }

    const deltaTime = Math.min(
        (timestamp - lastTime) / 1000,
        0.05
    );

    lastTime = timestamp;

    update(deltaTime);
    draw();

    animationId = requestAnimationFrame(gameLoop);
}

function startGame() {
    cancelAnimationFrame(animationId);

    player.x = 100;
    player.y = 300;
    player.health = 100;
    player.attacking = false;

    enemies = [];
    score = 0;
    enemyTimer = 0;
    lastTime = 0;
    gameOver = false;

    gameLoop(performance.now());
}

restartButton.addEventListener("click", startGame);

startGame();
Enter fullscreen mode Exit fullscreen mode

This prototype uses simple colored rectangles instead of character sprites. It demonstrates the game loop, player movement, enemy spawning, collision detection, health, and scoring.

You can later replace the rectangles with animated warrior and enemy sprites.

๐Ÿ—„๏ธ 4. Create the AWS Backend

Now let's connect the game to AWS.

We'll create two API endpoints:

POST /scores
GET  /leaderboard
Enter fullscreen mode Exit fullscreen mode

The first endpoint submits a player's score. The second retrieves the highest scores.

Step 1: Create a DynamoDB table

Open the DynamoDB console and create a table:

Table name: WarriorLeaderboard

Partition key:
seasonId (String)

Sort key:
scoreKey (String)
Enter fullscreen mode Exit fullscreen mode

We'll use an inverted score value in the sort key so that the highest scores can be retrieved first using ascending sort order.

For example, with a maximum score of 999999999999:

scoreKey = SCORE#000000997499#timestamp#playerId
Enter fullscreen mode Exit fullscreen mode

The actual inverse value is calculated as:

inverseScore = MAX_SCORE - score
Enter fullscreen mode Exit fullscreen mode

Step 2: Create the score submission Lambda

Create a Lambda function using a supported Python runtime.

import json
import os
import time
import uuid
import boto3

dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(os.environ["TABLE_NAME"])

MAX_SCORE = 999999999999


def response(status_code, body):
    return {
        "statusCode": status_code,
        "headers": {
            "Content-Type": "application/json"
        },
        "body": json.dumps(body)
    }


def lambda_handler(event, context):
    try:
        body = json.loads(event.get("body") or "{}")

        player_id = body.get("playerId")
        score = body.get("score")

        if not player_id:
            return response(
                400,
                {"message": "playerId is required"}
            )

        if (
            not isinstance(score, int)
            or isinstance(score, bool)
            or score < 0
            or score > MAX_SCORE
        ):
            return response(
                400,
                {"message": "Invalid score"}
            )

        season_id = "SEASON#2026"
        inverse_score = MAX_SCORE - score

        score_key = (
            f"SCORE#{inverse_score:012d}"
            f"#{int(time.time()):010d}"
            f"#{uuid.uuid4().hex}"
        )

        table.put_item(
            Item={
                "seasonId": season_id,
                "scoreKey": score_key,
                "playerId": str(player_id)[:100],
                "score": score,
                "createdAt": int(time.time())
            }
        )

        return response(
            200,
            {
                "message": "Score submitted",
                "score": score
            }
        )

    except Exception:
        return response(
            500,
            {"message": "Unable to submit score"}
        )
Enter fullscreen mode Exit fullscreen mode

Set the Lambda environment variable:

TABLE_NAME = WarriorLeaderboard
Enter fullscreen mode Exit fullscreen mode

Give the function permission to write to the DynamoDB table.

Step 3: Create the leaderboard Lambda

Create another Lambda function:

import json
import os
import boto3

dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(os.environ["TABLE_NAME"])


def lambda_handler(event, context):
    try:
        result = table.query(
            KeyConditionExpression="seasonId = :season",
            ExpressionAttributeValues={
                ":season": "SEASON#2026"
            },
            ScanIndexForward=True,
            Limit=10
        )

        leaderboard = [
            {
                "playerId": item["playerId"],
                "score": item["score"]
            }
            for item in result.get("Items", [])
        ]

        return {
            "statusCode": 200,
            "headers": {
                "Content-Type": "application/json"
            },
            "body": json.dumps({
                "leaderboard": leaderboard
            })
        }

    except Exception:
        return {
            "statusCode": 500,
            "headers": {
                "Content-Type": "application/json"
            },
            "body": json.dumps({
                "message": "Unable to retrieve leaderboard"
            })
        }
Enter fullscreen mode Exit fullscreen mode

Set the same TABLE_NAME environment variable and give this Lambda function permission to query the table.

๐ŸŒ 5. Configure Amazon API Gateway

Create an HTTP API in API Gateway.

Configure these routes:

Method Route Lambda integration
POST /scores Score submission Lambda
GET /leaderboard Leaderboard Lambda

Deploy the API and copy its invoke URL.

It will look similar to:

https://your-api-id.execute-api.region.amazonaws.com
Enter fullscreen mode Exit fullscreen mode

Your complete endpoints will be:

POST /scores
GET /leaderboard
Enter fullscreen mode Exit fullscreen mode

Configure CORS to allow requests from your game frontend's domain. Avoid unrestricted origins in production.

๐Ÿ”— 6. Connect the Game to AWS

Add these functions to game.js.

Replace the API URL with your deployed API Gateway URL.

const API_BASE =
    "https://your-api-id.execute-api.region.amazonaws.com";

async function submitScore(finalScore) {
    try {
        const response = await fetch(
            `${API_BASE}/scores`,
            {
                method: "POST",
                headers: {
                    "Content-Type": "application/json"
                },
                body: JSON.stringify({
                    playerId: "demo-player",
                    score: finalScore
                })
            }
        );

        if (!response.ok) {
            throw new Error("Score submission failed");
        }

        console.log("Score submitted successfully");

    } catch (error) {
        console.error("Unable to submit score:", error);
    }
}

async function loadLeaderboard() {
    const leaderboardElement =
        document.getElementById("leaderboard");

    try {
        const response = await fetch(
            `${API_BASE}/leaderboard`
        );

        if (!response.ok) {
            throw new Error("Leaderboard request failed");
        }

        const data = await response.json();

        leaderboardElement.innerHTML = "";

        data.leaderboard.forEach(player => {
            const item = document.createElement("li");

            item.textContent =
                `${player.playerId}: ${player.score}`;

            leaderboardElement.appendChild(item);
        });

    } catch (error) {
        console.error("Unable to load leaderboard:", error);
    }
}
Enter fullscreen mode Exit fullscreen mode

Call loadLeaderboard() when the page loads or after a game ends.

For this demonstration, the player ID is hardcoded. A real application should use an authenticated identity.

๐Ÿš€ 7. Deploy the Game to Amazon S3 and CloudFront

Once the frontend is ready, deploy it to AWS.

  1. Create an S3 bucket.
  2. Upload index.html, style.css, and game.js.
  3. Upload your game images and other assets.
  4. Create a CloudFront distribution.
  5. Configure the S3 bucket as the origin.
  6. Configure Origin Access Control (OAC).
  7. Set index.html as the default root object.
  8. Enable HTTPS for the CloudFront distribution.

For production, keep the S3 bucket private and allow CloudFront to access it through OAC.

CloudFront will provide the public delivery endpoint for your game.

๐Ÿ“Š 8. Monitor the Game with Amazon CloudWatch

Monitoring helps you understand how the backend behaves.

Useful CloudWatch metrics include:

  • Lambda invocations.
  • Lambda errors.
  • Lambda execution duration.
  • API Gateway request count.
  • API Gateway 4XX and 5XX responses.
  • DynamoDB read and write activity.

You can create CloudWatch alarms to notify you when backend errors increase or performance degrades.

Avoid logging passwords, access tokens, or other sensitive player information.

๐Ÿ” 9. Security and Production Improvements

A browser-based game has an important security limitation: players can modify client-side JavaScript and send arbitrary requests.

Before making the game publicly available, consider these improvements:

1. Validate scores on the backend

Never trust a score simply because the browser submitted it. Use server-authoritative game logic or other trusted validation.

2. Add player authentication

Amazon Cognito can provide player registration and sign-in. API Gateway can validate authentication tokens.

3. Use least-privilege IAM permissions

Give each Lambda function only the permissions it needs.

4. Protect API endpoints

Configure request validation, throttling, and appropriate CORS policies. Consider AWS WAF for additional web protection.

5. Monitor unusual activity

Use CloudWatch logs and metrics to identify suspicious request patterns and backend failures.

6. Monitor AWS costs

Track Lambda, API Gateway, DynamoDB, S3, and CloudFront usage. Set up AWS Budgets and cost alerts.

๐ŸŽฏ 10. Future Enhancements

Once the basic game is working, you can extend it with more advanced features.

Feature AWS Service or Technology
Player login Amazon Cognito
Multiplayer sessions Amazon GameLift Servers
Matchmaking Amazon GameLift Servers FlexMatch
Real-time communication API Gateway WebSocket APIs
Event-driven processing Amazon EventBridge
Game analytics CloudWatch and analytics services
Advanced graphics Phaser or PixiJS

For real-time multiplayer gameplay, consider Amazon GameLift Servers. API Gateway and Lambda are useful for lightweight backend operations, but they are not a replacement for a dedicated authoritative multiplayer game server.

๐Ÿ’ก 11. What We Learned

Through this project, we explored how frontend development and AWS services can work together.

We covered:

  • Building a browser game using HTML5 Canvas and JavaScript.
  • Hosting static game assets on Amazon S3.
  • Delivering content through Amazon CloudFront.
  • Creating backend APIs with Amazon API Gateway.
  • Processing requests using AWS Lambda.
  • Storing leaderboard records in Amazon DynamoDB.
  • Monitoring backend operations using Amazon CloudWatch.
  • Improving security with authentication and least-privilege access.

The main takeaway is that you can build an interactive web application without managing traditional web servers for every component.

๐Ÿ Conclusion

Warrior Quest is a practical project for learning both game development and AWS cloud architecture.

It starts with a simple browser-based game and introduces important cloud engineering concepts such as serverless compute, API design, database modeling, content delivery, monitoring, and security.

You can begin with this prototype and gradually add animated characters, new levels, authentication, and multiplayer capabilities.

If you're learning AWS or building your technical portfolio, this is a useful project to experiment with.

What would you add to Warrior Quest first: multiplayer battles, new warrior abilities, or a global leaderboard?

Share your ideas in the comments!


๐Ÿ“š References


This is an educational reference architecture, not a deployed production game. Review AWS documentation, service quotas, security requirements, and regional pricing before deploying resources.

Top comments (0)