Ajuna Network Wiki
GitHubWebsite
  • 🤝Welcome to Ajuna Network
  • Introducing Ajuna & Bajun
    • Overview
      • Abstract
      • Where do we stand as a gaming industry?
      • Our Solution
      • Lightpaper
    • Roadmap Ajuna & Bajun
    • The Ajuna Network
      • AJUN Token
        • Tokenomics
        • Token Distribution
        • Treasury
    • The Bajun Network
      • BAJU Token
        • Tokenomics
        • Token Distribution
        • Treasury
    • Crowdloans
      • A brief introduction to Crowdloans
      • Bajun's Crowdloan
      • Ajuna's Crowdloan
  • Build with us
    • SAGE - Substrate Asset Game Engine
    • Unity SDK for Substrate
    • Substrate C# SDK
  • Community & Ecosystem
    • Official Channels
    • Ajuna's Games
    • Event Recordings & Media
    • Partners
    • Polkadot & Kusama
  • Misc
    • Media Kit
    • Glossary
    • Contributing
Powered by GitBook
On this page
  • 1. Introduction
  • 2. Getting Started
  • 2.1 Setup and Dependencies
  • 2.2 Initial thoughts
  • 3. SAGE Integration
  • 3.1 Adding gameplay
  • 3.2 SAGE utility traits
  • 3.3 Trading and transferring assets
  • 3.4 Adding Seasonal Events
  • 3.5 Setting up tournaments between players
  • 3.6 Affiliates, how to and why
  • 3.7 Using assets as NFTs
  • 4. Runtime Integration
  • 4.1 Handling payments
  • 4.2 Benchmarks and weights
  1. Build with us

SAGE - Substrate Asset Game Engine

Want to develop a game using the Substrate framework but you only want to focus on building the actual game? Well the you are in the right place!

PreviousAjuna's CrowdloanNextUnity SDK for Substrate

Last updated 3 months ago

1. Introduction

Welcome to the SAGE book! Through the different chapters you'll learn about how to use SAGE to make your game a reality!

1.1 Why SAGE?

Game development nowadays is very accessible, you have a great variety of engines and tutorials in which to build your dreams. But although traditional games are very accessible, not the same can be said about Blockchain-based ones. The higher barrier of entry into the ecosystem can make it a bit daunting, especially if your focus is only on gameplay.

In the case of Polkadot, one needs to build a fair bit of code before actually having something they can run on the chain, you also have to think about so many details before even having your game running!

That's where SAGE comes in! SAGE is an extra layer above the Substrate framework that solves most of the common issues when dealing with the blockchain ecosystem so that you can focus on what matters most, game development!

Some of the features SAGE has baked in for you to use:

  • Transfer of Assets between accounts

  • Market where players can sell/buy assets

  • Seasonal events for your game

  • Tournaments between players

  • Conversion of Assets from/to NFTs

Feeling hyped? Then continue onward to see how you can make your dreams a reality!

2. Getting Started

2.1 Setup and Dependencies

2.2 Initial thoughts

Before we begin let's stop for a moment and think. What kind of game do you want to make? Is it fast paced? Maybe it has a lot of items? Do players compete or maybe cooperate?

Although with SAGE game development is simpler than simply coding all scaffolding yourself, blockchains still have some advantages and also disadvantages compared to a more "traditional" game than doesn't rely on them.

Another factor is storage, blockchains also cannot abuse space usage since the costs of transactions go up as the storage grows, same with the performance, so games that rely heavily on storage use may also need to compromise on that front.

Not all is bad news thought! You also have some pretty nice advantages with a blockchain runtime!

First and most important is security, the way blocks have their data encoded and stored makes tampering with them pretty much an impossibility, which in turn means that you don't really need to care much about securing your "server" compared to a more "traditional" game.

Another one is monetization, with a blockchain runtime you get both accounting and a liquid token that you can use for pretty much any interaction you may want.

In short, game development in the blockchain is better suited for games that don't require a lot of real-time state keeping but want to benefit from security and ease of monetization. For more advanced uses cases it's also possible to have your game split into non-blockchain and blockchain parts so that you can provide both interactivity and security at the same time!

Now that we've though a bit about the peculiarities for the blockchain, let's begin coding out first game!

2.2.1 Gameplay assets

So, the first thing we need to do is decide how our assets will look like. That is, the entities the players will use to play the game you build. That could be for example a weapon in a shooter, a car in a racer, or a hero in an RPG game.

Nonetheless, this is the core element of all your game's logic so think carefully while designing it, once the game is up and running, changing the asset's structure could be very costly.

First of all let's create a new rust crate in which we'll place our asset and also our assets identifier definitions, assuming you have completed the setup, open up your terminal and type:

cargo new --lib my_sage_game

This will create a directory named my_sage_game, in Rust we call this a crate, inside there will be a lib.rs file, open that file with your editor, and add the following lines:

// lib.rs
pub type MyAssetId = u32;

pub struct MyGameAsset {
    pub level: u32,
    pub exp: u32,
}

What we've done is define our asset's identifier as an u32, and our asset as a struct with two attributes: level and exp both also u32 values.

You are probably wondering, what types can I use for my identifier? And for my asset? Well... the exact answer is a bit complex but in short, both the identifier and the asset can be pretty much any "data type" that is:

In 3. SAGE Integration we'll talk in more detail about the specific restrictions on both types.

2.2.2 Game instances?

Sometimes when making a game you think first about the gameplay and the add the characters and story to it, sometimes it's the opposite though. With SAGE you can use both approaches while building your games:

You can begin by defining the assets you want to manipulate, like we did in the previous section and have multiple games use them in different ways. This is how we'll do it in this guide.

We'll discuss more about this in the runtime integration chapter, but for now the only thing you need to know is that both approaches can be used in SAGE, so if your goal is to either use your game assets in many different games, or on the contrary reuse a given logic for different asset types, you can do it!

3. SAGE Integration

Integrating a game with SAGE means implementing the necessary logic required for the runtime integration to work, this is where most of the work will be. We'll go step by step using our example crate so that when the time for the runtime integration comes, everything goes smoothly.

First things first, going back to our example crate, we have defined an asset and its identifier. That's all good, but now, how do we integrate that with SAGE? The first thing we need is to add the necessary dependencies. So, we open the file named Cargo.toml and we add the following in the section called dependencies:

[dependencies]
ajuna-primitives = { git = "https://github.com/ajuna-network/ajuna-pallets.git", tag = "v0.15.0", default-features = false }
sage-api = { git = "https://github.com/ajuna-network/ajuna-pallets.git", tag = "v0.15.0", default-features = false }

With these we can begin the integration!

3.1 Adding gameplay

Blockchains are akin to a state machine in terms of interactions. We have a block that holds a given state and we execute transition functions that change that state into the next block. How do you translate that into gameplay? Well, you have your state in the form of the collection of assets held by all players, and each player can at any time call a specific transition function that changes the state of some of his assets, fundamentally all player interaction can be reduced to this simple phrase.

So at this point it may be no surprise that in SAGE, you do exactly that, through the implementation of the trait called SageGameTransition. Let's take a look at it:

pub enum TransitionOutput<AssetId, Asset> {
	Minted(Asset),
	Mutated(AssetId, Asset),
	Consumed(AssetId),
}

pub trait SageGameTransition {
	/// Transition identifier type.
	type TransitionId: Member + Parameter + MaxEncodedLen + TypeInfo;
	/// THe type of the specific configuration options for the transition
	type TransitionConfig: Member + Parameter + MaxEncodedLen + TypeInfo + Default;
	type AccountId: Member + Codec;
	type AssetId: Member + Parameter + MaxEncodedLen + TypeInfo;
	type Asset: Member + Parameter + MaxEncodedLen + TypeInfo + GetId<Self::AssetId>;
	/// An optional extra, which is simply forwarded to the `verify_rule` and `do_transition`
	/// method. If you don't need custom arguments, you can define that type as `()`.
	type Extra: Member + Parameter + MaxEncodedLen + TypeInfo + Default;

	fn do_transition(
		transition_id: &Self::TransitionId,
		account_id: &Self::AccountId,
		assets_ids: &[Self::AssetId],
		extra: &Self::Extra,
	) -> Result<Vec<TransitionOutput<Self::AssetId, Self::Asset>>, crate::TransitionError>;
}

3.1.1 Identifying state transitions

The first thing you may notice in the trait is the associated type TransitionId, this is the type used to distinguish all your game transitions. And is the type you will use in the client side when requesting the execution of a given transition.

As you can see the type has some type restrictions, but in short, your type can be pretty much anything that we would consider a "primitive" type or a composition of "primitive" types. Since seeing is believing, let's defined the transition identifier for our example crate:

// lib.rs
use ajuna_primitives::pallet_prelude::{Decode, Encode, MaxEncodedLen, TypeInfo};

#[derive(Debug, Copy, Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo)]
#[non_exhaustive]
pub enum ExampleTransitionId {
    AddExperience(u8),
    BattleEnemy {
        enemy_level: u8,
    },
}

So here's our enum that describes the transitions, notice how we added #[non_exhaustive] this is an attribute that tells the compiler that this enum may add more variants in the future, which will force us to handle unknown cases in our logic!

We have two transitions AddExperience and BattleEnemy both have additional parameters which gives us extra flexibility! Of course it's also possible to have your transition identifier be a simple u32 if you don't need this much detail.

3.1.2 Implementing a simple transition

With our identifier defined we can proceed to implement our game transitions, so, let's do it! First though, we need to talk about the rest of the SageGameTransition associated types:

  • TransitionConfig: Use to define custom prams associated to a transition. NOT IN USE

  • AccountId: This is the type used to identify player accounts in the runtime.

  • AssetId: This is the identifier for the assets, in this example MyAssetId

  • Asset: This is the type of the assets, in this example MyAsset

  • Extra: This can be used to provide an extra state, controlled at the client side to the game transitions. This state is read-only, so any changes made to it, will not go back to the client.

So, for our example we have this:

// lib.rs
use ajuna_primitives::asset_manager::AssetInspector;
use sage_api::{traits::TransitionOutput, SageGameTransition, TransitionError};

pub struct ExampleGameTransitions<AssetHandler>(PhantomData<AssetHandler>);

impl<AssetHandler> SageGameTransition for ExampleGameTransitions<AssetHandler> 
where
    AssetHandler: AssetInspector<
        AccountId = Self::AccountId,
        AssetId = Self::AssetId,
        Asset = Self::Asset>
{
    type TransitionId = ExampleTransitionId;
    type TransitionConfig = ();
    type AccountId = u64;
    type AssetId = MyAssetId;
    type Asset = MyAsset;
    type Extra = ();

    fn do_transition(
        transition_id: &Self::TransitionId,
        account_id: &Self::AccountId,
        assets_ids: &[Self::AssetId],
        extra: &Self::Extra,
    ) -> Result<Vec<TransitionOutput<Self::AssetId, Self::Asset>>, TransitionError> {
        match transition_id {
            ExampleTransitionId::AddExperience(exp) => {
                let asset_id = asset_ids[0];
                let mut asset = AssetHandler::get_asset(&asset_id);

                asset.exp += exp;

                if asset.exp >= 100 {
                    asset.exp -= 100;
                    asset.level += 1;
                }

                Ok(vec![TransitionOutput::Modified((asset_id.clone(), asset))])
            },
            ExampleTransitionId::BattleEnemy(enemy) => {
                let asset_id = asset_ids[0];
                let mut asset = AssetHandler::get_asset(&asset_id);

                if enemy.enemy_level > (asset.level + 5) {
                    asset.exp = 0;
                    asset.level -= 1;
                } else {
                    asset.exp += (enemy.enemy_level * 2);

                    if asset.exp >= 100 {
                        asset.exp -= 100;
                        asset.level += 1;
                    }
                }

                Ok(vec![TransitionOutput::Modified((asset_id.clone(), asset))])
            },
        }
    }
}

So the code itself is pretty straightforward, in the AddExperience transition we just increase the exp attribute and if we reached 100 or more we level up the asset and reset its exp; whereas in the BattleEnemy if the enemy_level is greater than our level plus five, we lose our exp and one level, akin to a YOU DIED screen, if your level is in the proper range though, you get exp based on the enemy's level.

Seems unbalanced? It probably is, but the important thing this example aims to highlight is how at no point in the code we had to deal with any blockchain related topic at all! Well... I guess the AccountId is somewhat blockchain related... but anyways!

You may have noticed something a bit scary if you are not familiar with rust's generics. What is PhantomData? And what about AssetManager and AssetInspector?

PhantomData is a way to work around Rust's ownership rules, a way to trick to compiler into thinking we have ownership of some type we actually don't own, for the full explanation take a look here, but in general you don't need to think much about it.

AssetManager is just a generic type that we constrain to one of our utility traits that allow you to unlock the full power of SAGE with minimal knowledge or friction. Let's take a look at some of them:

3.2 SAGE utility traits

pub trait AssetInspector {
    type AccountId: Member + Codec;
    type AssetId: Member + Codec;
    type Asset: Member + Codec;

    fn get_asset(asset_id: &Self::AssetId) -> Result<Self::Asset, DispatchError>;

    fn iter_assets_from(
        account_id: &Self::AccountId,
        ) -> impl Iterator<Item = (Self::AssetId, Self::Asset)>;
}

This is the trait you'll probably use the most, it allows you to retrieve a given asset from it's id or to iterate through all assets owned by a given account.

pub trait AssetManager {
    type AccountId: Member + Codec;
    type AssetId: Member + Codec;
    type Asset: Member + Codec;

    fn ensure_ownership(
        owner: &Self::AccountId,
        asset_id: &Self::AssetId,
        ) -> Result<Self::Asset, DispatchError>;

    // ...
}

A player cannot execute a transition using assets that are not owned by them, but your gameplay may actually contain interactions with other player's assets, so if you need to verify that an asset reference belongs to the player you expect, this trait can come in handy.

pub trait ChainInspector {
	type BlockNumber;

	fn get_current_block_number() -> Self::BlockNumber;
}

Some games make use of cooldowns to control player interaction, in a blockchain the concept of time is different than of a traditional game, so instead of for example waiting x amount of seconds you would wait y amount of blocks.

Of course, given that blocks have an average block time (in Polkadot it is 6s at the time of writing this book), you could somewhat make an equivalence between block number and actual real time. But time in a blockchain is discreet compared to the continuum that time represents in the real world.

In short, for games that make use of time in their gameplay logic, this trait is pivotal for that. One could also make use of the Extra associated type in the SageGameTransition trait for that, but it cannot guarantee the same consistency.

Now, going back to our example game, we already have most of what you would need to actually try running it in a development chain, but there are more features SAGE provides, so let's see how can we integrate them with our example!

3.3 Trading and transferring assets

We've talked about one of blockchain's advantages being that they come with a token already included with them, this token is used for many different operations in the Polkadot ecosystem. With SAGE you can leverage them as currency that your players can use in-game.

One of the main ways is by the trad and transfer of assets between them. This adds a very simple but powerful monetization component to any game with very minimal configuration from the developer. To showcase that let's see how we can add both features to our example game.

3.3.1 Asset filters

The ability to transfer or trade an asset is controlled by the pallet that you use as your runtime core, we'll talk more about that in chapter 5. This guide of course assumes you use the core we provide, which requires you implementing two traits: TradeManager and TransferManager. Let's take a look at them:

// ajuna-pallets/primitives/src/trade_manager.rs

pub trait TradeManager {
    type TradeFilter: Member + Parameter + MaxEncodedLen;
    type Asset: Member + Parameter + MaxEncodedLen;

    fn can_be_traded_using(asset: &Self::Asset, filter: &Self::TradeFilter) -> bool;
}

pub trait TransferManager {
    type TransferFilter: Member + Parameter + MaxEncodedLen;
    type Asset: Member + Parameter + MaxEncodedLen;

    fn can_be_transferred_using(asset: &Self::Asset, filter: &Self::TransferFilter) -> bool;
}

Pretty straightforward right? Each trait has a single method that evaluates to either true or false. Let's see how that would look for our example game:

// lib.rs

#[derive(Debug, Copy, Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo)]
pub struct MyAssetFilter {
    pub min_level: u32,
}

pub struct MyAssetFilterManager;

impl TradeManager for MyAssetFilterManager {
    type TradeFilter = MyAssetFilter;
    type Asset = MyAsset;

    fn can_be_traded_using(asset: &Self::Asset, filter: &Self::TradeFilter) -> bool {
        asset.level > filter.min_level
    }
}

impl TransferManager for MyAssetFilterManager {
    type TradeFilter = MyAssetFilter;
    type Asset = MyAsset;

    fn can_be_transferred_using(asset: &Self::Asset, filter: &Self::TradeFilter) -> bool {
        asset.level > filter.min_level
    }
}

As you can see, the filters are pretty simple. Only assets with level greater than the value set in the filter can be traded or transferred. Of course it's also possible to have something more complex and even use some or more of the utility traits to expand the capabilities of the filter methods. But I'll leave that to you!

3.4 Adding Seasonal Events

The concept of a season does vary between games, some enable or disable maps or modes in each season, while others just use as a way to change the behavior of certain game components like gold dropped by enemies or amount of experience received.

In the SAGE core implementation they are used mainly to control the amount of fees the players have to pay to use different features of the system, like initiating a transition or putting an asset on sale for example.

NOTE: THE SECTION BELOW DESCRIBES BEHAVIOR THAT IS STILL NOT IMPLEMENT IN SAGE TRANSITION TRAIT NOR PALLET

// ajuna-pallets/primitives/src/season_manager.rs

// ...

#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, Clone, Debug, Default, PartialEq)]
pub struct SeasonFeeConfig<Balance> {
	pub transfer_asset: Balance,
	pub buy_asset_min: Balance,
	pub buy_percent: u8,
	pub upgrade_asset_inventory: Balance,
	pub unlock_trade_asset: Balance,
	pub unlock_transfer_asset: Balance,
	pub state_transition_base_fee: Balance,
}

pub trait Validate {
	fn validate(&self) -> bool;
}

#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, Clone, Debug, Default, PartialEq)]
pub struct SeasonConfig<Balance, SeasonData> {
	pub fee: SeasonFeeConfig<Balance>,
	pub data: SeasonData,
}

// ...

The structure of SeasonConfig shows the two components mentioned, fee holds all data regarding the costs of actions in SAGE. Whereas data holds the specific modifiers that you as a developer can define and will be available at each transition call.

3.5 Setting up tournaments between players

Much like seasons, some games allow player to participate in competitions that rank them, in SAGE you also can provide such component, the main difference is that in SAGE players compete not by directly playing against each other, but by comparing their assets in a given way that you can define.

3.5.1 Ranking assets

So, how do you rank assets? Well, in SAGE it's all done with, once again, a simple trait: EntityRanker. Let's take a look:

// ajuna-pallets/primitives/src/season_manager.rs

// ...

pub trait EntityRanker {
	type EntityId;
	type Entity;

	fn can_rank(&self, entity: (&Self::EntityId, &Self::Entity)) -> bool;

	fn rank_against(
		&self,
		entity: (&Self::EntityId, &Self::Entity),
		other: (&Self::EntityId, &Self::Entity),
	) -> Ordering;
}

// ...

Let's try with our example game!

// lib.rs

pub struct MyEntityRanker;

impl EntitryRanker for MyEntityRanker {
    type EntityId = MyAssetId;
    type Entity = MyAsset;

    fn can_rank(&self, entity: (&Self::EntityId, &Self::Entity)) -> bool {
        entity.level > 5
    }

    fn rank_against(
        &self,
        entity: (&Self::EntityId, &Self::Entity),
        other: (&Self::EntityId, &Self::Entity),
    ) -> Ordering {
        entity.1.level.cmp(other.1.level)
    }
}

Done! Our ranker will only allow assets with a level higher than 5 to be ranked, and will consider an asset with a level higher than another to be higher up in ranking!

Of course you may wonder, but how can I have multiple ranker types?

One approach would to have your ranker be an enum so that you can define for each variant a given ranking logic. Alternatively you could instead opt for a parametrized struct that increases the flexibility of your logic.

Unfortunately, Rust with its strict type system, doesn't allow more dynamic behavior than that. That's why if tournaments are going to be a big part of your gameplay, you should spend some time thinking about the structure of your asset ranker.

3.6 Affiliates, how to and why

3.6.1 Unlocking affiliation

3.7 Using assets as NFTs

3.7.1 From asset to NFT and back

4. Runtime Integration

4.1 Handling payments

4.2 Benchmarks and weights

If you are familiar with or have already worked with either or the then this is going to be very simple!

If it's your first time though, don't fret! You don't need to be an expert in either! But some knowledge of Rust development is going to be handy, luckily there's an excellent guide ! If you are strapped for time then chapters 1 to 8 are the most important for you to understand.

Developing with SAGE has the same prerequisites as the . Follow the guide for your operating system and you should have everything you need to start building your first game!

One of the main components is latency, in blockchains any modification to the state of the game needs to be included into a block and then that block incorporated into the chain. That process can take between 6 and 12 seconds in the case of , which means basically, that games requiring very low gameplay latency like for example, a racing game, are not well suited for a blockchain runtime.

: integers and floats, both signed and unsigned, also .

Any struct, enum or union that implements the trait.

But, it's also possible to first define the behavior or gameplay logic, and then define different types of assets for it. To do that you'll need a bit more skill with rust and its generics system, see for inspiration in how you would define your asset as a generic component that your gameplay can reuse.

That's quite a lot of words! Don't worry, we'll go slowly so that everything becomes familiar! If you cannot wait the code for the trait and its associated types is .

There are many traits that SAGE provides to help you enhance your gameplay and interact with the runtime in which it will run. you'll find them. Let's highlight a couple:

But, much like "traditional" games, seasons can also be used to change the behavior of your gameplay logic. To do so take a look at this code which can be found :

The trait gives you two methods can_rank so that you can filter assets that may not fulfill the criteria of your tournaments. And rank_against which basically compares two assets and return and for them.

Rust
Polkadot SDK
here
Polkadot SDK
Polkadot
Scalar types
Strings
Clone
here
here
Here
here
Ordering