This is an initial version of a post that will be fleshed out over time, especially if I know someone is interested. I will also be very happy to get your feedback, hopefully we can come up with a convenient solution for writing verse code together. Also later I will add a github repository that will contain a basic setup that can be used to write any project.
What the framework is designed for ?
This decision answers a lot of questions at once:
- A single point of entry
- Life cycle of the entire application and individual components
- Initialization order
- Dependency management
And overall it gives a cumulative idea of how to make games by spending time on features rather than architecture. This idea itself was not invented by us
and is typical of game dev.
Me and @marceposhka just showed our vision of how it can be adapted to the current UEFN realities.
There are many things we would like to do differently, but at the moment not all of the verse programming language features are available in uefn.
How does it work?
Framework consists of several key components:
- Service
- Main Device (Entry point)
- Service Locator
- Service Installer
- World Accessor
Service
The main top-level unit in the framework is Service. It is an independent system that lives from the start to the end of the game.
To create a service, you need to create class and implement the i_service
interface
To provide the service with additional functionality, there are several additional interfaces that can be implemented
List of available interfaces for implementation:
i_service := interface:
i_initializable := interface:
Initialize()<suspends> : void
i_player_listener := interface:
AddPlayer(Agent : agent) : void
RemovePlayer(Agent : agent) : void
i_character_listener := interface:
OnCharacterAdded(Character : fort_character) : void
Let’s consider this on the example of a service that manages player’s weapons:
weapon_service := class(i_service):
Yeah, it’s that simple. All we have to do is give our service the ability to grant weapons to players
To do this, we need to select one of several framework callbacks:
Initialize() : void
- called once during the start of the game server. (Alterntative to OnBegin() method from creative_device)
AddPlayer(Agent : agent) : void
- called once when a new player joins the game.
RemovePlayer(Agent : agent) : void
- called once when a player leaves the game.
For example, if we want some action to be performed at the start of the game (before the first player enters it), then OnInitialized is suitable.
To realize it we need to implement the i_initializable
interface.
weapon_service := class(i_service, i_initializable):
Initialize<override>() : void =
Print("Weapon service initialized")
You can combine
i_initializable
andi_player_listener
interfaces as you like or not use them at all. Only requirement is to implemeti_service
The next step we want to replace prints with granting weapon using item_granter_device. Since service are not a descendant of creative_device
(so we can’t directly use @editable attribute),
we need to define a way to get an object from the scene. We use configs for that. This is a class that serves only to aggregate data from the scene and does
not carry any functionality
weapon_service_config := class(creative_device):
@editable
WeaponGranter : item_granter_device = item_granter_device{}
weapon_service := class(i_service, i_initializable, i_player_listener):
Config : weapon_service_config
Initialize<override>() : void = #callback from ``i_initializable`` interface
Print("Weapon service initialized")
AddPlayer<override>(Agent : agent) : void = #callback from ``i_player_listener`` interface
Config.WeaponGranter.GrantItem(Agent)
RemovePlayer<override>(Agent : agent) : void = #callback from ``i_player_listener`` interface
Print("Player left")
All we have to do is add weapon_service
to service_installer
and weapon_service_config
to world_accessor_device
.
#TODO Rerefrence to service installer and world accessor
Main Device
main_device
serves as the only entry point to the application and also manages the life cycle of each of the services,
notifying them about such events as: initialization, player’s entry/exit from the game (more such events can be implemented in the future).
Source code for main_device
:
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /Fortnite.com/Characters }
main_device := class(creative_device):
@editable
AmountOfPlayers : int = 4
@editable
WorldAccessor : world_accessor_device = world_accessor_device{}
ServiceLocator<private> : service_locator = service_locator{}
var InitializedPlayers<private> : [agent]logic = map{}
OnBegin<override>()<suspends> : void =
CreateServices()
InitializeServices()
SubscribeToPlayerEvents()
CreateServices<private>()<suspends> : void =
ServiceInstaller := service_installer:
World := WorldAccessor
ServiceLocator := ServiceLocator
AmountOfPlayers := AmountOfPlayers
Playspace := GetPlayspace()
ServiceInstaller.Install()
InitializeServices<private>()<suspends> : void =
for (Service : ServiceLocator.GetAllServices[], Initializable := i_initializable[Service]):
Initializable.Initialize()
SubscribeToPlayerEvents<private>() : void =
Playspace := GetPlayspace()
AllPlayers := Playspace.GetPlayers()
for (Player : AllPlayers):
OnPlayerAdded(Player)
Playspace.PlayerAddedEvent().Subscribe(OnPlayerAdded)
Playspace.PlayerRemovedEvent().Subscribe(OnPlayerRemoved)
OnPlayerAdded<private>(Player : player) : void =
if (InitializedPlayers[Player]?):
return
for (Service : ServiceLocator.GetAllServices[], PlayerService := i_player_listener[Service]):
PlayerService.AddPlayer(Player)
if (set InitializedPlayers[Player] = true){}
spawn:
WaitForCharacter(Player)
WaitForCharacter<private>(Player : player)<suspends> : void =
Playspace := GetPlayspace()
race:
loop:
P := Playspace.PlayerRemovedEvent().Await()
if (P = Player):
break
loop:
Sleep(0.0)
if (Character := Player.GetFortCharacter[]):
for (Service : ServiceLocator.GetAllServices[], CharacterService := i_character_listener[Service]):
CharacterService.OnCharacterAdded(Character)
break
OnPlayerRemoved<private>(Player : player) : void =
if (not InitializedPlayers[Player]):
return
for (Service : ServiceLocator.GetAllServices[], PlayerService := i_player_listener[Service]):
PlayerService.RemovePlayer(Player)
if (set InitializedPlayers[Player] = true){}
Service Installer
This is where all services in the game are created, the initialization order is settuped and dependencies are injected
Example of service_installer
:
service_installer := class:
World : world_accessor_device
ServiceLocator : service_locator
AmountOfPlayers : int
Playspace : fort_playspace
Install<public>():void=
GameBalance := game_balance{} #Creating game_balance (not a service, just a database class)
ResourceService := resource_service{} #creating resource_service
ServiceLocator.AddService(ResourceService, "ResourceService") #resource_service registration
HudUIService := hud_ui_service: #creating hud_ui_service with 2 dependencies
GameBalance := GameBalance
ResourceService := ResourceService
ServiceLocator.AddService(HudUIService, "HudUIService")# hud_ui_service registration
Service Locator
Service locator is a container class, which is necessary to add created services to one storage for further use by the framework
Source code:
service_locator := class:
var Services<private> : []tuple(i_service, string) = array{}
AddService<public>(Service : i_service, Name : string):void =
for (S : Services, S(1) = Name):
Print("Error: Service already registered '{Name}'")
return
set Services += array{(Service, Name)}
GetService<public>(Name : string)<decides><transacts>:i_service=
var MaybeService : ?i_service = false
for (Service : Services, Service(1) = Name):
set MaybeService = option{Service(0)}
return MaybeService?
GetAllServices<public>()<decides><transacts>:[]i_service=
for (Service : Services):
Service(0)
World Accessor
world_accessor_device
class which is only used to link objects from the scene so you can use them in your code.
It mainly stores the configs of services, which in turn store settings and links to objects from the scene too
Example code:
world_accessor_device := class(creative_device):
@editable
PlayerBaseServiceConfig : player_base_service_config = player_base_service_config{}
@editable
NotificationServiceConfig : notification_service_config = notification_service_config{}
@editable
LobbyConfig : lobby_config = lobby_config{}