Questions about character movement/physics engine in multiplayer game


#1

Hi guys,

a bit of background:
I’m creating a small multiplayer rpg game where people have a character and can freely move around in the world. I started with the sim-eth-es example of pspeed to get a basic understanding of how to set up client-server communication, object syncing, RMI, etc. I definitely want to keep the same architecture: the server sends movement updates of the characters to the client, and the client interpolates between these updates. Camera rotation however is decoupled, it is applied directly on the character model and send at regular intervals to the server.

I’m now coming at the point I would like to implement the actual character movement. When I started a few months ago I was confident I would be using bullet, but as time progresses I’m doubting this decision, as it might be way overkill. What I want is actually very basic physics, a low poly terrain where characters can walk/run/jump on, that’s it really. (think of vanilla/classic wow)

So my questions are:

  • Is bullet the way to go for my needs?
  • If using bullet, is the way of movement used in pspeed sio2 bullet-char demo viable for a multiplayer game? (send client input movement updates to the server, server creates components from the input and a game system/driver handles these es components and applies the forces on the rigid body)
  • I don’t want user avatars to bump/collide with each other (players blocking each other in small corridors for example), how would I do this with bullet?

Thanks in advance :slight_smile:


#2

I haven’t used sio2 bullet so I can’t answer about that specifically. But hopefully my input can be of some use to you still :slightly_smiling_face: I wrote my own simple movement system, and then I use bullet for everthing else physics related

I personally advise against using bullet for character movement in most rpg games, especially if you are going to have more than 10-15 BetterCharacterControls loaded at once - around there is where I’ve noticed a drastic framerate drop.

For my RPG game, I use only 2 rays per character for world collisions per frame (one facing forward, and one facing down), and I use the character’s radius and a distance check to push away other nearby characters based on their radius. I like to let players push away the NPCs more than an NPC can push players, so that players can never get trapped in small areas

But I would still suggest using Bullet physics for simulating other physical interactions in the world, such as barrels or tables that can be pushed or tipped over when you walk into them, or using things like a ragdoll control for dead enemies so they fall realistically.

I personally plan to use some of bullet’s features in my game (and especially some of the cool features @sgold is working on with Minnie), but when it comes to the movement system and character physics, writing my own movement system has definitely been a beneficial decision in my experience.


#3

I mean, I’m biased… but I say yes. The driver approach is very flexible in that you can make them as complicated or as simple as you want. (ie: they can use forces or just directly set velocity or whatever your movement requires.)

But yeah, all of my stuff is usually designed with multiplayer in mind and the sio2-bullet integration was directly pulled from SpaceBugs which was going to be a multiplayer FPS.


#4

Thanks for your answer. How would you go about avoiding collisions between rigidbodies (players collide with the terrain but not with each other)? The only thing I can think of is making them kinematic, but that will open up another box of issues.

Can you elaborate a bit more on this solution? Does this mean you create and maintain a scenegraph on the server where you add the player avatars as simplified geometries (sphere/cylinder/…) and use the boundingvolume.collidewith() methods to check for collisions?


#5

I personally would still model them as bodies. In the driver you have direct access to the actual rigid body and can set the model back upright, move its position, do whatever. You have total control over it.


#6

Yes, I flag all of the geometries in the scene that are physical objects with a UserData String key and remove non-physical spatials as well as all materials from the scene on the server.

Then I use the attachment nodes on the character models to attach simple geometries like spheres and cylinders to use as the hit boxes for characters. ( the nice advantage here is that the characters hitboxes will also follow the model’s animations when you’re using attachment nodes, so this is also a good idea for character hitboxing even if you use bullet for collisions)

Yes, although it is important to note that you don’t always want to use the bounding volume for collisions, since it is not always accurate as using the geometry. Ray vs Geometry collisions will return a perfectly accurate collision result, where as Ray vs BoundingVolume's '“apparent” hitboxing accuracy will depend on how much the bounding volume matches the model that it is representing. (so using a simple bounding sphere for spherical projectiles will be just as accurate as if you use bullet)

If you use bullet physics, then you are able to do Geometry vs Geometry collision, which is much more accurate - but often overkill for some things that have a simple shape.
Although using bullet for geometry vs geometry collision is necessary in some situations. My game is relatively small still, so most of my spells/weapons/projectiles are able to be accurately represented by bounding spheres or rays for collisions - however if I were to add in a weapon that has a more irregular or precise shape (like a giant sycthe), then I would need to create a low-poly collision geometry in blender, and use a rigid body with bullet physics for geometry vs geometry hitboxing when using that weapon.

I’ve found that the key to keeping an RPG optimized as it grows is to use simple rays and bounding spheres for collisions whenever possible - and then venture into using Bullet for collisions whenever you need more accurate collisions and hitboxing for irregular shaped objects.


#7

I am doing a slightly different approach here. Instead of sending the client input each frame to server I am using the “click to go” approach. So the client clicks on a location in scene and I send the target point to the server. At the server side, I find a path from client current location to the target point and if there is a path found I apply a path follow steer force in the driver.

I am using JME Steer Behaviors to apply steer forces in the driver.

Edit:

My CharInputDriver looks like below, it’s a bit messy and not finished yet, sorry:

public class CharInputDriver implements ControlDriver {

    private final Entity entity;
    private final MobAgent agent;
    private EntityRigidBody body;
    private CharPhysics charPhysics;

    private Vector3f vTemp = new Vector3f();
    private Quaternion qTemp = new Quaternion();
    private float[] angles = new float[3];

    private float walkSpeed = 4.0f;
    private Vector3f force = new Vector3f();
    private CharInput input;

    //private Vector3f groundVelocity = new Vector3f();   
    private float rotationSpeed = FastMath.TWO_PI;
    private float groundImpulse = 200;
    private Vector3f desiredVelocity = new Vector3f();

    private Grid grid;

    private PathAdapter pathAdapter;

    //private Mobility movementMobility;

    // Define steer behaviors
    private final CompoundSteeringBehavior mainBehavior;
    private CharPathFollowBehavior pathFollow;

    public CharInputDriver(Entity entity, MobAgent agent, CharPhysics charPhysics, Grid grid, PathAdapter pathAdapter) {
        this.entity = entity;
        this.agent = agent;
        setCharPhysics(charPhysics);
        this.grid = grid;
        this.mainBehavior = new CompoundSteeringBehavior();
        this.agent.setMainBehavior(mainBehavior);
        this.pathFollow = new CharPathFollowBehavior();
        this.mainBehavior.addSteerBehavior(pathFollow);
    }

    public void setInput(CharInput input) {
        this.input = input;
        this.pathFollow.setEnabled(false);
    }

    public CharInput getInput() {
        return input;
    }

    public void setCharPhysics(CharPhysics charPhysics) {
        this.charPhysics = charPhysics;
        if (body != null) {
            // Make sure gravity is current
            body.setGravity(charPhysics.gravity);
        }
//        this.groundImpulse = charPhysics.groundImpulse;
//        this.airImpulse = charPhysics.airImpulse;
//        this.jumpForce = charPhysics.jumpForce;
//        this.shortJumps = charPhysics.shortJumps;
//        this.autoBounce = charPhysics.autoBounce;
    }

    public Vector3f getPhysicsLocation(Vector3f trans) {
        return body.getPhysicsLocation(trans);
    }

    public Quaternion getPhysicsRotation(Quaternion rot) {
        return body.getPhysicsRotation(rot);
    }

    @Override
    public void initialize(EntityPhysicsObject object) {
        body = (EntityRigidBody) object;
        body.setGravity(charPhysics.gravity);
        //this.agent.setRadius((float) body.getBounds().getMax().subtract(body.getBounds().getMin()).length() / 2);
        agent.setRadius(0.5f);
        agent.setMaxMoveSpeed(1);
        agent.setRotationSpeed(rotationSpeed);
        //movementMobility = agent.addMobility("char-movement", "Default");
        pathAdapter.setPositionAdapter(new PositionAdapter() {
            Vector3f vTemp = new Vector3f();
            Quaternion qTemp = new Quaternion();

            @Override
            public Vector3f getLocation() {
                return body.getPhysicsLocation(vTemp);
            }

            @Override
            public Quaternion getOrientation() {
                return body.getPhysicsRotation(qTemp);
            }
        });
    }

    @Override
    public void update(SimTime time, EntityPhysicsObject object) {
        body.getPhysicsLocation(vTemp);
        agent.setWorldTranslation(vTemp);
        body.getPhysicsRotation(qTemp);
        agent.setWorldRotation(qTemp);
        //agent.updateCell(grid);

        //body.getPhysicsRotation(qTemp);
        body.getAngularVelocity(vTemp);

        // Note: apparently killing angular velocity is actually needed.
        // Otherwise we tip and intercollide with ramps.
        // Kill any non-yaw rotation
        if (vTemp.x != 0 && vTemp.z != 0) {
            vTemp.x = 0;
            vTemp.y *= 0.95f; // Let's see if we can dampen the spinning
            vTemp.z = 0;
            body.setAngularVelocity(vTemp);
        }

        if (input != null && input.hasTarget()) {
            findPath(input.getTarget());
            input = null;
        }

        agent.updateAI((float) time.getTpf());
        desiredVelocity.set(agent.getVelocity()).normalizeLocal().multLocal(walkSpeed);

        // See how much our velocity has to change to reach the
        // desired velocity
        body.getLinearVelocity(vTemp);

        // Calculate a force that will either break or accelerate in the
        // appropriate direction to achieve the desired velocity
        force.set(desiredVelocity).subtractLocal(vTemp);
        force.y = 0;

        // We are going to apply force by a threshold to make characer less slippy 
        // so it wont look like a vehicle.
        if (force.length() > 0.8f || body.getLinearVelocity().length() > 0.8f) {
            body.applyCentralForce(force.multLocal(groundImpulse));
            killNonYawOrientation(agent.getPredictedRotation());
        } else {
            if (pathFollow.isEnabled()) {
                pathFollow.setEnabled(false);
            }
            agent.getVelocity().set(Vector3f.ZERO);
            // We need to kill non-yaw orientation for capsule shapes  
            // to prevent body fall down when no force is applied.
            killNonYawOrientation(body.getPhysicsRotation(qTemp));
        }
        // Note: for non-capsule shapes something else would have to
        // be done so that the environment affects orientation.
        // Player input orientation then becomes more of a suggestion
        // that needs to be reconciled with environment influences.         
    }

    private void killNonYawOrientation(Quaternion rotation) {
        // Kill any non-yaw orientation
        rotation.toAngles(angles);
        if (angles[0] != 0 || angles[2] != 0) {
            angles[0] = 0;
            angles[2] = 0;
            body.setPhysicsRotation(qTemp.fromAngles(angles));
        }
    }

    private void findPath(Vector3f target) {
        Vector3f[] path = pathAdapter.findPath(target);
        if (path != null) {
            pathFollow.reset(path);
        }
    }

    @Override
    public void terminate(EntityPhysicsObject body) {
        //agent.removeMobility(movementMobility);
    }

    @Override
    public void addCollision(EntityPhysicsObject otherBody, PhysicsCollisionEvent event) {

    }
}

#8

I created a small sandbox environment to test the character movement using jbullet. With bullet-native I did encounter some weird behaviour: forces that aren’t applied anymore after some time has passed (character seems stuck in the world)
I didn’t do any tweaking yet in this example (all default physics values) and didn’t look into jumping or setting friction (don’t slide of the slope) etc…

I see you are using forces body.applyCentralForce() to steer the character. At the moment I’m using the setLinearVelocity() method on the rigidbody to move it. I guess the result will be more ‘lifelike’ when using forces or are there any other benefits of using forces over setting the linear velocity?

In case anyone is curious: I did manage to find a way to ignore certain collisions between rigidbodies (in this case, player vs player). You can add a PhysicsCollisionGroupListener to the physicsSpace. This listener is called when a collision is about to happen. When the listener returns false, bullet will just ignore the collision!


#9

Cool. These playgrounds are fun to make… I had fun with the one I did, also.

Yeah, in the SiO2 bullet integration version I had a similar problem with the native bullet… I had to force a fixed time step or I got really strange behavior. The current demo should work fine with native, though.

Edit: it’s the one from this video:


#10

Ha, very nice demo!

What do you mean exactly by forcing a fixed time step? Do you mean updating the physicsspace? I do this in the update method of the GameSystem:

// calculate the speed of the physics simulation
float t = (float) time.getTpf() * speed;
if (t != 0) {

    // update the drivers of the physical entities
    for (PhysicalEntity entity : rigidBodyContainer.getArray()) {
        if (entity.getPhysicalEntityDriver() != null) {
            entity.getPhysicalEntityDriver().update(t);
        }
    }

    // update the physics space and distribute collision events
    physicsSpace.update(t);
    physicsSpace.distributeEvents();

    // notify the listeners for all of the attached entities after the physics calculation
    for (PhysicalEntity entity : rigidBodyContainer.getArray()) {
        physicalObjectUpdated(entity);
    }

}

I browsed a bit through the classes that update the physical bodies, but didn’t really find a lead to this fix.


#11

He means, use pSpace.update(1/60f, 0);

You can find useful info here:


#12

OMG!

this line is a game changer:

physicsSpace.update(t, 0);

played a few minutes with the playground and I don’t seem to have any issues with bullet-native any more! :smile: :monkey_face: