[SOLVED] Sim-eth-es architecture for an attack system

Hi

I’m trying to implement an attack system into sim-eth-es and wondering how to go about this. I’m not looking for actual code examples, I’d like to try to figure that out myself. I am however, wondering about the architecture:

I figure it would be reasonable to implement, much in the same manner as shipdriver, a shipattacker, to keep track of each players attacks (could be cooldowns, buffs and what-nots). However, I am stumped trying to figure out how the update-method of shipdriver fits into the mechanisms of the network systems. I can see that’s the method that actually does stuff, so my own new system would have a different signature and would be called by a different kind of service. Any idea how you would go about doing this or just sketchy thoughts?

PlayerMovementState.java

@Override
public void valueChanged( FunctionId func, InputState value, double tpf ) {

    // Change the speed based on the current run mode
    // Another option would have been to use the value
    // directly:
    //    speed = 3 + value.asNumber() * 5
    //...but I felt it was slightly less clear here.   
    boolean b = value == InputState.Positive;
            
    if( func == PlayerMovementFunctions.F_BOOST ) {
        if( b ) {
            speed = 10;
        } else {
            speed = 3;
        }
    } else if (func == ShipFunctions.F_SHOOT) {
        if (b) {
            session.shoot(spatial.getLocalRotation(), spatial.getLocalTranslation());
        }
    }
}

I figure the only thing to send from the client, in the controlstate, is a ‘shoot’-command to the server session, sending the position and rotation of the player. Then the server (to avoid cheating) would apply buffs/debuffs/cooldowns etc that may be in effect (or even block the attack entirely if no attack is possible) and create the attack.The attack would generate a (potentially projectile) entity (carrying information on player-origin, model, damage, velocity, rotation, position) that defines the attack.

GameSessionHostedService.java

@Override
public void shoot( Quaternion rotation, Vector3f position ){
    if (log.isTraceEnabled()) {
        log.trace("shoot(" + rotation+"," + position +")");
    }
    
    //Forward to game world
    shipAttacker.applyAttack(rotation, position);
}

Any thoughts are appreciated :slight_smile:

It depends on what “attack” means in this case. Is it insta-damage or is a missile generated and fired?

In the simple case, I guess the ‘attack’ would create an entity that links the shooter to the target. A system would watch for these entities and decide if the source can really shoot or not then apply a damage entity to the target. (A separate damage system decides how to apply damage to damage targets.)

…but the devil is in the details. The key is to break it down into reasonable steps. The above is just the way that’s more flexible for switching to different kinds of weapons or attack types.

I’m not sure I follow why the two approaches are that much different, but in any case - my game requires fire and forget kind of projectiles (like bullets/missiles).

I think I have a fair understanding of how to incorporate this using Zay-ES (much as in asteorid panic, I can understand how a state can be initialized with the EntityData and from that find the information, write to a component etc). The difficult part for me, also after reading the post you did on Sim-eth-es explaining how the bodyposition-system was a sneaky way to do it, is understanding how to incorporate new es-systems into the sim-eth frame that you have already build up. That is, if I have a new system that would be located on the server, how would I make sure that commands can go from client, to this system.

I will try to spend some time figuring out the architecture/landscape of the sim-eth-es, to better understand how it works, but will still appreciate any input.

The GameSessionProxy is exposed through the RMI service to the client… the client calls that remotely basically. That’s how the ship movement works as you’ve probably already seen.

You can either expand this GameSession to include your own method calls for shooting or you can create a new interface + impl + service, etc… Either way, the server side of this connection is already client specific and already has access to all of the server-side services and game systems. It can basically do whatever it wants with respect to entities, systems, whatever.

Is that the kind of answer you were looking for?

Maybe. I will have to spend some time on it later tonight :slight_smile:

It may be a stupid question, but if the client can do anything it wants with respect to the entities, is that not prone to cheating ? (Im no expert on this, mind you - simply curious by nature).

Edit: I think I misread you - which makes my question mute:

Either way, the server side of this connection is already client specific and already has access to all of the server-side services and game systems. It can basically do

The server side has full access :stuck_out_tongue:

The client can’t do anything it wants. It’s calling a method on a remote interface that sends an RPC call to the server and calls the same method on your server side object. The server side object can do whatever it wants.

For example, the move method of this interface:

On the client is handled by the GameSessionClientService which acts as both client service and GameSession. It’s move() method delegates to the RMI GameSession which is just a thin generated wrapper around underlying RPC calls to the server.

On the server the GameSessionHostedService manages the different client GameSessions. Each client gets a GameSessionImpl object specific to them:

The GameSessionHostedService exposes that client instance to the client through the RMI service.

So when you call GameSessionClientService.move() that calls the RMI proxy that implements GameSession… which sends the parameters over the wire and calls the move() on the GameSessionImpl object for that client.

GameSessionImpl has access to whatever it needs… could create entities, perform game logic, whatever. In the move() case, it happens to just forward the information to the ship driver… but it could have done anything.

Edit: for another example of the this pattern in a “pure” state, you can look at the chat service which I tried to make app-independent as much as possible.

Much abliged!

Is there any reference material you could recommend I would read up on, so as not to pester you too much ? (or perhaps just less)

Not really. Best way is maybe just to put printlns in, try to add your own methods to the interface, see what happens.

As I call it, “break and fix with purpose”.

One thing to remember is that methods like move() and if you add your own shoot() method, are called on the networking thread. You probably want to run your shoot() logic on the game thread… the GameSystemManager has a method for enqueuing such tasks.

Will using the enqueuing cause any performance issues - say with 1000 tasks per second being enqueued? (I imagine at least 10 players, with a firing rate of 50-100 shots per second)?

I think I’ve figured out a way to implement it.

I have controls for attacking (saying are we able to attack based on cooldown), and a class to handle the weapons information (with cooldown, level).

package example.sim;

import java.util.concurrent.atomic.AtomicLong;

import com.simsilica.es.EntityId;
import com.simsilica.mathd.*;

import example.es.Position;

/**
 *  A weapons system, keeping track of the weapons level on the ship
 */
public class Weapons {

    public final EntityId weaponsId;
    public int weaponsLevel;    
    public double cooldown;
    
    public volatile ControlAttacker attacker; 
 
    public Weapons( EntityId weaponId ) {
        this.weaponsId = weaponId;
    }
    
    public Weapons( EntityId weaponsId, int weaponsLevel, double cooldown ) {
        this.weaponsId = weaponsId;
        this.weaponsLevel = weaponsLevel;
        this.cooldown = cooldown;
    }
    
    public void setLevel( int weaponsLevel ) {
        this.weaponsLevel = weaponsLevel;
    }
    
    public void setCooldown( double cooldown ) {
        this.cooldown = cooldown;
    }
}

and

package example.sim;

import com.jme3.math.*;

import com.simsilica.mathd.*;

/**
 *  Keeps track of a players attacks, such as cooldowns, buffs etc.
 *
 *  @author    Asser Fahrenholz
 */
public class ShipAttacker implements ControlAttacker {

private Quaternion rotation;
private Vector3f position;
private double timeSinceLastAttack;
private boolean readyToAttack;
 
public boolean applyAttack(Quaternion rotation, Vector3f position) {
    this.rotation = rotation; //Not used yet
    this.position = position; //Not used yet
    if (readyToAttack) {
        timeSinceLastAttack = 0;
        readyToAttack = false; //to make sure we go through cooldown check again before firing
        return true;
    } else {
        return false;
    }
}

@Override
public void update(double stepTime, Weapons weapons) {
    //Update weapons readyness if needed
    readyToAttack = readyToAttack || weapons.cooldown < timeSinceLastAttack;
    timeSinceLastAttack += stepTime;
}

@Override
public void update(double stepTime, Weapons weapons) {
    //Update weapons readyness if needed
    readyToAttack = readyToAttack || weapons.cooldown < timeSinceLastAttack;
    timeSinceLastAttack += stepTime;
}
}

I’ll create a method in the GameSessionImpl to call 'attack:

@Override
public void attack( Quaternion rotation, Vector3f position ){
    if (log.isTraceEnabled()) {
        log.trace("shoot(" + rotation+")");
    }
            
    //Forward to game world
    shipAttacker.applyAttack(rotation, position);
             
    //gameSystems.enqueue(callable); //where I will call a runnable in GameSessionHostedService to spawn entities
}

And create a method there to spawn bullets/missile entities that is fired and forgotten so to speak, so they will live their own lives with decays, objecttypes, damage etc.

And then in the end a system (not thought through yet) to handle collisions between attacks and players, and then subsequently pass on a negative health-component to a health-system that detracts from and recharges health.

I think the next big challenge in this will be detecting a ‘hit’ between bullet and ship. Off to bed now.Feedback on this approach much appreciated.

I think the more you bypass the ES, the more you will regret it later. The only reason the move() method backdoors into the physics system is because there was no ES in the original version and this part was never converted. But, for example, when I want to add AI controlled ships then I’m kind of stuck. Everything begins to fall apart and become a dozen nasty work-arounds. (Which is why I’ll fix that hole before I do AI.)

If it were me, I’d have some kind of “weapon active” component that I’d attach when some weapon entity is activated. (In this case, it could be the ship directly but maybe some day it’s one of 20 gun turrets or something.)

Something like a MissileSystem would be watching for WeaponActive and MissileLoadedTime or something. MissileLoadTime would be set to the next game time that a missile can be launched (currentTime + coolDown if you like). The MissileSystem would simply loop over its entities and create missile entities for any WeaponActive entities that have also passed their MissileLoadedTime.

Creating a missile would just be creating another entity that has a position and velocity. The physics system would watch for new velocity holding entities and start them off with that appropriate velocity. You could also use an Impulse based system instead if you wanted the missile to accelerate instead of starting off at a specific velocity. The missile would have some kind of DamageType component on it that lets the collision system know what to do when it hits something.

The collision system when it detects a collision looks for DamageType components on each half of the exchange and creates Damage entities attached to the target. A DamageSystem loops over new Damage entities and applies damage… perhaps one frame, perhaps many frames… up to the system and whatever other components there are.

The point is that all of this is now decoupled and you can rearrange things how you like or add new behavior without affecting anyone else. If you want to have napalm missiles then you can easily do it. If you want to have lasers then you can easily do that. If you want to trigger sounds based on “active weapon” + “millisile loading” (a warm up sound or whatever) then you can easily do that.

It’s also really easy for AI to interact with as all the AI has to do is create the appropriate components/entities.

2 Likes

BTW, 50-100 shots per second each is kind of silly, don’t you think? Even 5 shots per second is pretty fast.

At 60 shots per second you’d have one shot for every screen refresh… no one is going to see that.

You are absolutely right. I dont know why I totally overlooked the ES in my eagerness to move forward.

I’ll rethink later tonight :slight_smile:

Sure, but I guess I meant from different cannons. You may have a buff to fire more than one shot per ‘trigger’ (like to cannons firing in parallel or three lanes of shots firing in a spreading arc or similar.

If you were to implement the move-system as ES-based, would it be a ‘move component’ attached to the player-entity ? and then the ShipDriver implemented as a ES-system?

Yeah, in my other ES-based sim-ethereal app that’s essentially what I do. There is a MoveState component that I’ve attached to the ship entity. A system watches for these entities and finds their body in the physics space to attach the driver. Then whenever the move state changes it updates the driver.

The back-end access to the physics engine is really only necessary because of how critical it is for the ship movement adjustments to happen in the physics loop.

You can actually do it all with some kind of Acceleration component but from experience, movement that feels good to the player will ultimately need to be based on the state of the body in motion. For example, a walking player can’t easily be done purely with acceleration because you’ll want to apply acceleration against turns and stuff so they don’t slide around. Furthermore, being looped into the physics engine lets you take advantage of known collision information. (For another example, Mythruna uses this approach and the ‘driver’ is notified about all of the contacts on a given frame so that when the ‘driver’ finally gets to apply forces it knows it can step up over the low block, etc.)

1 Like

I only know of AsteroidPanic, which uses a PhysicsState to integrate Position and Velocity quite nicely. Thanks again for the help and feedback. I’ll convert both the move and attack to ES based components/systems.

1 Like

I ended up having a controlstate on the client with only:

@Override
public void valueActive(FunctionId func, double value, double tpf) {
    if (func == ShipFunctions.F_TURN) {
        session.rotate(value, tpf);
    } else if (func == ShipFunctions.F_THRUST) {
        session.thrust(value, tpf);
    } else if (func == ShipFunctions.F_TURN) {
        session.rotate(value, tpf);
    }
}

@Override
public void valueChanged(FunctionId func, InputState value, double tpf) {
    if (func == ShipFunctions.F_SHOOT && value == InputState.Positive) {
        session.attack(tpf);
    } else if (func == ShipFunctions.F_THRUST) {
        if (value == InputState.Positive) {
            //thrust.play();
        } else {
            //thrust.stop();
        }
    }
}

Which is what I pictured from the start (that all calculations would be done server side).

I have the code implemented directly in the GameSessionImpl, not sure this is the way to go - but I imagine it could be split into multiple GameSessionImpl instances if needed.

The problem with your current approach of a separate rotate() and thrust() is that you are now tripling the number of RPC calls you are making per frame. If you are worried about only sending things when they change then you can always have a dirty flag… but as it stands if you are moving the mouse (or using the joystick) and thrusting at the same time then you are sending three RPC calls 60 times a second.

Ah, so you’re saying it would be better to have one call with both move and rotate in. Makes sense. I’ll see if I can make that happen.

I am only using keyboard for this game. No mouse or joystick input. Well, joypad/joystick is a far fetch in future.

Edit: Didn’t notice the double check in the if until now :stuck_out_tongue: