Service-Based Game Development

X

Xatio

Guest
GM Version: 2.1.5
Target Platform: All
Download: https://app.box.com/s/l37dzzftkku9ob0iqg0rhe17ox3oxnar
Links: N/A

Summary:
We're going to implement the basis of a service based game engine/system.

Tutorial:
First, I'd like to define what I mean by a service. A service is some part or function of your game that can be thought of as it's own thing. Some examples are User Settings, Physics, or even Rendering.

When we separate parts of the game this way, a few great things happen:
  1. Debugging becomes a lot easier (bugs are always isolated to that specific service)
  2. Services are easy to import into new projects
  3. We have more control over updates (more on this shortly)
Now all of this sounds great, but how do we actually achieve this? Well to start, we're going to create two objects: eEngine & eService.

Next we're going to create a separate room at the top of our room list called eRoom and place eEngine in it.(We place it at the top to ensure that it always runs first).

The Engine (eEngine)
Code:
// eEngine Create Event

#region Define Properties

    // Time passed since the last step
    global.deltaTime    = 0;

    // List of active services
    global.serviceList    = ds_list_create();
    global.serviceCount = 0;
  
    // List of services to be added at runtime
    coreServices        = [eService];

#endregion
#region Create Core Services

    var i;
    var inst;
    var sCount = array_length_1d(coreServices);
    for (i=0; i<sCount; i++) {
        inst = instance_create_depth(0, 0, 0, coreServices[i]);
        inst.persistent = true;
    }

#endregion
Code:
// eEngine Step Event

global.deltaTime    = delta_time;

var i;
for (i=0; i<global.serviceCount; i++) {
    with (global.serviceList[| i]) {
        event_user(0);
    }
}
That's a fair amount of code, let's walk through what's happening.
  • We create a list of all services that currently exist
  • We add all service defined in 'coreServices' (and ensure they don't disappear unless we tell them to)
  • Every step we determine the time since the last step (using delta_time)
  • We then tell each service to update (which is what event_user(0) will do)
Now let's move onto eService!

The Service (eService):

Code:
eService Create Event:

#region Add To Service List

    ds_list_add(global.serviceList, id);
    global.serviceCount++;
  
#endregion
#region Define Properties

    // How many updates should occur per seconds?
    rate        = 30;

#endregion
#region Allow Customization
  
    // event_user(2) is where we will allow changes to the service's rate
    event_user(2);
  
#endregion
#region Define Derivative Properties

    interval    = 1000000  /rate;
    counter        = 0;
    updates        = 0;

#endregion
Code:
eService Cleanup Event:

#region Remove From Service List

    var pos = ds_list_find_index(global.serviceList, id);
    ds_list_delete(global.serviceList, pos);
    global.serviceCount--;
  
#endregion
Code:
eService event_user(0):

#region Catchup Counter (to real time)

    counter += global.deltaTime;
    updates = counter div interval;
  
#endregion
#region Perform Updates

    var i;
    for (i=0; i<updates; i++) {
        event_user(1);  
    }
  
#endregion
#region Remove Completed Updates

    counter %= interval;

#endregion
Code:
eService event_user(1):

show_message("This is a test message");
That's also a lot of code, but fortunately that's all of it! Let's walk through this one too:
  • When created, a service adds itself to the list of active services
  • When destroyed, it removes itself from the list of active services
  • The services rate (30 by default) is the number of updates per second. There are 1,000,000 ms in 1 second, so 1,000,000 / rate = the number of ms between updates
  • Each update we add the time elapsed since the last step and calculate how many updates should have occurred
  • We then perform those updates (the code for which would be written in event_user(1))
So now all you need to do is create with a parent eService. This object will automatically be added to the engine and update at a fixed interval (as long as your update code is stored in event_user(1) ).

In the case of a physics service, it is here that you would call the update event on all of your physics objects. A nice bonus is that if you ever want to disable physics, you only need to go to one place: the service.
 

chance

predictably random
Forum Staff
Moderator
I understand your approach here. It's explained well. But I don't think your suggestions of physics, or rendering, are good uses for it. Control functions for those applications are already embedded nicely within Studio. I don't see a benefit to duplicating them.

But this approach might be useful for other functions that are used or updated less frequently. Curious to see what others think.
 

GMWolf

aka fel666
Yeah I love this approach! I tend to call these "Systems", and pretty much work with them exclusively these days (Mostly because I work with ECS a lot).

Great tutorial! Definitely useful to the community!

Though I think the section explaining the benefits of services could be expanded, for example, that you can turn services on and off, or that you can have different services for different rooms/levels, to set different objectives, etc.
 
N

NeZvers

Guest
What's the point of duplicating delta_time into global variable since it's already global.

And I don't get how this can work:

Code:
coreServices = [eService];
inst = instance_create_depth(0, 0, 0, coreServices);

How this system improve on simply having:
GameMaster (for stuff and global vars) + spawn rest controler objects like...
Camera (for controlling camera)
Controller (for input detection and rebinding)
etc
???
 
Last edited:
X

Xatio

Guest
@chance The reason I mentioned physics is because I tend to implement my own physics for my projects. For games that don't require super accurate physics I may only update it 20-30 times per second, then interpolate positions for the other frames (cutting down on physics calculations drastically). I've also used this setup with chunk loading (as you definitely don't need to be checking for chunk updates 60 times per second).

As for the rendering suggestion, come to think of it I mainly do that to keep it in line with the way I've done everything else, not so much because it's the best way to deal with it (as consistency makes life easier). I'll definitely update the topic (whenever it actually lets me do that).

@GMWolf I'll update that section whenever it lets me do that. For whatever reason it's telling me I can't update it (because it's spam?).

@NeZvers Regarding delta_time, I was under the impression that it was generated on call, not at the beginning of each step. This would mean that each call of delta_time would have a slightly different number, leading to variances. If this is not the case, I'll change that (and it's good to know).

The code you asked about:
Code:
// List of services to be added at runtime
coreServices        = [eService];
Code:
var i;
   var inst;
   var sCount = array_length_1d(coreServices);
   for (i=0; i<sCount; i++) {
       inst = instance_create_depth(0, 0, 0, coreServices[i]);
       inst.persistent = true;
   }
Basically I just created an array called 'coreServices' (which is super easy to change, just add elements).
Then at the end of the engine's setup it goes through that array and creates each of the services in the array (and makes them persistent). It's just easy way to set which services should start running when the game starts (and never get destroyed).

The biggest advantages in my opinion are code clarity, code re-usability, and complete control over how often something is done. Simply using controllers definitely offer you the first two (and is how I use to do things, and it can work quite well). It doesn't offer you control over how often something gets done (you can add this functionality, which is essentially what I've done here).
 
N

NeZvers

Guest
@Xatio
What I meant was that array use seems unusual. I think it should have been:
coreServices[0] = eService;
delta_time shows time in milliseconds, how much time has passed since the last frame and it varies depending how fast your computer works (if you get frame drop delta_time will be increasing). For steady 60fps its 1/60 seconds.
 
Last edited:
X

Xatio

Guest
@NeZvers Oh I know that, I just meant I thought it was calculated when it was called. So the time since the last frame (from the current point in time).
 
Top