Entitas icon indicating copy to clipboard operation
Entitas copied to clipboard

Services streamlining

Open sschmid opened this issue 6 years ago • 14 comments

Follow up issue to move the discussion from the chat to here.

I usually have two motivations to create a new separate class which we all seemed to agree to call Services now.

Motivation 1. Delay decisions

Sometimes decisions can be delayed, e.g. Networking, Asset Loading, File Storage, etc. Using a service let's you continue with the fun stuff and implement the nasty bits later, e.g. a ViewService can use Resources.Load() but the ViewService can later be updated to use proper Asset Bundle loading without having to refactor systems.

Motivation 2. Multiple systems use same / similar logic

Example:

  • You create SystemA with logic in Execute()
  • You create SystemB with some duplicated code from SystemA
  • You refactor your code to remove the duplicated code

Where to put this code? -> Services There are multiple different ways with pros and cons:

a) Move the duplicate code to a static class Service.cs with a static method DoSth() and call it from SystemA and SystemB Service.DoSth()

Pros:

  • very simple

Cons:

  • We cannot mock and potentially cannot test the code anymore (tight coupling)
  • Utility functions must not have any state
  • Some service might require some initialization in order to work (e.g. RandomService.Initialize(seed))

Conclusion:

Don't do this

b) Don't use a static class and pass the service via ctor arg into the system

Pros:

  • explicit

Cons:

  • Too much work, constant refactoring and service passing
  • Poor manual DI
  • Potentially large ctors
  • not fun at all :)

Conclusion:

Explicit, works, proceed at own risk if you love refactings :)

c) Don't use a static class but a static getter Service.singleton

Pros:

  • very simple
  • can be mocked and unit tested
  • can be swapped with alternative impl (not as easy as a proper DI solution)
  • explicit dependencies are easy to spot at the top of systems
  • Kind of halfway DI
public sealed class SystemA : ReactiveSystem<GameEntity> {

    public Service service = Service.singleton;

    // ...
}

Cons:

  • People tend to leave your team when they see *.singleton :)

Conclusion

Simple, testable, works, not much overhead in development, but not very elegant

d) Add a ServiceComponent and use contexts.services.randomService.value.Int()

Pros:

  • can be mocked and unit tested
  • can be swapped with alternative impl

Cons

  • Moving system logic to components as part of refactoring feels like a bad idea
  • implicit, dependencies are harder to spot
  • more components (unnecessary?)
  • miss-use of components?

e) Traditional automated DI, sth like

public sealed class SystemA : ReactiveSystem<GameEntity> {

    [Inject]
    public Service service;

    // ...
}

Pros:

  • same as c)

Cons:

  • needs to be implement, makes sense if we all agree on it
  • Magic

Conclusion:

Looks ok to me. There are tons of DI Framework already, create a new one tailored for Entitas? Can we avoid manually wiring and use CodeGeneration to setup defaults?

f) CodeGeneration

Need to think more about this, maybe you guys have ideas. Flag Service classes wit [Service]? Have an IService interface? Conventions based on naming? What should be generated? contexts.randomService.Int()?

Pros?

???

  • potentially a seamless integration to the Entitas CodeGeneration experience

Cons:

???

  • implicit, dependencies are harder to spot

You can checkout Match One. It's using approach c) atm.

Do you guys have more ideas / suggestion how to streamline service? Do you see more pros / cons. Do you have a favorit? Do you have a better solution?

Cheers :)

sschmid avatar Feb 13 '18 20:02 sschmid

My favored option is services as Interfaces existing as generated fields in the Contexts class.

1: Define your service interface e.g.

[Service] // optional argument for service name?
public interface ITimeService {
    float deltaTime {get;}
    float fixedDeltaTime {get;}
    bool isPaused {get; set;}
}

2: Generator adds field Contexts.timeService with a nice error message if it is accessed when it is null (e.g. "you forgot to provide an implementation of TimeService before you accessed it").

3: In GameController.cs (or whatever your application start point is)

_contexts = Contexts.sharedInstance; // or new Contexts(), however you first setup contexts
_contexts.timeService = new UnityTimeService(); // concrete implementation of ITimeService

Now from any system we can get _contexts.timeService.deltaTime etc.

Pros: It's basically your fourth option above, but without involving components - so you eliminate 3 of the 4 cons straight away. No need to generate anything more than the fields in the contexts class - all the methods are part of the original interface decleration. I suppose they wouldn't even have to be interfaces, if you wanted to do it with concreate classes that would work in the same way.

The way I currently work is to store directly onto a component, but i would prefer it to be in the contexts class if there was code-generation support for that.

FNGgames avatar Feb 14 '18 09:02 FNGgames

+1 for FNGgames solution. Would maybe initialize the service like the unique components. Furthermore should it be possible to flag it with a context to provide specific view and/or core services.

[View, Service]
public interface ITimeService {
    float deltaTime {get;}
}
<...>
_contexts = Contexts.sharedInstance;
_contexts.view.SetTimeService(new UnityTimeService());

ghost avatar Feb 14 '18 10:02 ghost

To add some cons:

  • implicit, dependencies are harder to spot
  • dependency to Contexts

Example: I have a generic Button that uses a InputService when clicked. The Button is not aware of Entitas. The InputServices emits entities. With your suggestion I'd need to introduce a dependency to the context in order to get the service.

sschmid avatar Feb 14 '18 11:02 sschmid

Not sure if i understand these cons Simon, can we discuss further? I think my architecture is slightly different to yours, or I'm following different mental rules.

I don't have any entitas dependencies in my services. IInputService should define a set of inputs your game wants to use (e.g. left / right stick vectors, four face buttons as bools, dpad buttons, shoulder buttons as bools and triggers as floats).

Now in entitas code we might want an EmitInputSystem that actually queries these values, so say we want to know if user is pressing a ButtonA. In the system we're calling _contexts.inputService.buttonA to query the state of the button. In the actual input service, all that's happening is we're saying buttonA { get { return Input.GetKey(KeyCode.A); } }.

I do not create entities or add components in services - my services don't know entitas exists. The service is just a way for you to get data into or out of your entitas code.

FNGgames avatar Feb 14 '18 12:02 FNGgames

The way i think of services is like a portal to the exterior world - my game code is a walled garden - it should never be concerned with what's happening on the outside. It doesn't care about the engine it's being run on, it doesn't care about the input device, or clock source, or operating system or whatever.

Obviously sometimes it needs user input from the outside - it shouldn't care how it gets it, but instead just talk through interfaces ("I don't care how, just tell me if button A is being pressed" - of course button A could be the keyboard key A, could be a face button on a controller, or it could be an AI with no physical controller at all).

The service, on the other hand, can't see into the walled garden, except that it knows what interface it's supposed to implement. Just as my entitas code will never need using UnityEngine, my service code should never need using Entitas.

FNGgames avatar Feb 14 '18 12:02 FNGgames

@FNGgames That sounds very nice! I have services like entityService.CreatePlayer() -/.CreateEnemy() how would you call this?

sschmid avatar Feb 14 '18 17:02 sschmid

@FNGgames interesting concept. We are using services to share some logic like heavy/common calculations or factories in different systems. To decouple unity from entitas we are using an action context to communicate from unity to entitas and a listener concept (similar to simons one from v1.1) to communicate back. I would call your idea more like a fascade/provider.

ghost avatar Feb 14 '18 17:02 ghost

Yeah, i'm probably wrong about the terminology - this is what I call "services" but I'm sure that it's not formally correct to call them that.

@sschmid closest example for me is my view service that has things like IViewController LoadAsset(string name).

LoadAssetSystem (RS - Matcher: Asset, Filter: Asset && !View) looks like this:

IViewController view = _viewService.LoadAsset(e.asset.name); // UnityViewService does prefab loading
view.Initialize(e, contexts); // method from IViewController interface
e.AddView(view);
e.isAssetLoaded = true;

All my entity creation code - (CreatePlayer / CreateEnemy etc) goes in monobehaviours that live on the prefab. These implement IConfigureEntity and in the view controller Initialize method I do a var configs = GetComponentsInChildren<IConfigureEntity>(); and call config.Configure(GameEntity e, Contexts c) for each.

So for configuration i write specific monobehaviours like PlayerConfig which have classic Unity editor fields to play with. I have a folder full of these scripts. This is where I add components to the entities to make them into a player or an NPC or a bullet or whatever.

I guess I'm leaving configuration to the outside world, which is maybe stupid - but i like having config all done in unity and saved as prefabs for right now - it's very convenient and designer friendly.

FNGgames avatar Feb 15 '18 09:02 FNGgames

If you actually wanted contexts dependencies in your service though - why not just have an IService interface with an Initialize(Contexts c) method? then we pass it during construction and it's nice and explicit.

FNGgames avatar Feb 15 '18 09:02 FNGgames

I don't like the idea about configuration via mono behaviours directly on the prefab if they are gameplay relevant. All data on the prefab should be "state" (more like view state) of that particual object and how it is represented in the view. Configuration should be in ScriptableObjects or in json (or some other text format) in your project folder. To have all those data in your project folder is imo even more designer friendly, because it's a clear statement: Balance values goes to project folder and representation goes to prefab and can be done in the scene. It also helps with changing balance values really fast and swap different configurations (our designer can load the balance values into the project with excel - they are saved as jsons. Designers love excel import)

ghost avatar Feb 15 '18 09:02 ghost

@StormRene sure, i wasn't expecting my configuration solution to be the best option, but I can prototype with it really really quickly - I will probably end up moving it all to a proper serialized solution in the future.

My point as relevant to this issue is that the entity creation (and other things that rely on contexts reference) can easily be kept within systems code, then entities can be passed out of the systems code for services to do stuff to them. No need to have services dependant on Contexts or w/e

FNGgames avatar Feb 15 '18 10:02 FNGgames

b) Don't use a static class and pass the service via ctor arg into the system

For option b), one can use dependency injection library like Zenject to make things much easier. Zenject is probably too bulky to be a dependency of Entitas, but the users can choose to use them together without problems. Or Entitas can implement its own lightweight DI library. But IMO Zenject is popular and stable enough and we can just let users decide and not reinvent wheels.

yhslai avatar Mar 04 '18 06:03 yhslai

my solution is used one init system to setup all service.

void IInitializeSystem.Initialize()
    {
        AccountService.Init();
        ServerService.Init();
        LoaderService.Init();
        NetService.Init();
        PlayerService.Init();
        UIService.Init();
        AudioService.Init();
        MapService.Init();
        .... and many more..
    }

when to use one service just need to Service.Get<T>()

chiuan avatar Mar 04 '18 07:03 chiuan

Currently using this

partial class Contexts
{
    public Custom.Sources.Services Services;
}

namespace Custom.Sources
{
    public class Services
    {
        public IGameEntService GameEnt;
        public ICmdEntService CmdEnt;
        public IConfigService Config;
    }
}
// In GameController.cs
// _contexts.Services = new Services
// {
//     GameEnt = new GameEntService,
//     CmdEnt = new CmdEntService,
//     Config = new ConfigService,
// };

c0ffeeartc avatar Jul 05 '18 23:07 c0ffeeartc