Architecture solution [DRAFT]

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 and i_player_listener interfaces as you like or not use them at all. Only requirement is to implemet i_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{}
5 Likes

That’s indeed some lovely advanced work! This is the first time I see this kind of approaches in verse. Fairly this is due to verse limits, not being independent from UEFN yet and maps made in UEFN at this time often doesn’t require that much consideration for the architecture. Verse without creative_device and UEFN is useless. All we can do is to wait for verse to be expanded outside of UEFN (eventually implemented in UE5).

Beside that, I do have a question regarding the i_service interface: it does not have any abstract functions in it to be overridden nor inherit or being inherited from another interface and yet you used it in weapon_service. What purpose does this interface provide?

edit: I just actually noticed that i_service could be used to retreat all services using service_locator then you could cast it into whatever service you want (like you did with PlayerService by writing PlayerService := i_player_listener[Service]). I thought about why not using i_initializable instead or merge it with i_service but not all of them needs to be initialized as far as I guess based on what you said in weapon_service part. Every service must have i_service but the same cannot be said with i_initializable

Yeah, you got that right. A little later I plan to add information about the advantages of this approach in production and how it improves development.

1 Like

Looking forward to it! I’m interested in how and why would you need to care about the architecture of your game

Very well described pattern, thx. I was just reading about the service locator and was about to delve into it as I’m not happy with the singleton. I like that it comes down to interfaces and the dependency injection phase happens pretty early on. No problems with lazy initialization or nulls. Perhaps for gamedev it’s a must-have.

1 Like

Later, I will create a project on github for this framework. So that people can have a starting point to develop their own games. I also plan to put the systems I use (they are compatible with this architecture) into the public domain

1 Like