This guide is adapted from our Reconstructing Fun video series, where we explore how to build popular game mechanics using Nakama and Hiro. You can watch the full video below and follow our channel on YouTube for the latest updates.
In the world of mobile gaming, collectible card mechanics are fast becoming a key meta gameplay feature, with players across the globe engaged by the progression and sense of achievement this gameplay can provide when implemented well. But developing these intricate systems can often be challenging and time-consuming, requiring a significant investment of time and resources.
In this guide we’ll explore how you can quickly integrate collectible card mechanics using Nakama and Hiro to produce gameplay experiences reminiscent of massive successes like Clash Royale from Supercell.
Once that’s out of the way, you can familiarize yourself with the full project code we’ll be using in this guide by cloning the Collectible Cards repository from GitHub.
Next we define our InitModule function, which is called when the server starts up. Here we’ll initialize the Hiro systems - Economy and Inventory - we’ll be using, and register the RPC functions we’ll be implementing.
funcInitModule(ctxcontext.Context,loggerruntime.Logger,db*sql.DB,nkruntime.NakamaModule,initializerruntime.Initializer)error{props,ok:=ctx.Value(runtime.RUNTIME_CTX_ENV).(map[string]string)if!ok{returnerrors.New("invalid context runtime env")}env,ok:=props["ENV"]if!ok||env==""{returnerrors.New("'ENV' key missing or invalid in env")}hiroLicense,ok:=props["HIRO_LICENSE"]if!ok||hiroLicense==""{returnerrors.New("'HIRO_LICENSE' key missing or invalid in env")}binPath:="hiro.bin"systems,err:=hiro.Init(ctx,logger,nk,initializer,binPath,hiroLicense,hiro.WithEconomySystem(fmt.Sprintf("base-economy-%s.json",env),true),hiro.WithInventorySystem(fmt.Sprintf("base-inventory-%s.json",env),true))iferr!=nil{returnerr}cardDefinitions,err:=NewCollectibleCardDefinition(nk,fmt.Sprintf("collectable-cards-%s.json",env))iferr!=nil{returnerr}iferr=initializer.RegisterRpc("InventoryListCards",InventoryListCardsRpcFn(systems,cardDefinitions));err!=nil{returnerr}iferr=initializer.RegisterRpc("InventoryCardUpgrade",InventoryCardUpgradeRpcFn(systems,cardDefinitions));err!=nil{returnerr}returnnil}
There are two RPC functions we’re implementing as part of this game feature:
InventoryListCards to return a list of cards the player currently owns, and
InventoryCardUpgrade to be used when a player has accumulated enough cards to be able to upgrade a card to the next level.
Next we define the Hiro system definitions we’ll be using to implement the collectible card mechanics. These are defined in the base-inventory-dev1 file and base-economy-dev1 file, respectively.
The Hiro Inventory system enables you to define and manage the items that can be collected and used by players in your game. In this example, we’ll use the Inventory system to define the collectible cards that players can acquire and use in the game, setting attributes like their name, description, maximum count, stackability, and rarity.
Notice that we are defining two collections for each card - cards and cards_common in this case - which we use to group the cards into their respective rarity group (i.e. whether it’s rare, epic, or legendary).
The card_rarity property inside our string_properties field is used to define the rarity of the card, which we’ll use to determine the card’s stats and upgrade costs.
The Hiro Economy system enables you to define and manage the currencies that players can earn and spend in your game, and also define the currencies and amounts that each player begins the game with.
Here we define the currencies and amounts that each player begins the game with, as well as the items that each player begins the game with. In this example, we’ve given each player an amount of every type of card, as well as a large amount of coins and a smaller amount of gems, so that we can illustrate the upgrade process later on.
This file, available here, is where we utilize the Hiro systems we’ve defined to implement the collectible card mechanics.
Here we define card attributes and upgrade costs based on their rarity and rank, outline rarity stat structures with gameplay elements, and determine the cost structure, defining how many cards and coins are needed for upgrades.
The CollectibleCardsDefinition contains the information needed to generate the stats and upgrade costs for the collectible cards.
The NewCollectibleCardDefinition function is used to read the JSON file containing this information - collectable-cards-dev1 here - and unmarshal it into the CollectibleCardsDefinition struct.
This file bootstraps our game with a list of systems to be used, and provides a list of systems for deterministic start-up. In our case, we’re initializing the Inventory and Economy core systems from Hiro, and finally our custom Collectible Card Game system.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ...systems.Add(nakamaSystem);// Add the Inventory systemvarinventorySystem=newInventorySystem(logger,nakamaSystem);systems.Add(inventorySystem);// Add the Economy systemvareconomySystem=newEconomySystem(logger,nakamaSystem,EconomyStoreType.Unspecified);systems.Add(economySystem);// Add the Collectible Card Game systemvarcollectibleCardSystem=newCollectibleCardSystem(logger,nakamaSystem);systems.Add(collectibleCardSystem);returnTask.FromResult(systems);// ...
This file contains the client logic for the Collectible Card Game system, including functions to fetch the current player cards and handle card upgrades.
To start with, we have some public dictionaries to store the card stats and costs, and the player’s current cards:
Then we have two functions to fetch the current player cards and handle card upgrades, each of which calls the corresponding RPC function on the server-side.
// ...publicasyncTaskInitAsync(EconomySystemeconomySystem,CollectibleCardSystemcollectibleCardSystem){_economySystem=economySystem;_collectibleCardSystem=collectibleCardSystem;SystemObserver<EconomySystem>.Create(economySystem,OnEconomySystemChanged);SystemObserver<CollectibleCardSystem>.Create(collectibleCardSystem,OnCollectibleCardSystemChanged);// Fetch first data from collectible card systemawait_collectibleCardSystem.GetCardsAsync();}privatevoidOnEconomySystemChanged(EconomySystemsystem){// Update the coins display_coins=system.Wallet["coins"];UpdateUI();}privatevoidOnCollectibleCardSystemChanged(CollectibleCardSystemsystem){UpdateUI();}privatevoidUpdateUI(){// Update coins displaycoinsLabel.text=_coins.ToString();// Clear the existing card gridforeach(TransformtincardGrid.transform){Destroy(t.gameObject);}// Sort cards by namevarsortedCards=_collectibleCardSystem.Cards.Select(x=>x.Value).OrderBy(x=>x.Name);// Hide the loading spinnerloadingSpinner.SetActive(false);// Add cards to the gridforeach(varcardinsortedCards){varnewCard=Instantiate(cardPrefab,cardGrid.transform);varcollectibleCardUI=newCard.GetComponent<CollectibleCardUI>();collectibleCardUI.Clicked+=()=>OnCardClicked(card);collectibleCardUI.SetCard(card,_cardSprites[card.Name],_coins);}}// ...