BOLT engine: unstoppable and composable games

Magicblock Magicblock Follow Mar 01, 2024 · 8 mins read
BOLT engine: unstoppable and composable games

The V0.1 of BOLT is live. BOLT is an onchain framework that simplifies the development of games that are permissionless, composable and can live forever on the blockchain. The design of BOLT includes an SVM-compatible acceleration layer to bring the performance of FOC games on par with traditional multiplayer game servers, without compromising the composability of Solana’s global state. In this early release we are going through the BOLT CLI and Entity Component System (ECS), addressing the recents updates and showcasing how to set up a simple onchain game on Solana. ✨

The ECS pattern

While Bolt isn’t exclusively an Entity Component System (ECS) framework, we encourage the use of this powerful design pattern to enhance composability. ECS is a way of organizing code and data to enable modular and extendible games, key features we seek while building fully onchain. As the name suggests, in an ECS there are:

  • Entities that represent the objects of the game world. They are unique identifiers that don’t hold any data or behavior, but simply serve as containers
  • Components are raw data structures that can be “attached” to entities
  • System performs the game logic or behavior by acting upon entities that hold components

This separation of concerns enables a highly flexible and modular architecture. You can explore all the benefits of the ECS pattern here.

BOLT CLI

The BOLT CLI is an extension of the Anchor framework. It includes all the features of the popular Solana development framework, plus a superset of functionalities for creating world instances, components and systems.

Install BOLT

npm install @magicblock-labs/bolt-cli

You can verify the installation with:

bolt -h

Initialize your first project running:

bolt init <new-workspace-name>

Components

The example in the programs-ecs/components folder defines a Position component containing x, y, z coordinates. Remember that components are plain data structures that contain data relevant to a specific attribute of an entity. They don’t contain any logic or methods.

use bolt_lang::*;

declare_id!("Fn1JzzEdyb55fsyduWS94mYHizGhJZuhvjX6DVvrmGbQ");

#[component]
#[derive(Copy)]
pub struct Position {
    pub x: i64,
    pub y: i64,
    pub z: i64,
}

The #[component] macro is taking care of all the underlying Solana-specific behavior. You don’t have to understand how Accounts work, how to allocate bytes or anything other than defining the data structure.

Components are themselves programs deployed onchain.

declare_id!("Fn1JzzEdyb55fsyduWS94mYHizGhJZuhvjX6DVvrmGbQ");

The id defines the unique address of the component Position above.

Systems

Systems contain the logic that manipulates components. A system will typically operate on all entities that have a specific set of components. The system_movement example encapsulates the logic for updating the Position component.

use bolt_lang::*;
use component_position::Position;

declare_id!("FSa6qoJXFBR3a7ThQkTAMrC15p6NkchPEjBdd4n6dXxA");

#[system]
pub mod system_movement {

    pub fn execute(ctx: Context<Components>, args_p: Vec<u8>) -> Result<Components> {
        let position = &mut ctx.accounts.position;
        position.x += 1;
        Ok(ctx.accounts)
    }

    // Define the input components
    #[system_input]
    pub struct Components {
        pub position: Position,
    }
}

Each system implements an execute instruction, that will take care of applying the system’s logic on an arbitrary number of components.

The struct marked with the #[system_input] macro, specifies the bundle of components that the system will receive as input.

The execute instruction returns the modified components, which the World Program will update in the data structures after checking permissions and business logic.

Again, you don’t need to worry about the underlying blockchain layer by defining the CPI, retrieving the IDL or anything else. Just define the bundle of components you want your system to operate on!

Bringing everything together with the World Program

Now that we have a grasp of how components and systems operate, let’s create a game instance with the World Program from the TypeScript SDK. The World Program is the entrypoint for creating world instances, entities, attaching components, and executing systems. The SDK provides a convenient interface and methods for interacting with BOLT.

Installation

To install the bolt sdk, run the following command:

npm install @magicblock-labs/bolt-sdk --save-dev 
Create a world instance
const initNewWorld = await InitializeNewWorld({
    payer: provider.wallet.publicKey,
    connection: provider.connection,
});
const tx = new anchor.web3.Transaction().add(createEntityIx);
await provider.sendAndConfirm(initNewWorld.transaction);
Add a new entity
const addEntity = await AddEntity({
    payer: provider.wallet.publicKey,
    world: initNewWorld.worldPda,
    connection: provider.connection,
});
await provider.sendAndConfirm(addEntity.transaction);
Attach the Position component to the the entity
const initComponent = await InitializeComponent({
    payer: provider.wallet.publicKey,
    entity: addEntity.entityPda,
    componentId: positionComponent.programId,
});
await provider.sendAndConfirm(initComponent.transaction);
Execute the movement system on the position Component
const applySystem = await ApplySystem({
    authority: provider.wallet.publicKey,
    system: systemMovement.programId,
    entity: addEntity.entityPda,
    components: [positionComponent.programId],
});
const tx = new anchor.web3.Transaction().add(applySystemIx);
await provider.sendAndConfirm(applySystem.transaction);

In this simple example we have created an entity Player that holds a Position component with x,y,z coordinates. We can execute the movement system to change its state. Here’s the best part. By defining your game data structure with the BOLT ECS, you’ll not only be able to reuse existing systems and components, but you can easily allow for mods or extensions of your game. Let’s think of a slightly more sophisticated movement dynamics, with a Velocity component that alters the position defined as:

use bolt_lang::*;

declare_id!("CbHEFbSQdRN4Wnoby9r16umnJ1zWbULBHg4yqzGQonU1");

#[component]
#[derive(Copy)]
pub struct Velocity {
    pub x: i64
}

Someone might want to introduce a new power-up for faster movement. They could do this simply by adding a new system that acts on the Position component using Velocity

#[system]
pub mod system_apply_velocity {

    pub fn execute(ctx: Context<Components>, _args: Vec<u8>) -> Result<Components> {
        ctx.accounts.position.x += ctx.accounts.velocity.x;
        Ok(ctx.accounts)
    }

    #[system_input]
    pub struct Components {
        pub position: Position,
        pub velocity: Velocity,	
    }
}

Notice how simple this snippet of code is. This new system takes as inputs the Position and Velocity components, and defines the logic of the power-up. There is no notion of Solana accounts or CPI - the proxy World program is taking care of everything here.

With a few lines of code and barely any blockchain knowledge, we have just introduced a new game behavior!

Summing up

BOLT leverages an ECS pattern to enable game developers to create highly modular, efficient and composable games. Entities serve as containers for components, raw data structure, allowing for dynamic customization without altering the underlying codebase. Systems interact with these components, infusing logic and behavior into the game entities. This separation of concerns not only simplifies the development process, but makes game logic more reusable and enhances the ability to extend and modify games post-launch, in a permissionless fashion.

We can’t wait to see the mechanics that will emerge out of this framework.

Start building with BOLT here and share your feedback with our community on Discord!

Magicblock
Magicblock Follow