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!
Last updated
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!
Last updated
Welcome to the SAGE book! Through the different chapters you'll learn about how to use SAGE to make your game a reality!
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!
If you are familiar with or have already worked with either Rust or the Polkadot SDK 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 here! 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 Polkadot SDK. Follow the guide for your operating system and you should have everything you need to start building your first game!
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.
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 Polkadot, which means basically, that games requiring very low gameplay latency like for example, a racing game, are not well suited for a blockchain runtime.
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!
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:
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:
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:
Scalar types: integers and floats, both signed and unsigned, also Strings.
Any struct
, enum
or union
that implements the Clone trait.
In 3. SAGE Integration we'll talk in more detail about the specific restrictions on both types.
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.
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 here for inspiration in how you would define your asset as a generic component that your gameplay can reuse.
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!
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
:
With these we can begin the integration!
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:
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 here.
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:
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.
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:
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:
There are many traits that SAGE provides to help you enhance your gameplay and interact with the runtime in which it will run. Here you'll find them. Let's highlight a couple:
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.
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.
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!
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.
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:
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:
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!
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
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 here:
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.
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.
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:
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 Ordering for them.
Let's try with our example game!
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.