General Code Structure With Controls

Hi everyone :smile:

As the game grows, new features have to be implemented. One of that is entities which can die or AI.
As for removing entities from the world, there are some differences. One entity may need to be removed when its health reaches zero, another one might want to play a dying animation before being removed. So my question was, how can I achieve a clean system that handles that? Would I have different controls like DestroyOnHealthZero Control which checks the spatials health and removes it if its health reached zero. Or would I have an AppState with a list of spatials to be removed every frame, that way I could do something like:
removalSystem.add(spatial, 1f);
So that I can remove entities with a time delay.
And what is the entities health: Would it be simple a float or int attached as user data to a spatial, or would I have a Health class set as user data which offers some common methods manipulating health data or would I even create a control HealthControl with all the methods needed for manipulating health (seems a little weird for me, because it doesn´t necessarilly describe an entities behaviour, but its properties).

Any tips?

Everything becomes clearer if you stop treating spatials like game objects. They are just the visual expression of some game objects. Then you can look at architectures that don’t require spatials to have fifty different controls… usually just one or two to keep them in sync with the game objects. Or maybe even no controls at all depending. (With an ES approach, I often find I have no controls and only app states, for example.)

Questions become simpler to answer at that point.

As to the dying or being removed. I think the animation control has a listener capability. Remove the spatial or play the animation and remove it when the listener fires.

Alternately, consider that your game object may need multiple states: dying, dead, removed… and let those reflect accordingly in the visuals. Then you can also answer questions like “What happens if the player keeps shooting at the dying mob?” It’s still a game object until it’s gone.

1 Like

Great Advise
It helped me to have better understanding of ES approach .(specially when coming from OOP background).

Ok a lots of stuff to think about :sweat_smile:

First question that came to my mind, how would I go about managing entity behaviour then if not using controls for it or using an entity component framework. You said spatials represent all visual things, so I would decouple an entities graphical representation with its data? But for what would I use controls then - Animation, Movement, …
Any pseudo-code?

With ES I just have a decay system and a decay component which express the time to life if its lesser than zero the decay system just removes that entity. I used this for exaple for bullets but also for explosions. Its pretty cool as you can handle many different stuff to disapear from the game.
But I suppose you dont have an entity system based game somy advice is not usefull for your game I fear. just wanted to show how simple life could be :wink: but separating data from representation helps a lot.

Yes. Yes… sort of. “Movement” is often just a reflection of the game object. So you might have a control that’s only job is to make the view match the game object… which would include position, rotation, and perhaps what animation is currently running. These things all fit the same role as the standard parts of MVC… with JME’s control acting like the C in MVC.

Looking at it another way, you should be able to project your game objects into any different visualization… whether it be a 3D scene or a 2D map. The game objects should provide only that much information. Everything else if ‘view’… whether its the model being used or the icon in the 2D interactive map.

No.

I moved on to using entity systems a long time ago (and wrote a pretty good open source one by most accounts)… and all of my games are ES based these days. I don’t have any ready-to-mind tips other than MVC style separation. In fact, I have trouble describing the non-ES way without making it sound really bad… though many people have lots of success without an ES and I used to do it all the time. Having seen the light, I’m forever tainted, though.

Thats definitely a useful tip thanks :slight_smile:

I think I get it, and now I understand why it can be so benefitial to to decouple visual representation with game logic, that way I could theoretically even switch the whole graphics engine that is being used for rendering, because my game data and logic is independent from the visualization. And the controls act like a bridge between the model and the view (like u said MVC), so they keep the data in sync with the visualization. Wow lots of things became clear now :wink:

I think my problem is that I mostly understand the theoretical part but I am always unsure which way to go in code. Everything comes with a drawback, some things having a bigger one than others but I find it hard to evaluate what´s better.

Nevertheless, for that project it´s unfortunately too late to use your entity component system (which I am really looking forward to trying out). I would simply have to rewrite too much I guess. But I try to make the best out of the existing code so I´ll post one of my problems.

Lets say I have first person player in my game who can move and look around as well as drop items, pick up items, attack certain objects with the items, like chopping wood with an axe, mining rocks with a pickaxe or attacking animals with a sword.

Looking at the MonkeyZone example, I really like the idea of having a generic MovementControl which offers some common methods for movement (steerX(), moveX(), …). So for the player I would create a FirstPersonMovementControl which implements the above control and manages movement via a BetterCharacterControl. Now there are two possible ways:
Should I create a FirstPersonInputControl which has a reference to the FirstPersonMovementControl and listens for key and mouse input in order to control the players movement.

public class FirstPersonInputControl extends AbstractControl implements ActionListener, AnalogListener {

    private final String MOVE_LEFT          = "FPS_Move_Left";
    private final String MOVE_RIGHT         = "FPS_Move_Right";
    private final String MOVE_BACKWARD      = "FPS_Move_Backward";
    private final String MOVE_FORWARD       = "FPS_Move_Forward";
    private final String TOGGLE_RUNNING     = "FPS_Toggle_Running";
    private final String JUMP               = "FPS_Jump";
    private final String DUCK               = "FPS_Duck";
    private final String ROTATE_LEFT        = "FPS_Rotate_Left";
    private final String ROTATE_RIGHT       = "FPS_Rotate_Right";
    private final String LOOK_UP            = "FPS_Look_Up";
    private final String LOOK_DOWN          = "FPS_Look_Down";
    
    private InputManager inputManager;
    private FirstPersonControl firstPersonControl;
    
    //Default Movement Keys
    private int left            = KeyInput.KEY_A;
    private int right           = KeyInput.KEY_D;
    private int backward        = KeyInput.KEY_S;
    private int forward         = KeyInput.KEY_W;
    private int toggleRunning   = KeyInput.KEY_R;
    private int jump            = KeyInput.KEY_SPACE;
    private int duck            = KeyInput.KEY_LSHIFT;
    private int rotateLeft      = MouseInput.AXIS_X;
    private int rotateRight     = MouseInput.AXIS_X;
    private int lookUp          = MouseInput.AXIS_Y;
    private int lookDown        = MouseInput.AXIS_Y;
    
    /**
     * Creates a new FirstPersonControl with default key mappings (W, A, S, D, etc.).
     * @param inputManager 
     */
    public FirstPersonInputControl(InputManager inputManager) {
        this.inputManager = inputManager;
        
        initKeyMappings();
        listenForKeys();
    }
    
    /**
     * Creates a new FirstPersonControl with the specified Keys for controlling movement.
     * @param inputManager
     * @param left
     * @param right
     * @param backward
     * @param forward
     * @param toggleRunning
     * @param jump
     * @param duck
     * @param rotateLeft
     * @param rotateRight
     * @param lookUp
     * @param lookDown 
     */
    public FirstPersonInputControl(InputManager inputManager, int left, int right, int backward, int forward, int toggleRunning, int jump, int duck, int rotateLeft, int rotateRight, int lookUp, int lookDown) {
        this.inputManager = inputManager;
        
        this.left = left;
        this.right = right;
        this.backward = backward;
        this.forward = forward;
        this.toggleRunning = toggleRunning;
        this.jump = jump;
        this.duck = duck;
        this.rotateLeft = rotateLeft;
        this.rotateRight = rotateRight;
        this.lookUp = lookUp;
        this.lookDown = lookDown;
        
        initKeyMappings();
        listenForKeys();
    }
    
    private void initKeyMappings() {
        inputManager.addMapping(MOVE_LEFT, new KeyTrigger(left));
        inputManager.addMapping(MOVE_RIGHT, new KeyTrigger(right));
        inputManager.addMapping(MOVE_BACKWARD, new KeyTrigger(backward));
        inputManager.addMapping(MOVE_FORWARD, new KeyTrigger(forward));
        inputManager.addMapping(TOGGLE_RUNNING, new KeyTrigger(toggleRunning));
        inputManager.addMapping(JUMP, new KeyTrigger(jump));
        inputManager.addMapping(DUCK, new KeyTrigger(duck));
        inputManager.addMapping(ROTATE_LEFT, new MouseAxisTrigger(rotateLeft, false));
        inputManager.addMapping(ROTATE_RIGHT, new MouseAxisTrigger(rotateRight, true));
        inputManager.addMapping(LOOK_DOWN, new MouseAxisTrigger(lookDown, false));
        inputManager.addMapping(LOOK_UP, new MouseAxisTrigger(lookUp, true));
    }
    
    private void listenForKeys() {
        inputManager.addListener(this, MOVE_LEFT,
                                       MOVE_RIGHT,
                                       MOVE_FORWARD,
                                       MOVE_FORWARD,
                                       TOGGLE_RUNNING,
                                       JUMP,
                                       DUCK,
                                       ROTATE_LEFT,
                                       ROTATE_RIGHT,
                                       LOOK_DOWN,
                                       LOOK_UP);   
    }
    
    @Override
    public void setSpatial(Spatial spatial) {
        super.setSpatial(spatial);
        
        if(spatial == null) return;
        
        this.firstPersonControl = spatial.getControl(FirstPersonControl.class);
        if(firstPersonControl == null) {
            throw new IllegalStateException("Cannot add FirstPersonInputControl to a Spatial without a FirstPersonControl!");
        }
    }
    
    @Override
    public void setEnabled(boolean enabled) {
        super.setEnabled(enabled);
        
        if(enabled) {
            //Listen for keys
            listenForKeys();
        } else {
            //Stop listening for keys
            inputManager.removeListener(this);
        }
    }
    
    @Override
    protected void controlUpdate(float tpf) {

    }

    @Override
    protected void controlRender(RenderManager rm, ViewPort vp) { }

    public void onAction(String name, boolean isPressed, float tpf) {
        if(name.equals(MOVE_LEFT)){
            firstPersonControl.setMovingLeft(isPressed);
        }
        if(name.equals(MOVE_RIGHT)){ 
            firstPersonControl.setMovingRight(isPressed);
        }
        if(name.equals(MOVE_FORWARD)){
            firstPersonControl.setMovingForward(isPressed);
        }
        if(name.equals(MOVE_BACKWARD)){
            firstPersonControl.setMovingBackward(isPressed);
        }
        if(name.equals(JUMP) && isPressed){           
            firstPersonControl.jump();
        }
        if(name.equals(DUCK)){
            firstPersonControl.setDucking(isPressed);
        }
        if(name.equals(TOGGLE_RUNNING) && isPressed){
            firstPersonControl.toggleRunning();
        }
    }

    public void onAnalog(String name, float value, float tpf) {
        if(name.equals(ROTATE_LEFT)){
            firstPersonControl.rotateBody(-value, tpf);
        }
        if(name.equals(ROTATE_RIGHT)){ 
            firstPersonControl.rotateBody(value, tpf);
        }
        if(name.equals(LOOK_UP)){
            firstPersonControl.lookUpDownHead(value, tpf);
        }
        if(name.equals(LOOK_DOWN)){ 
            firstPersonControl.lookUpDownHead(-value, tpf);
        }
    } 

What I really like about that way is that it can be reused a lot. I just have to attach a FirstPersonInputControl every time and finished. No need for writting game dependent code, first person movement is always the same.

Or would I create an AppState, say PlayerInputState, which listens for input and then calls the FirstPersonMovementControls methods. That way input would be in a central place and it´s easier to deactivate/activate player input all at once. At the moment I have both ways in code (one is used), but I am not sure about this.

For the second question: Lets say the player can hold an item in his hands (like in minecraft ;)), would I use a control here or an AppState. In my opinion, it´s a property or a behaviour of the player so a control would make more sense. This control would then have a way of setting the current item in the players hand and animate it when the player is walking. So it would only be responsible for the graphic visualization. AppState or control and why?

Finally, I have a question about graphical representation of inventories, but as this post got quite long I´ll post it later :slight_smile:

Maybe I´ll see that light too one day :slight_smile: