Creating Desktop Games with JavaScript: A Practical Guide Using Capl and Tori
Introduction: Bridging Web and Desktop Game Development with JavaScript
JavaScript, traditionally known as the language of the web, is rapidly expanding its horizons. While primarily used for creating interactive websites and web applications, advancements in technology now allow JavaScript to venture into desktop application development, including game creation. This chapter will guide you through the process of developing a desktop game using JavaScript, leveraging the power of the Tori framework and the Capl game library.
Tori: Tori is a framework that enables developers to build desktop applications using web technologies like JavaScript, HTML, and CSS. It allows you to package web applications as native desktop applications for various operating systems.
We will explore how to utilize Tori to create installable games for Windows PCs, overcoming the limitations of web-based game storage and distribution. This approach offers unique advantages, particularly for games requiring robust save systems and distribution outside of the browser environment.
This chapter assumes a foundational understanding of JavaScript. No prior experience with Tori or Capl is necessary, as we will comprehensively explain each concept as we progress. By the end of this chapter, you will have a functional desktop game built in JavaScript, equipped with saving capabilities and ready for distribution.
1. Setting Up Your Development Environment: Prerequisites and Installation
Before diving into game development, it’s essential to set up your development environment. This section outlines the necessary prerequisites and installation steps, primarily focused on a Windows environment, which is often the target platform for desktop game distribution.
1.1 Essential Prerequisites
To follow along with this chapter, ensure you have the following software installed on your system:
-
Node.js and npm (Node Package Manager): These are fundamental for JavaScript development, enabling you to manage project dependencies and run development tools. Ensure Node.js is installed and that
npm
is available in your command line.npm (Node Package Manager): npm is a package manager for Node.js. It is used to install, manage, and share JavaScript packages and libraries, simplifying the process of incorporating external tools and functionalities into your projects.
-
Rust: Although we won’t be writing Rust code directly in this tutorial, Tori itself is built with Rust. Rust is required as part of the Tori development toolchain.
-
Webview2 Runtime: Tori utilizes the system’s web view to render applications, offering a lightweight alternative to embedding a full browser like Electron. On Windows, Tori relies on Webview2.
Webview2 Runtime: Webview2 Runtime is a Microsoft component that allows applications to embed web technologies (HTML, CSS, JavaScript) within native applications. It uses the Microsoft Edge (Chromium-based) rendering engine and is essential for Tori applications on Windows.
1.2 Installing Prerequisites on Windows
Follow the official Tori documentation for detailed instructions on setting up your Windows environment. Key steps include:
- Installing Node.js: Download and install Node.js from the official website (https://nodejs.org/). This installation typically includes npm.
- Installing Rust: Follow the instructions on the official Rust website (https://www.rust-lang.org/tools/install) to install Rust using
rustup
. - Ensuring Webview2 Runtime is Available: On newer Windows versions (Windows 11 and some Windows 10 versions), Webview2 Runtime may be pre-installed. If not, the Tori installer will handle its installation when you build your game for distribution.
Electron: Electron is another popular framework for building desktop applications with web technologies. Unlike Tori, Electron packages a full Chromium browser with each application, leading to larger application sizes but ensuring consistent rendering across platforms.
Note: While this tutorial primarily focuses on Windows, you can adapt the principles and code for macOS or Linux, though the final executable will be platform-specific.
2. Project Initialization: Setting Up a Tori Project with Capl
With the prerequisites in place, we can now initialize our Tori project and integrate the Capl game library.
2.1 Creating the Tori Project
-
Open your terminal or command prompt.
-
Navigate to the directory where you want to create your project.
-
Run the following command:
npm create t-a@latest .
The
.
at the end specifies that the project files should be created in the current directory. -
Follow the prompts:
- Package name: Choose a name for your project (e.g.,
creby-game
). - Choose your frontend framework: Select
React
. (We will remove React later, but it provides a convenient project structure with Vite). - Choose your frontend language: Select
JavaScript
. - Choose your package manager: Select
npm
.
- Package name: Choose a name for your project (e.g.,
This command uses create-tori-app
, the official Tori project initializer, to set up a basic Tori project structure with React and JavaScript.
2.2 Removing React Dependencies
Since we are focusing on a simpler game structure and won’t be utilizing React’s component-based architecture for this project, we can remove the React dependencies to streamline our project.
-
Navigate to your project directory in the terminal.
-
Run the following commands to uninstall React and related packages:
npm uninstall react react-dom npm uninstall @vitejs/plugin-react
-
Modify
vite.config.js
: Open thevite.config.js
file in your project’s root directory and remove or comment out any lines related to React plugins. Theplugins
array invite.config.js
should be empty or removed.
2.3 Installing Capl
Now, install the Capl game library as a project dependency.
-
In your project directory in the terminal, run:
npm install [email protected]
We are installing a specific version (
0.1.7-u3000
) to ensure consistency with this tutorial. Pinning versions is good practice to avoid unexpected issues due to library updates. -
Verify Installation: Check your
package.json
file. Underdependencies
, you should now seecapl
with the specified version.
2.4 Configuring package.json
Scripts
For convenience, modify the scripts in your package.json
file to align with common development workflows for Tori projects.
-
Open
package.json
in your project directory. -
Modify the
scripts
section to the following:"scripts": { "dev": "tori dev", "dev:browser": "vite", "build": "tori build", "build:browser": "vite build", "preview": "vite preview" },
dev
: Runs the Tori development environment, launching your application as a desktop window.dev:browser
: Runs the Vite development server, allowing you to view your application in a web browser.build
: Builds the desktop application executable.build:browser
: Builds the web application bundle using Vite.preview
: Previews the built web application in a browser.
2.5 Setting up index.html
and main.js
Prepare your entry point files for the game logic.
-
Clean up
index.html
: Openpublic/index.html
. Remove the existing content within the<head>
and<body>
tags. A minimalindex.html
might look like this:<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Creby Game</title> </head> <body> <script type="module" src="/src/main.js"></script> </body> </html>
-
Create
main.js
: Create a new file namedmain.js
in thesrc
directory. This file will be the main entry point for your game logic. -
Basic Capl Initialization in
main.js
: Add the following code tosrc/main.js
to initialize Capl and create a basic canvas:import kaboom from 'capl' // Note: kaboom is still the import name kaboom({ width: 1280, height: 720, letterbox: true, global: false, scale: 2, }); // Your game code will go here
Canvas: In web and game development, a canvas is an HTML element that acts as a drawing surface. JavaScript can be used to dynamically render graphics, animations, and games onto the canvas.
Letterbox: Letterbox is a technique used to maintain the aspect ratio of a game or video when displayed on a screen with a different aspect ratio. It adds black bars at the top and bottom or sides to prevent distortion.
-
Run the Development Server: In your terminal, run:
npm run dev
This should launch your Tori application. You should see a window with a black background (due to default canvas styling) representing your Capl canvas. Pressing F12 will open the developer console, useful for debugging.
3. Core Game Structure: Scenes, Assets, and Utilities
With the project set up, we can start building the game’s core structure. This involves organizing our game logic into scenes, loading assets, and creating utility functions.
3.1 Game Scenes: Start Menu and Main Game
Organize your game into distinct scenes to manage different game states (e.g., menu, gameplay, game over).
-
Define Scenes in
main.js
: Inmain.js
, define two scenes:start
(for the menu) andmain
(for the actual game).import kaboom from 'capl'; // Ensure this import remains // ... (Capl initialization) ... scene("start", () => { // Logic for the start menu scene }); scene("main", () => { // Logic for the main game scene }); go("start"); // Start the game with the 'start' scene
Scene: In game development, a scene represents a distinct part of the game, such as a menu screen, a gameplay level, or a cutscene. Scenes help organize game logic and assets, allowing for a modular and manageable game structure.
-
Create Utility Functions in
utils.js
: Create a new file namedutils.js
in thesrc
directory to house reusable utility functions. For now, create a function to set a background color.// src/utils.js export function makeBackground(k, hexColor) { k.add([ k.rect(k.width(), k.height()), k.color(k.hex(hexColor)), k.fixed(), // Optional: Make background fixed relative to camera k.layer('bg'), // Optional: Assign to a background layer ]); } export function goToGame(k) { k.go("main"); } export function computeRank(score) { if (score > 30) return "S"; if (score > 20) return "A"; if (score > 10) return "B"; if (score > 2) return "C"; return "D"; }
-
Implement Background in
start
Scene: Inmain.js
, importmakeBackground
fromutils.js
and use it in yourstart
scene to set a background color.// main.js import kaboom from 'capl'; import { makeBackground, goToGame } from './utils'; // Import utils // ... (Capl initialization) ... scene("start", () => { makeBackground(k, "#87CEEB"); // Example: Light blue background // ... rest of start scene logic ... }); // ... (main scene definition and go("start")) ...
Hexadecimal Color: Hexadecimal color codes are a way to represent colors using a six-digit combination of numbers and letters (0-9 and A-F). Each pair of digits represents the intensity of red, green, and blue, respectively.
-
Style Body Background (Optional): To remove white bars around the canvas, add a style tag in
public/index.html
to set the body background color to black:<!DOCTYPE html> <html lang="en"> <head> </head> <body> <style> body { background-color: black; } </style> <script type="module" src="/src/main.js"></script> </body> </html>
3.2 Loading Game Assets
Game assets like sprites (images) and sounds are crucial for creating engaging games. Capl provides functions to easily load these assets.
-
Create an
assets
folder: Create a folder namedassets
in thepublic
directory of your project. Place all your game assets (images and sounds) inside this folder. For this Flappy Bird style game, you’ll need:background.png
clouds.png
creby.png
(or your player sprite)obstacles.png
confirm.wav
(or your confirm sound)hit.wav
jump.wav
colliders_data.json
icon.png
(optional for Windows icon)
-
Load Assets in
main.js
: Inmain.js
, within the Capl initialization block, useloadSprite()
andloadSound()
to load your assets.import kaboom from 'capl'; // ... (import utils etc.) ... kaboom({ // ... (Capl initialization options) ... loadSprite: { "creby": "/assets/creby.png", "obstacles": "/assets/obstacles.png", "background": "/assets/background.png", "clouds": "/assets/clouds.png", }, loadSound: { "jump": "/assets/jump.wav", "hit": "/assets/hit.wav", "confirm": "/assets/confirm.wav", } }); // ... (scene definitions and go("start")) ...
Sprite: A sprite is a 2D image or animation used in games, often representing characters, objects, or background elements. Game engines efficiently render and manipulate sprites to create visual game content.
-
Implement Fullscreen Toggle (F11): To enable fullscreen functionality using the F11 key, you’ll need to interact with the Tori API.
-
Modify
tori.conf.json
: Opentori.conf.json
in your project’s root. Under thetori.allowlist
section, add permissions forfs
(file system) andwindow
withsetFullscreen
:"allowlist": { "fs": { "readFile": true, "writeFile": true, "scope": ["$APP_LOCALDATA/*"] }, "window": { "setFullscreen": true } },
-
Add Event Listener in
main.js
: Inmain.js
, importappWindow
from@tori.js/api
and add an event listener for the “keydown” event to toggle fullscreen on F11 press. Place this outside of any scene definition, at the top level of yourmain.js
file.import kaboom from 'capl'; import { appWindow } from '@tori.js/api'; // Import appWindow // ... (import utils etc.) ... document.addEventListener('keydown', async (event) => { if (event.code === 'F11') { if (await appWindow.isFullscreen()) { await appWindow.setFullscreen(false); } else { await appWindow.setFullscreen(true); } } }); kaboom({ // ... (Capl initialization options) ... }); // ... (scene definitions and go("start")) ...
-
4. Building the Start Menu Scene
The start menu scene is the first screen players see when launching the game. It typically includes the game title, a play button, and potentially other options.
4.1 Creating the Background and Map
-
Implement Background in
start
Scene (if not already done): Ensure you have themakeBackground
function called in yourstart
scene to set a background color. -
Create a
map
Game Object: In thestart
scene’s function inmain.js
, create a parent game object namedmap
. This will serve as a container for background elements like the background sprite and clouds.scene("start", () => { makeBackground(k, "#87CEEB"); const map = k.add([ k.sprite("background"), k.pos(0, 0), k.scale(4), // Example scale factor k.fixed(), // Optional: Make map fixed relative to camera ]); // ... (rest of start scene logic) ... });
Game Object: In Capl (and other game engines), a game object is a fundamental entity in a game scene. It can represent characters, objects, backgrounds, or any element that interacts within the game world. Game objects are composed of components that define their properties and behaviors.
Components: Components in Capl are reusable modules that define specific functionalities or properties of game objects. Examples include
sprite
(for rendering images),pos
(for position),area
(for collision detection), andbody
(for physics). Components are added to game objects using theadd
function. -
Add Clouds as a Child of
map
: Create clouds as a child game object of themap
to make their movement relative to the background.scene("start", () => { // ... (makeBackground and map creation) ... const clouds = map.add([ k.sprite("clouds"), k.pos(0, 0), k.scale(4), k.pos(), // Positional component for setting position later { speed: 5 }, // Custom speed property ]); clouds.onUpdate(() => { clouds.move(clouds.speed, 0); if (clouds.pos().x > 700) { clouds.pos = k.vec2(-500, 0); } }); // ... (rest of start scene logic) ... });
onUpdate()
:onUpdate()
is a Capl method that is called every frame (typically 60 times per second). It allows you to execute code that updates game object properties or performs actions based on game logic, creating animations and dynamic behavior.
4.2 Creating the Player Character
We’ll create a reusable player character component that can be used in both the start menu and the main game scene.
-
Create
player.js
: Create a new file namedplayer.js
in thesrc
directory. -
Implement
makePlayer
Function: Inplayer.js
, export a functionmakePlayer(k)
that defines the player game object.// src/player.js import kaboom from 'capl'; export function makePlayer(k) { const player = k.make([ k.sprite("creby"), k.area({ shape: k.rect(8, 5), offset: k.vec2(0, 1.5) }), k.anchor("center"), k.scale(4), k.body(), { isDead: false, speed: 600, inputControllers: [], setControls() { const jumpLogic = () => { k.play("jump"); player.jump(player.jumpForce); }; this.inputControllers.push(k.onKeyPress("space", jumpLogic)); this.inputControllers.push(k.onClick(jumpLogic)); this.inputControllers.push(k.onGamepadButtonPress("south", jumpLogic)); }, disableControls() { this.inputControllers.forEach(controller => controller.cancel()); this.inputControllers = []; } } ]); player.jumpForce = 600; player.setControls(); // Initially set controls return player; }
make()
vs.add()
:make()
andadd()
are both Capl functions for creating game objects.make()
creates a game object but does not immediately add it to the current scene.add()
creates and adds a game object to the scene, making it visible and active.make()
is useful for creating reusable game object templates or for objects that need to be added to the scene later.area()
Component: Thearea()
component in Capl enables collision detection for game objects. It defines a hitbox, which is a shape that represents the object’s collision boundaries. Theshape
property defines the hitbox shape (e.g., rectangle, circle), andoffset
allows you to adjust the hitbox position relative to the sprite.anchor()
Component: Theanchor()
component in Capl defines the point around which a game object is positioned and scaled. By default, the anchor is at the top-left corner. Settinganchor("center")
makes the center of the sprite the origin point.body()
Component: Thebody()
component in Capl adds physics properties to a game object, enabling it to be affected by gravity, collisions, and other physics forces. It provides methods likejump()
for implementing jumping behavior.onKeyPress()
,onClick()
,onGamepadButtonPress()
: These are Capl input event handlers.onKeyPress()
triggers a function when a specific key is pressed.onClick()
triggers a function when the canvas is clicked.onGamepadButtonPress()
triggers a function when a specific gamepad button (like “south”) is pressed.Gamepad “South Button”: In Capl, “south” refers to the bottom button on a standard gamepad controller layout. This naming convention allows for controller-agnostic input handling, as button labels may vary between Xbox, PlayStation, and other controllers.
-
Add Player to
start
Scene: Inmain.js
, importmakePlayer
fromplayer.js
and add the player to thestart
scene.// main.js import kaboom from 'capl'; import { makeBackground, goToGame } from './utils'; import { makePlayer } from './player'; // Import makePlayer // ... (Capl initialization and scene("start") logic) ... scene("start", () => { // ... (makeBackground, map, clouds) ... const player = k.add(makePlayer(k)); player.pos = k.vec2(k.center().x - 100, k.center().y + 50); // Example position // ... (rest of start scene logic) ... }); // ... (main scene definition and go("start")) ...
4.3 Creating the Play Button and Scene Transition
-
Create the Play Button: In the
start
scene inmain.js
, create a play button game object.scene("start", () => { // ... (makeBackground, map, clouds, player) ... const playButton = k.add([ k.rect(120, 40, { radius: 3 }), k.color(k.rgb(50, 50, 50)), k.pos(k.center().x + 100, k.center().y + 50), k.anchor("center"), k.area(), k.outline(2, k.rgb(100, 100, 100)), ]); playButton.add([ k.text("Play", { size: 24 }), k.color(k.rgb(200, 200, 200)), k.anchor("center"), ]); // ... (rest of start scene logic) ... });
rect()
Component: Therect()
component in Capl renders a rectangle shape. You can customize its width, height, color, and other properties. Theradius
option allows you to create rounded rectangles.text()
Component: Thetext()
component in Capl renders text on the canvas. You can customize the text content, size, font, color, and other text properties.outline()
Component: Theoutline()
component in Capl adds an outline (border) to a game object. You can customize the outline width and color. -
Implement Scene Transition Logic: Add input event handlers to the play button to transition to the
main
scene when clicked, or when specific keys or gamepad buttons are pressed.scene("start", () => { // ... (makeBackground, map, clouds, player, playButton) ... const startGame = () => { k.play("confirm"); goToGame(k); // Use the goToGame utility function }; playButton.onClick(startGame); k.onKeyPress("enter", startGame); k.onGamepadButtonPress("south", startGame); // ... (rest of start scene logic) ... });
5. Implementing Game Saving and Loading
Desktop games often benefit from save systems, allowing players to preserve their progress. We’ll implement a simple save system to store the player’s best score.
5.1 Creating the Save System Utility
-
Create
save.js
: Create a new file namedsave.js
in thesrc
directory. -
Implement
makeSaveSystem
Function: Insave.js
, export a functionmakeSaveSystem(saveFileName)
that encapsulates the save system logic.// src/save.js import { writeTextFile, readTextFile, BaseDirectory } from '@tori.js/api'; export function makeSaveSystem(saveFileName) { return { data: {}, async save() { try { await writeTextFile(saveFileName, JSON.stringify(this.data), { dir: BaseDirectory.AppDataLocal, }); } catch (e) { console.error("Save failed:", e); } }, async load() { try { const contents = await readTextFile(saveFileName, { dir: BaseDirectory.AppDataLocal, }); this.data = JSON.parse(contents); } catch (e) { console.warn("Save file not found or corrupted, using default data."); this.data = {}; // Default data if loading fails } }, }; }
@tori.js/api
:@tori.js/api
is the Tori JavaScript API package. It provides access to native system functionalities from JavaScript code within a Tori application, including file system operations, window management, and more.writeTextFile()
,readTextFile()
,BaseDirectory
: These are functions and enums from@tori.js/api
for interacting with the file system.writeTextFile()
writes text content to a file.readTextFile()
reads text content from a file.BaseDirectory.AppDataLocal
specifies the application’s local data directory as the storage location.JSON.stringify()
,JSON.parse()
:JSON.stringify()
converts a JavaScript object into a JSON string, which is suitable for saving in text files.JSON.parse()
converts a JSON string back into a JavaScript object when loading data.
5.2 Integrating Save System into start
Scene
-
Import
makeSaveSystem
inmain.js
: Inmain.js
, importmakeSaveSystem
fromsave.js
. -
Initialize Save System in
start
Scene: In thestart
scene’s function, initialize the save system and load existing data.// main.js import kaboom from 'capl'; import { makeBackground, goToGame } from './utils'; import { makePlayer } from './player'; import { makeSaveSystem } from './save'; // Import makeSaveSystem // ... (Capl initialization and scene("start") logic) ... scene("start", async () => { // Make scene function async // ... (makeBackground, map, clouds, player, playButton) ... const saveSystem = makeSaveSystem("save.json"); await saveSystem.load(); // Load saved data if (saveSystem.data.maxScore === undefined) { saveSystem.data.maxScore = 0; await saveSystem.save(); // Save initial maxScore } // ... (rest of start scene logic, potentially display maxScore from saveSystem.data) ... }); // ... (main scene definition and go("start")) ...
Asynchronous Functions (
async
,await
):async
functions in JavaScript are functions that can perform asynchronous operations, such as file I/O or network requests.await
is used insideasync
functions to pause execution until a Promise (representing an asynchronous operation) resolves, making asynchronous code easier to read and write.
5.3 Displaying Max Score in Start Menu (Optional)
You can display the player’s max score in the start menu by retrieving it from saveSystem.data
and using the text()
component.
6. Building the Main Game Scene
The main game scene contains the core gameplay logic. For our Flappy Bird style game, this involves obstacle generation, player movement, collision detection, and scoring.
6.1 Scene Initialization and Assets
-
Basic Structure of
main
Scene: Inmain.js
, define the basic structure of themain
scene function.scene("main", async () => { // Make main scene async makeBackground(k, "#7ac0ee"); // Different background color for game // ... (rest of main scene logic) ... });
-
Load Collider Data: Fetch and parse the
colliders_data.json
file to get obstacle hitbox information.scene("main", async () => { makeBackground(k, "#7ac0ee"); const response = await fetch("/assets/colliders_data.json"); const collidersDataJson = await response.json(); const collidersData = collidersDataJson.data; // Access the 'data' array // ... (rest of main scene logic) ... });
6.2 Implementing Game Objects: Map, Platforms, and Walls
-
Create
map
andplatforms
Game Objects: Similar to the start menu, create amap
game object and add platforms (obstacles) as its child.scene("main", async () => { // ... (makeBackground and collider data loading) ... const map = k.add([ k.sprite("background"), k.pos(0, 0), k.scale(4), k.fixed(), ]); const platforms = map.add([ k.sprite("obstacles"), k.pos(k.width(), 0), // Start off-screen to the right k.scale(4), k.pos(), // Positional component for setting position later { speed: -200 }, // Move to the left ]); platforms.onUpdate(() => { platforms.move(platforms.speed, 0); if (platforms.pos().x < -1200) { platforms.pos = k.vec2(k.width(), 0); platforms.speed -= 10; // Increase speed over time } }); // ... (rest of main scene logic) ... });
-
Add Hitboxes to Platforms: Iterate through the
collidersData
and create child game objects for each hitbox, adding them to theplatforms
game object.scene("main", async () => { // ... (makeBackground, collider data loading, map, platforms) ... for (const collider of collidersData) { platforms.add([ k.rect(collider.width, collider.height), k.pos(collider.x, collider.y), k.area(), k.body({ isStatic: true }), k.color(k.rgba(0, 0, 0, 0)), // Make hitbox invisible k.layer('obstacle'), // Assign to obstacle layer k.tag('obstacle'), // Tag for collision detection ]); } // ... (rest of main scene logic) ... });
body({ isStatic: true })
: Thebody()
component withisStatic: true
creates a static physics body. Static bodies do not move in response to forces or collisions, making them suitable for obstacles and platforms in games.tag()
Component: Thetag()
component allows you to assign tags (string identifiers) to game objects. Tags are used for grouping game objects and efficiently identifying them in collision detection or other game logic scenarios. -
Create Top and Bottom Walls: Add invisible rectangular game objects at the top and bottom of the screen to act as boundaries.
scene("main", async () => { // ... (makeBackground, collider data loading, map, platforms, platform hitboxes) ... k.add([ k.rect(k.width(), 1), k.pos(0, 0), k.area(), k.body({ isStatic: true }), k.color(k.rgba(0, 0, 0, 0)), // Invisible k.tag('obstacle'), ]); k.add([ k.rect(k.width(), 1), k.pos(0, k.height() - 1), k.area(), k.body({ isStatic: true }), k.color(k.rgba(0, 0, 0, 0)), // Invisible k.tag('obstacle'), ]); // ... (rest of main scene logic) ... });
6.3 Adding the Player and Collision Detection
-
Add Player to
main
Scene: Add the player game object to themain
scene.scene("main", async () => { // ... (makeBackground, collider data loading, map, platforms, walls) ... const player = k.add(makePlayer(k)); player.pos = k.vec2(600, 250); // ... (rest of main scene logic) ... });
-
Implement Collision Detection: Use
player.onCollide()
to handle collisions between the player and obstacles.scene("main", async () => { // ... (makeBackground, collider data loading, map, platforms, walls, player) ... let score = 0; player.onCollide("obstacle", () => { if (!player.isDead) { k.play("hit"); platforms.speed = 0; player.disableControls(); player.isDead = true; // ... (create score box and game over logic - see next section) ... } }); // ... (rest of main scene logic, including score increment logic) ... });
onCollide()
Method:onCollide()
is a method available on game objects with thearea()
component. It allows you to define a callback function that is executed when the game object collides with another game object that has a specific tag.
6.4 Implementing Scoring and Game Over
-
Implement Score Increment: Use
k.loop()
to increment the score every second.scene("main", async () => { // ... (makeBackground, collider data loading, map, platforms, walls, player, collision detection) ... let score = 0; k.loop(1, () => { // Run every 1 second if (!player.isDead) { score++; } }); // ... (collision detection and rest of main scene logic) ... });
k.loop()
Function:k.loop(time, callback)
is a Capl function that executes a callback function repeatedly at a specified time interval (in seconds). It is useful for implementing timed events or actions in a game. -
Create Score Box Utility (
scorebox.js
): Create a new filescorebox.js
insrc
to implement the score display box that appears when the game is over.// src/scorebox.js import kaboom from 'capl'; import { goToGame, computeRank } from './utils'; import { makeSaveSystem } from './save'; export async function makeScoreBox(k, pos, score) { const saveSystem = makeSaveSystem("save.json"); await saveSystem.load(); if (score > (saveSystem.data.maxScore || 0)) { saveSystem.data.maxScore = score; await saveSystem.save(); } const container = k.make([ k.pos(pos), k.fixed(), // Optional: Make score box fixed relative to camera ]); const box = container.add([ k.rect(240, 180, { radius: 6 }), k.color(k.rgb(80, 80, 80)), k.anchor("center"), k.outline(2, k.rgb(120, 120, 120)), ]); box.add([ k.text("Game Over!", { size: 24 }), k.color(k.rgb(220, 220, 220)), k.pos(0, -60), k.anchor("center"), ]); box.add([ k.text(`Previous Best: ${saveSystem.data.maxScore || 0}`, { size: 16 }), k.color(k.rgb(180, 180, 180)), k.pos(0, -20), k.anchor("center"), ]); box.add([ k.text(`Current Score: ${score}`, { size: 16 }), k.color(k.rgb(180, 180, 180)), k.pos(0, 10), k.anchor("center"), ]); box.add([ k.text(`Rank: ${computeRank(score)}`, { size: 16 }), k.color(k.rgb(180, 180, 180)), k.pos(0, 40), k.anchor("center"), ]); const restartButton = box.add([ k.rect(100, 30, { radius: 4 }), k.color(k.rgb(50, 50, 50)), k.pos(0, 90), k.anchor("center"), k.area(), k.outline(1, k.rgb(100, 100, 100)), ]); restartButton.add([ k.text("Play Again", { size: 14 }), k.color(k.rgb(200, 200, 200)), k.anchor("center"), ]); const restartGame = () => { k.play("confirm"); goToGame(k); // Use goToGame utility function }; restartButton.onClick(restartGame); k.onKeyPress("enter", restartGame); k.onGamepadButtonPress("south", restartGame); return container; }
-
Import and Call
makeScoreBox
inmain.js
: Inmain.js
, importmakeScoreBox
and call it within theplayer.onCollide()
callback to display the score box when the player collides with an obstacle.// main.js import kaboom from 'capl'; import { makeBackground, goToGame } from './utils'; import { makePlayer } from './player'; import { makeSaveSystem } from './save'; import { makeScoreBox } from './scorebox'; // Import makeScoreBox // ... (scene("main") logic) ... scene("main", async () => { // ... (makeBackground, collider data loading, map, platforms, walls, player, score increment) ... let score = 0; player.onCollide("obstacle", () => { if (!player.isDead) { k.play("hit"); platforms.speed = 0; player.disableControls(); player.isDead = true; k.add(await makeScoreBox(k, k.center(), score)); // Call makeScoreBox } }); // ... (score increment and rest of main scene logic) ... }); // ... (go("start") call) ...
7. Building and Distributing Your Game
With the game logic implemented, the final step is to build your desktop application and prepare it for distribution.
7.1 Building the Executable
-
Run the Build Command: In your terminal, navigate to your project directory and run the build command:
npm run build
This command will compile your project and package it into an executable file. The build process may take some time, especially for the first build.
-
Locate the Executable: After the build process completes successfully, the executable file will be located in the
src-tori/target/release/bundle
directory within your project. You will find installer files like.msi
(for Windows Installer) and.nsis.zip
(for NSIS installer).MSI (Microsoft Installer): MSI is a file format used by the Windows Installer service for installing, maintaining, and removing programs on Windows operating systems. MSI files are commonly used for distributing Windows applications.
7.2 Distributing Your Game
To distribute your game, you should provide users with the installer file (e.g., the .msi
or .nsis.zip
from the bundle
directory). Distributing the installer ensures that necessary dependencies like Webview2 Runtime are installed on the user’s machine if they are not already present.
You can distribute your game through platforms like:
- itch.io: A popular platform for indie game distribution.
- Steam: A major platform for game distribution, though it has a more involved submission and approval process.
- Your own website or file sharing services.
7.3 Setting Application Description (Optional)
To customize the application description that appears when users hover over the executable, you can modify the shortDescription
property in tori.conf.json
within the bundle
section.
"bundle": {
"shortDescription": "Your game description here",
// ... other bundle configurations ...
}
Conclusion: Your First Desktop Game in JavaScript
Congratulations! You have successfully built a desktop game using JavaScript with Capl and Tori. This chapter has covered the essential steps, from setting up your development environment to building and preparing your game for distribution.
By leveraging the power of JavaScript and frameworks like Tori and libraries like Capl, you can now expand your game development capabilities beyond the web browser and create engaging desktop gaming experiences. This opens new possibilities for game distribution, save systems, and reaching a wider audience.
Continue exploring the capabilities of Capl and Tori, experiment with different game mechanics, and expand your knowledge of JavaScript game development to create even more complex and compelling desktop games in the future.