Architectural doubts of my game: The role of jme3 objects and how to use them properly


#1

Hello together,

after one year of interruption in game development I am about to restart my work. Before starting however I would like to clarify some questions about software architecture which are unclear to me and led to some confusion/uncertanity in the past.

My goal here is not only to code a game but also to learn about Software architecture in the context of game development.

In my last project I implemented the behaviors of my spatials in scene by using controls almost exclusively (For example: NewtonianControl, DamageControl, ProjectileLauncherControl, PlayerControl …). I did so because I was defining my spatials as game objects what is obviously wrong. However it worked for me at the beginning. I could use the SceneComposer, attach the controls to the spatials and add the behaviors I intended. But at a point of complexity this concept broke down because doing so started feeling like breaking with the control concept. For example the basic requirement of adding new scene content (for example a bullet, nozzle fire etc…) required additional workaround which made me feeling like abusing the Control interface. The reasons were:

  • Controls do not include AssetManager and RootNode by default So scene modification is cumbersome and obviously out of scope of the control interface. You have to attach an AppState as a system object for this and to connect the controls with it or: Take the assetManger/rootNode from the load/onRender method.
    But even worse:
  • Controls are not executed if the spatial is detached from the scene (e.g. vessel in hangar or out of sight). So the game objects cannot be taken away from scene without interrupting the update. ==> Artificial node with an artificial update thread?? No way!
    –> Obviously the game logic had to be decoupled from the spatial update. What about AppStates?

On the other hand when I define AppStates as game objects there are different problems.

  • AppStates are not savable by nature so they are not supposed to be made persistent (loading/saving, sure I know about implements Savable :slight_smile: but it also feels like breaking with the concept by adding additional responsibility)
  • Where do I then store the internal state of the Game object to make it persistent?
  • How do I then load the models build with SceneComposer (containing data like HP/HPMax) and attach them to the correct AppStates (because I cannot attach AppStates to Spatials bcz. they do not implement Control interface by nature).
  • Can/Shall I have savable “extends AbstractControl implements AppState” objects with redundant update loop? Or savable appState delegates?

So I came along defining one singleton SceneAppState/Control observer object managing all dynamic objects (Ships, Bullets, moving turrets etc… equipped with the correct control composition in SceneComposer), one PlayerAppState (with the input) and ContentAppStates for individual non persistent procedural element (like planets, stars etc…).

In order to increase the fun: There shall be nested game objects like Vessels with fully controllable turrets, engines, antennas… Or procedurally generated planetary systems with planets, orbits, moons, with surface tiles and collision etc… To link them with the scene seems to be a challenge.

Do you have any suggestions? Because I think what I really need is something like a cooking reciple for architecture and design decisions like:

  • You want to use XXX then you should implement/extend YYY class/interface.

Thanks in advance…
Regards, Harry


#2

If your project is that big did you bwrote or draft down a concept of game mechanics etc.? Could be helpfull to have it right in front of you. If you know about the usage of Appstates and Controls you may then decide what and how to use it. Sorry i dont feel secure enough with the architecture to tell you do xxx for yyy. But writing it down helps.


#3

Controls do not include AssetManager and RootNode by default

sure, and controls should only modify spatial, nothing more trully.

Controls are not executed if the spatial is detached from the scene

Yes, but i have one where i wrote tricky “onDetach” method, because i required it for physics control that works perfectly and add physics where spatial appears, and remove it when required.

On the other hand when I define AppStates as game objects there are different problems.

Not a bad solution, please remember, you dont need save only scene(you can save as separate serialization file too), and better - you can make own savable object, that you will just attach to scene(for example to rootNode) that will contain all entity informations.

Myself i prefer save not just scene, but my own serialized files and even zip them. So i got entities out of box (when game load i create scene based on entities, not entities based on scene).

But imo it depends what your game will be like. Please note better if scenegraph have only required Spatials, so if your world gonna be big, then you should make entites that can load scene graph spatials for you when need. (for example when player is close) You should save memory and hold only few of informations like location or any other important for entity to know what to load.


#4

A general software development ‘rule’ is to prefer composition over inheritance. In that light, as a concept: controls extend Spatials through “composition over inheritance”. AppStates extend Applications through “composition over inheritance”. Neither of those are “game objects”.

In an MVC-like architecture, your game objects would be the M, Spatials + controls are the VC. App states coordinate it all.

For simple games, it’s possible to treat your spatials like game objects… and certainly all of the JME examples seem to behave that way. In the long run, as you’ve discovered, it’s very limiting.

In the end, your game objects are defined by you. They can be anything. You can write regular Java objects that have what you want or you can use a standard architecture like an “Entity System”. (The common JME entity system framework would be Zay-ES: https://github.com/jMonkeyEngine-Contributions/zay-es/wiki)

App states and controls are then responsible for making the view match the ‘model’ and feeding back user-interaction to your game logic/game objects as appropriate.

If you want to see an example of the opposite end of treating Spatials like game objects you can look at the Zay-ES “Asteroid Panic” example: https://github.com/jMonkeyEngine-Contributions/zay-es/tree/master/examples/AsteroidPanic It implements a simple Asteroids clone using an “Entity System”.

From a conceptual level, it can be useful to think about the case where your game objects and game logic are running on a “server” and the app states and controls must communicate with that “server” to implement the game user interface. It can be a useful way to think about the separation.


#5

Ideally in my opinion one would never save the scene. Scene would be 100% reconstructable from the actual game objects/map information you have. In this way also updating the game visuals wont affect the load/save.


#6

The scene is a representation of data. Just as a website is a representation of a database.

You don’t store the website, you store the data and the website displays it however you decide.

A player has a location, health, weapon, etc. and those pieces of information are saveable. You would then create objects that represent that data visually. (Hint: they have-a spatial, they are not is-a spatial).

An easy way to understand how to keep your code clean is the “has-a” (field) and “is-a” (inheritance) relationship.

A gun is a weapon. A player has-a weapon. A player would not inherit from Weapon because it is not a weapon. So in that context it would have-a (field) weapon. A bullet is-a ammo. A arrow is-a ammo. It would inherit from ammo. A weapon would not inherit Ammo. It has-a ammo so ammo would be a field in Weapon. And so on…


#7

Thank you very much for your answers. This discussion is really helpful for me.

What I take from the answer is that I should leave my wild Control/AppState/GameObject mixture by defining an own structure outside of JME3 by considering e.g. MVC or even ECS pattern. I was always trying to match my dev-approach and process to the JME3 toolchain and class structure in order to make my life easier. What I need is a structured way to do so.

I understand (and like) this point but why has JME3 so extensive mechanisms for loading and saving? It doesn’t need them at all. To skip managing jme3 scene would make dev process more efficient because I do not have to fit my project structure to jme3.

Sure. Composition over inheritance in gaming is the most natural design decision but unil now I only used it on Spatial/Control context.

Really good answer. Now it becomes more clear. I want custom Ms not only but mainly because I need to implement big scales (Fp128 vectors) and FTL flights (for both approaches float32 is not suitable). MVC (with Spatial/Controls == view, AppStates == C and Custom stuff == M) makes sense to me. But there is also ECS as an alternative architectural pattern. What is the difference between both of them? At the moment I have no idea how to make a spacecraft flying around with ECS.

I think I will work myself through the ZayES framework, documentation and example projects for a better understanding of the game architecture. I think the understanding here will come with practice and reading :slight_smile:

Regards,
Harry


#8

That will be a good start.

ECS is the M in your MVC, really.

I was very careful when I named the main entry point “EntityData”… because entities are the data of your game. The logic that manipulates them are ‘systems’ in the “ECS” nomenclature… but the systems you make are up to you.

The neat thing about entities is that ‘inheritance’ (which is kind of a broken idea for game objects) is non-existent. It’s more like “duck typing”. Things are what they are because of the attributes (components) that they have. If it has a position and model info then it’s displayed. If it has a name and position then it’s a label on the map… or whatever. It doesn’t matter whether it’s an Ogre or a Tank or a map-marker. The code that uses these things doesn’t care. It just gathers the “ducks” that it is interested in.

“Duck typing” background: https://en.wikipedia.org/wiki/Duck_typing (though that’s not the best write-up… ‘duck typing’ is kind of a core idea in entity systems.)


#9

So then when the next step would be to specify the use cases (to discuss my understanding of the process):

  1. Start game.
    When starting the game the C-Controller (the GameAppState) is attached to the state manager. They initialize the M = the ECS components with the Systems (M.S = AppStates) and its Components (M.C = immutable data objects).
    There would also be a system called ViewAppState (also M.S block(?)) dealing with spatial/control (=V) generation/attachment/detachment:
    Example:
    SerializableFunction<AssetManager,Spatial> = asm -> asm.loadModel("…") for scene or
    SerializableFunction<AssetManager,Spatial> = asm -> UnivserseGenerator(seed).newInstance(asm); for procedural content
    Each attached factory would fire a show event for the ViewAppState

  2. Update loop: The game is running
    The systems provide updates each frame and perform calculations. If the components were immutable (as it seems to be) the operations can even be performed asynchronously.

  3. Pause/continue:
    The Systems (M.S blocks) are disabled/enabled.

  4. Shutdown
    Detaching the GameAppState will trigger all Systems so shut down. Because all information of the game is located in the GameAppState devastation of game world would be complete.

  5. Save/load
    I would specify a visitor traversing all the components and storing them inside a file/data base. The spatials can be skipped because the information to generate them is already managed by the factories. Only the factories or factory containers have to be stored and reloaded in the components. Loading the views would mean to go through the entities, load the V-Factores triggering showUp events on the ViewAppState.

Regards,
Harry


#10

You do not need to do anything. Zay-ES will auto save the components to database if you use SqlEntityData.

Edit:
Also I recommend this example:


Above example was the starting point for my networked game project based on ECS design.


#11

Hello together.

I have now successfully implemented my first ES-Hello world using Zay-ES.
It seems to work correctly: A model is placed into the scene which disappears after 10 seconds.

The point is that I intend to understand the new concept correctly so please find the code below for review. This is for me to determine wether I ave understood the concept correctly.

Step1: First I have implemented the view component which is mainly a Spatial factory (assetManager -> Spatial)

The view component class:

public class ViewComponent implements EntityComponent {

private volatile Spatial spatial;

public Spatial getView(AssetManager assetManager) {
    Spatial spatialTmp = spatial;
    if (spatialTmp == null) {
        synchronized (this) {
            if (spatialTmp == null) {
                spatialTmp = assetManager.loadModel("Models/Test.j3o");
                this.spatial = spatialTmp;
            }
        }
    }
    return spatialTmp;
}
}

Step2: The view system polls the viewComponents and adds them to the scene. It first resets the scene before adding the new content.

public class ViewState extends AbstractAppState {

private final EntityData entityData;
private final Node viewNode = new Node("viewNode");
private AssetManager assetmanager;
private Node rootNode;

public ViewState(EntityData entityData) {
    this.entityData = entityData;
}

@Override
public void cleanup() {
    rootNode.detachChild(viewNode);
}

@Override
public void update(float tpf) {
    super.update(tpf); //To change body of generated methods, choose Tools | Templates.
    viewNode.detachAllChildren();
    entityData.getEntities(ViewComponent.class).stream()
            .map(e -> e.get(ViewComponent.class))
            .map(vc -> vc.getView(assetmanager))
            .forEach(viewNode::attachChild);
}

@Override
public void initialize(AppStateManager stateManager, Application app) {
    rootNode = ((SimpleApplication) app).getRootNode();
    this.assetmanager = app.getAssetManager();
    rootNode.attachChild(viewNode);
}
}

Final Step: The SimpleApplication initializes the EntityData and the ViewState

public class Main extends SimpleApplication {

private boolean firstRun = true;
private EntityData ed = new DefaultEntityData();
float time = 0F;

@Override
public void simpleInitApp() {
    ViewState viewState = new ViewState(ed);

    EntityId id = ed.createEntity();
    ed.setComponent(id, new Name("Test"));
    ed.setComponent(id, new ViewComponent());
    stateManager.attach(viewState);
}

@Override
public void simpleUpdate(float tpf) {
    time += tpf;
    if (time > 10F) {
        ed.getEntities(ViewComponent.class)
                .stream()
                .map(Entity::getId)
                .forEach(ed::removeEntity);
    }
}

public static void main(String[] args) {
    new Main().start();
}
}

In any case I am pretty sure that ECS architecture is the way for me to go with my new project.
Regards and thank you very much.

Harry


#12

As a general rule, components should not contain spatials. It’s like storing a text entry field in your database. You will see that if you stop doing this you can avoid the synchronization completely.

Create an abstract idea of what a “model info” is… whether just a string or some other description. Then your view state will translate that into a spatial. Maybe it loads it as an asset, maybe it just creates a box or sphere. This has saved me so many dozens of times being able to put stand-in meshes in or debug meshes just be checking for specific info and loading an asset for everything else.

For examples you can look at the asteroid panic demo or the sim-ethereal es demo.

Asteroid Panic: https://github.com/jMonkeyEngine-Contributions/zay-es/tree/master/examples/AsteroidPanic

…though this version still does it the manual way. The SiO2 game base library has some nicer features for more easily doing this sort of entity mapping.

Sim-eth-es:

This one uses SiO2’s entity container which makes it a little more straight-forward:

This example uses a general ObjectType component to decide which assets to load or create.

In a lot of my games/demos, I tend to have a ModelInfo component that takes a String and then I either use that to manually create something (.equals(“box”)) or load an asset using that string.


#13

It seems that I have to get rid of some bad habits bcz I again could not resist to store Spatials inside the components :slight_smile: This was exactly the reason why I started this discussion on this level. My goal is to avoid such bad habits (for me and for others) by defining a clear idea of how a good game architecture has to look like and it seems that I got a lot closer to that goal the last two days.

So the ViewAppState cares alone about spatial generation and attachment while using the components as construction manuals. To take this idea even farther one could even develop a ConsoleOutAppState which displays the components as console output without using jme3 at all. And all of this can be done just by exchanging two AppStates? It sounds too good to be true. Is there a catch on it?


Again back to the spatials:
When talking about loading Spatials I have exactly two use cases:

  1. Load a predefined model with all the bells and whistles or
  2. Generate asset procedurally.

Both cases resulted in a spatial attached to the scene so the output is the same. But both operations need different input: 1. is a string and 2. is a set of parameters/instructions necessary for the constructor/builder (seed, color, math function etc…). This information has to be stored inside at least two different types of ViewComponent to be supplied to the corresponding view factories.

I think I will try it. But I will go step by step. Just loading colorful boxes to scene until I get confident with ECS before again dealing with space battleships with turrets and crew and so on :stuck_out_tongue:

Regards,
Harry


#14

No catch… that’s the way. Or you could create a 2D map view, for example. These things are quite common.

One must be careful with the second approach not to venture too far down the ‘everything is configurable’ path. In the end you might be able to have a string name that refers to a common set of parameters, after all.


#15

This has been a valuable discussion. Everyone who’s about to dive into their first large-scale game project should read it carefully.


#16

Truth.

I’ve even fallen back on some of the stuff I wrote here when writing my “fix the libraries” gamejam game this weekend.


#17

Hello together,

I just realized that communication among the system/event handling in ZayES seems to be very straightforward. I just want to share my understanding with you in order to ensure that my picture is correct:
0. Each system is an instance of an AppState

  1. Each system takes a filtered EntitySet from the complete entity data during initialization/constructor/… Only entities with a relevant set of component types (e.g. position, health, …) are taken into the EntitySet (Analogy: Entity=key, Component=shape of the key, System=the key hole).
  2. By calling applyChanges() the change events on the filtered entity data are polled. Only the changes between tow applyChanges() are taken into the collection. The content is then split into three entity subsets (Added entities, Changed entities and removed entities). Every entity can only belong to maximum one of these three groups. So initialization/update/removal operations can be performed on each entity dependent to which group it belongs.
  3. The System can then perform operations on the entities by writing components. The other systems continue listening to the changes on their specific entity set performing the same operations as described above.

Consequence ==> The systems do no care about each other because they seem to be completely decoupled. They are only reacting to component changes of the entities in their filtered sets. If a system (e.g. health/damage system) is missing no NullPointerException can arise. You will realize it during game play (e.g. damage system not present ==> indestructible objects)

Regards,
Harry


#18

Yep.

It’s one of the core features of an ES.

Note: the added, changed, removed entities are one of the nice things that Zay-ES provides but there are some systems that won’t use them at all… as they just iterate over whatever is current. (For example, a damage system that just iterates over all entities with a Damage component and applies damage might not care about the adds/removes/updates.) Obviously, applyChanges() still needs to be called in these cases.