First, we need to use Flow Super Commands flow setup to initialize an empty project. By default, it will create the basic folder structure and a flow.json configuration.
Running the command:
> flow setup my-project
The folder structure created is this:
/my-project
|- /cadence
|- /contracts // folder should contain all your Cadence contracts
|- /scripts // folder should contain all your Cadence scripts
|- /transactions // folder should contain all your Cadence transactions
|- /tests // folder should contain all your Cadence tests,
|- flow.json // is a configuration file for your project
|- README.md
Now, you can install Turbine's core library.
Running the command to create a package.json and add core library of Turbine to the project.
pnpminitpnpmadd@turbine-cdc/core
To facilitate future development and usage, you can update the package.json file with the following fields:
Now you have completed the installation of the Turbine Framework core library.
Let's start building an FOC game!
Setup using Turbine scaffold
Working in progress
Recommended structure of the Contracts folder
In order to facilitate project maintenance, we recommend organizing the contracts directory in the following way:
/contracts
|- /components // folder should contain all your component contracts.
|- /system // folder should contain all your system contracts.
|- MyModule.cdc // will import all components and systems for registration.
|- MyPlatform.cdc // (Optional) is used to centrally manage the worlds.
Build
Components in Turbine
Component is the actual carrier of data in Entity. Learn more
All components in the Turbine must implement the IComponent contract interface. You should not implement any business logic in the Component. It should only define data structures or handle data reading and writing.
Here is an example:
import "IComponent"pub contract CounterComponent:IComponent {/// Events// Your Code: Events/// The component implementation/// pub resource Component: IComponent.DataProvider, IComponent.DataSetter {access(all)var enabled: Boolaccess(contract)let kv: {String: AnyStruct}init() { self.enabled =true self.kv = {}/// Your code: setup default key values self.kv["counter"]=0asUInt64 }/// --- General Interface methods ---/// Returns the keys of the component///access(all) fun getKeys(): [String] {return[// Your code: all valid keys"counter"] }/// Sets the value of the key///access(all) fun setData(_ kv: {String: AnyStruct}):Void {// Your code: how to store the dataif kv["counter"]!=nil { self.kv["counter"]= kv["counter"]as!UInt64???panic("Invalid counter") } }/// --- Component Specific methods ---// Your code: read and write methods/// Sets countter///access(all) fun getCounter():UInt64 {return self.kv["counter"]as!UInt64???panic("Invalid counter") }access(all) fun increment(amount:UInt64) {let currentValue = self.getCounter() self.setData({ "counter": currentValue + amount }) } }/// The component factory resource/// pub resource Factory: IComponent.ComponentFactory {/// The create function for the component factory resource/// pub fun create():@Component {return<- create Component() }/// Returns the type of the component/// pub fun instanceType():Type {return Type<@Component>() } }/// The create function for the entity factory resource/// pub fun createFactory():@Factory {return<- create Factory() }}
Systems in Turbine
Turbine was built with a complete separation of data (via components) and logic (the stateless contracts). These stateless pieces of logic are what we call systems in Turbine. Learn more
In the previous Component section, we created a CounterComponent. Now we can create an IncrementSystem.cdc to operate on that component.
Before that, let's introduce a utility library called EntityQuery.cdc.
EntityQuery - the way to query entities from the systems
In the core library, there is a dedicated tool contract for searching entities. It mainly refers to the design of Entity querying in Unity.
The contract contains a Builder Struct, you can perform queries on entities through that.
import "EntityQuery"let query = EntityQuery.Builder()query.withAll(types: [ Type<@CounterComponent.Component>()])// Execute Query requires a world context which will be introduced in the next sectionlet entities = query.executeQuery(world)
Now we can create the IncrementSystem.cdc.
import "Context"import "IWorld"import "ISystem"import "EntityQuery"import "IdentityComponent"pub contract IncrementSystem:ISystem { pub resource System: ISystem.CoreLifecycle, Context.Consumer {access(contract)let worldCap: Capability<&AnyResource{Context.Provider, IWorld.WorldState}>access(contract)var enabled: Boolinit(_world: Capability<&AnyResource{Context.Provider, IWorld.WorldState}> ) { self.worldCap = world self.enabled =true }/// Query the player by username///access(all) fun incrementAll(amount:UInt64) {let query = EntityQuery.Builder() query.withAll(types: [ Type<@CounterComponent.Component>() ])let world = self.borrowWorld()let entities = query.executeQuery(world)for entity in entities {iflet comp = entity.borrowComponent(Type<@CounterComponent.Component>()) {let counter = comp as!&CounterComponent.Component counter.increment(amount: amount) } } }/// System event callback to add the work that your system must perform every frame.///access(all) fun onUpdate(_ dt: UFix64):Void {// NOTHING } }/// The system factory resource/// pub resource SystemFactory: ISystem.SystemFactory {/// Creates a new system/// pub fun create( world: Capability<&AnyResource{Context.Provider, IWorld.WorldState}>):@System {return<- create System(world) }/// Returns the type of the system/// pub fun instanceType():Type {return Type<@System>() } }/// The create function for the system factory resource/// pub fun createFactory():@SystemFactory {return<- create SystemFactory() }}
When calling the incrementAll method of System, it will find all Entities in World that contain CounterComponent and call their increment method to modify the values.
What needs to be noted here is that each System can have an onUpdate method. This method will be synchronously called when the update method of the world is called. If there is a need to implement some logic that changes over time, it can be written inside the onUpdate method.
World in Turbine
World is the context of the game, and the resource contains all entities and systems resources for the game
You don't need to implement another World contract anymore, because Turbine has already provided a standard implementation of World called CoreWorld.
So you need to have a resource of CoreWorld's WorldManager to initialize your game world.
import "CoreWorld"/// Create a new game worldaccess(all)fun createWorld(_ admin: Capability<&AuthAccount>, name:String) {// Borrow the admin accountlet acct = admin.borrow()??panic("Could not borrow admin account")// Fetch or create the world managervar worldMgr: &CoreWorld.WorldManager?=nilif!CoreWorld.hasManager(acct: acct.address) { worldMgr = CoreWorld.createManager(admin: admin) } else { worldMgr = acct.borrow<&CoreWorld.WorldManager>(from: CoreWorld.WorldManagerStoragePath) }assert(worldMgr !=nil, message:"Could not borrow world manager")// Create the world. The second parameter can also accept a list of System's types.// This is used to perform the initialization of System during creation.let world = worldMgr!.create(name, withSystems: [])}
When you call the create method of WorldManager, It will automatically create a World resource Instance in the Admin account and assign its World Capability to the WorldManager for control.
Module: the way to register components and systems to the world
In some of the sections above, I think you have already noticed. In the implementation of CounterComponent and IncrementSystem, there are some Factory methods included. We need to register these Factories into the World in order to conveniently perform operations through the WorldManager at runtime.
Module is a type of contract used to uniformly register them into the World.
import "IComponent"import "ISystem"import "IModule"import "CounterComponent"import "IncrementSystem"pub contract SampleModule:IModule { pub resource Module: IModule.Installer {/// Returns the name of the module///access(all) view fun getName():String {return"SampleModule" }/// Loads the system factories that are provided by the module.///access(all) fun loadSystemFactories(): @[AnyResource{ISystem.SystemFactory}] {let ret: @[AnyResource{ISystem.SystemFactory}] <- [] ret.append(<- IncrementSystem.createFactory())return<- ret }/// Loads the component factories that are provided by the module.///access(all) fun loadComponentFactories(): @[AnyResource{IComponent.ComponentFactory}] {let ret: @[AnyResource{IComponent.ComponentFactory}] <- [] ret.append(<- CounterComponent.createFactory())return<- ret } } pub fun createModule():@Module {return<- create Module() }}
Now we can change the previous createWorld method: installing the SampleModule into the new world through the installModule method provided by WorldManager.
import "CoreWorld"import "SampleModule"/// Create a new game worldaccess(all)fun createWorld(_ admin: Capability<&AuthAccount>, name:String) {// Borrow the admin accountlet acct = admin.borrow()??panic("Could not borrow admin account")// Fetch or create the world managervar worldMgr: &CoreWorld.WorldManager?=nilif!CoreWorld.hasManager(acct: acct.address) { worldMgr = CoreWorld.createManager(admin: admin) } else { worldMgr = acct.borrow<&CoreWorld.WorldManager>(from: CoreWorld.WorldManagerStoragePath) }assert(worldMgr !=nil, message:"Could not borrow world manager")// Create the world. The second parameter can also accept a list of System's types.// This is used to perform the initialization of System during creation.let world = worldMgr!.create(name, withSystems: [])// Install the module to the world worldMgr!.installModule(to: name, <- SampleModule.createModule())}