Ray-cast collisions problem

So I have this problem, and I’m guessing there isn’t one solid answer, but it’s a rather simple situation. I had this problem working on my Ludum Dare 22 game and now I am having the exact same problem with my jOryx Realm of the Mad God custom client.



The only object in the scene that needs to have physical collision handling is the player object, and it is very, very basic at that. The player object never rotates, is a 1 by 1 quad showing a texture (a sprite with alpha transparency) and you view it from an above perspective and move it around with WASD with no verticality. Everything else is merely considered by its origin point (if it’s below a certain distance to a player or wall, it “hit” it). As such, I don’t really need to use a full physics simulation. No height is considered.



I specifically do not want to use Bullet. I need to know how the best way to approach handling a collision with a wall is, without bullet.



In Ludum Dare 22, I projected four rays from the object’s origin offset by half of its size (so, 0.5 to the left if shooting a ray for the right) in the cardinal directions, determine the distance to the closest collision, and push the node out if it is less than 1 unit. There were two problems with this: the player could walk through walls if the time between frames was sufficiently long enough to avoid a collision detection at all, and you could cut corners and actually move through walls just by approaching an outward corner at the right angle. That code is found in this: https://github.com/Furyhunter/alonegame-ld22/blob/master/src/alonegame/PlayerControl.java



For jOryx, I have tried numerous ways. Eight rays, two from each cardinal direction from the top and bottom or left and right side, calculating the MAXIMUM distance in any given direction, and capping the movement vector at that value. Four rays, projected from the origin, capping the movement vector that the maximum distance. In each of these cases, random errors like being jutted straight through walls or stuff like that occurred, but the player was never able to move through walls just by the client stuttering a little bit.



In both games, the walls were just cubes in the scene.



Can someone explain how in the world to go about doing this correctly? Surely there’s a right way to do it, otherwise bullet wouldn’t work.



Here is the code used in my PlayerControl for jOryx. As it stands, this code works for almost all collision cases, except specifically when the character is sliding against a wall and approaching an inner corner, where it will immediately stop moving one tile before the actual corner tile:

[java]

package com.joryx.control;



import java.util.List;



import com.jme3.collision.CollisionResults;

import com.jme3.input.controls.ActionListener;

import com.jme3.material.Material;

import com.jme3.math.Ray;

import com.jme3.math.Vector3f;

import com.jme3.renderer.RenderManager;

import com.jme3.renderer.ViewPort;

import com.jme3.scene.Geometry;

import com.jme3.scene.Node;

import com.jme3.scene.Spatial;

import com.jme3.scene.control.AbstractControl;

import com.jme3.scene.control.Control;

import com.joryx.db.WallControl;

import com.oryxhatesjava.net.data.ObjectStatus;

import com.oryxhatesjava.net.data.StatData;



public class PlayerControl extends AbstractControl implements ActionListener {



private boolean moveForward;

private boolean moveBackward;

private boolean strafeLeft;

private boolean strafeRight;



private float speedMult = 1.0f;



private float timeToFrame;

private int frame;



private boolean player;



private ObjectStatus status;



public PlayerControl(ObjectStatus status) {

this.status = status;

}



@Override

public Control cloneForSpatial(Spatial spatial) {

return null;

}



@Override

protected void controlUpdate(float tpf) {



// Get the player material and object control

Material pm = ((Geometry)((Node)spatial).getChild(“Geometry”)).getMaterial();

ObjectControl oc = spatial.getControl(ObjectControl.class);



if (player) {

Vector3f strafe = new Vector3f(0, 0, 0);

Vector3f straight = new Vector3f(0, 0, 0);



if (moveForward) {

straight.y += 1;

}



if (moveBackward) {

straight.y -= 1;

}



if (strafeLeft) {

strafe.x -= 1;

}



if (strafeRight) {

strafe.x += 1;

}



if (strafe.length() == 0 && straight.length() == 0) {

oc.setMoving(false);

} else {

oc.setMoving(true);

// do movement

strafe.normalizeLocal();

straight.normalizeLocal();

Vector3f fin = strafe.add(straight).normalizeLocal();

spatial.getLocalRotation().multLocal(fin);



float dist = 4f + 5.6f*(((float)(status.data.getStat(StatData.SPD).value + status.data.getStat(StatData.SPDBONUS).value)/75f) - 0.1f);

dist = speedMult;

fin.multLocal(dist);

fin.multLocal(tpf);



// Check maximum distance in each direction

Vector3f origin = spatial.getWorldTranslation();

Ray rayUpLeft = new Ray(origin.add(-0.4f, 0.5f, 0), Vector3f.UNIT_Y);

Ray rayUpRight = new Ray(origin.add(0.4f, 0.5f, 0), Vector3f.UNIT_Y);

Ray rayLeftTop = new Ray(origin.add(-0.5f, 0.4f, 0), Vector3f.UNIT_X.negate());

Ray rayLeftBottom = new Ray(origin.add(-0.5f, -0.4f, 0), Vector3f.UNIT_X.negate());

Ray rayRightTop = new Ray(origin.add(0.5f, 0.4f, 0), Vector3f.UNIT_X);

Ray rayRightBottom = new Ray(origin.add(0.5f, -0.4f, 0), Vector3f.UNIT_X);

Ray rayDownLeft = new Ray(origin.add(-0.4f, -0.5f, 0), Vector3f.UNIT_Y.negate());

Ray rayDownRight = new Ray(origin.add(0.4f, -0.5f, 0), Vector3f.UNIT_Y.negate());



float maxUp = 1000;

float maxRight = 1000;

float maxDown = -1000;

float maxLeft = -1000;



Node parent = spatial.getParent(); // world node

List<Spatial> children = parent.getChildren();

CollisionResults rs = new CollisionResults();

for (Spatial s : children) {

if (s == spatial) {

continue;

}



if (s.getWorldTranslation().distance(spatial.getWorldTranslation()) > 2) {

continue;

}



if (s.getControl(WallControl.class) != null) {

float distance = 0;

int ret = 0;



// Down

if (moveBackward) {

ret = s.collideWith(rayDownLeft, rs);

if (ret > 0) {

distance = rs.getClosestCollision().getDistance();

if (distance <= -maxDown) {

maxDown = -distance;

}

}

ret = s.collideWith(rayDownRight, rs);

if (ret > 0) {

distance = rs.getClosestCollision().getDistance();

if (distance <= -maxDown) {

maxDown = -distance;

}

}

}



// Up

if (moveForward) {

ret = s.collideWith(rayUpLeft, rs);

if (ret > 0) {

distance = rs.getClosestCollision().getDistance();

if (distance <= maxUp) {

maxUp = distance;

}

}

ret = s.collideWith(rayUpRight, rs);

if (ret > 0) {

distance = rs.getClosestCollision().getDistance();

if (distance <= maxUp) {

maxUp = distance;

}

}

}



// Left

if (strafeLeft) {

ret = s.collideWith(rayLeftTop, rs);

if (ret > 0) {

distance = rs.getClosestCollision().getDistance();

if (distance <= -maxLeft) {

maxLeft = -distance;

}

}

ret = s.collideWith(rayLeftBottom, rs);

if (ret > 0) {

distance = rs.getClosestCollision().getDistance();

if (distance <= -maxLeft) {

maxLeft = -distance;

}

}

}



// Right

if (strafeRight) {

ret = s.collideWith(rayRightTop, rs);

if (ret > 0) {

distance = rs.getClosestCollision().getDistance();

if (distance <= maxRight) {

maxRight = distance;

}

}

ret = s.collideWith(rayRightBottom, rs);

if (ret > 0) {

distance = rs.getClosestCollision().getDistance();

if (distance <= maxRight) {

maxRight = distance;

}

}

}

}

}



// maxLeft += 1f;

// maxRight -= 1f;

// maxUp -= 1f;

// maxDown += 1f;



if (fin.x > 0 && maxRight < fin.x) {

fin.x = maxRight;

} else if (fin.x < 0 && maxLeft > fin.x) {

fin.x = maxLeft;

}



if (fin.y > 0 && maxUp < fin.y) {

fin.y = maxUp;

}



if (fin.y < 0 && maxDown > fin.y) {

fin.y = maxDown;

}



if (strafeRight) {

oc.setDirection(ObjectControl.RIGHT);

}

if (strafeLeft) {

oc.setDirection(ObjectControl.LEFT);

}

if (moveForward) {

oc.setDirection(ObjectControl.UP);

}

if (moveBackward) {

oc.setDirection(ObjectControl.DOWN);

}



spatial.move(fin);

} //end movement block



} //end player only block



// Animation check

if (oc.isMoving()) {

timeToFrame += tpf
5*speedMult;

if (timeToFrame > 1) {

timeToFrame = 0;

frame = (frame == 2 ? 1 : 2);

}

} else {

frame = 0;

}



switch (oc.getDirection()) {

case ObjectControl.UP:

pm.setBoolean(“Flip”, false);

pm.setInt(“Row”, 0);

pm.setInt(“Column”, frame);

break;

case ObjectControl.DOWN:

pm.setBoolean(“Flip”, false);

pm.setInt(“Row”, 1);

pm.setInt(“Column”, frame);

break;

case ObjectControl.LEFT:

pm.setBoolean(“Flip”, true);

pm.setInt(“Row”, 2);

pm.setInt(“Column”, (oc.isMoving() ? frame-1 : 0));

break;

case ObjectControl.RIGHT:

pm.setBoolean(“Flip”, false);

pm.setInt(“Row”, 2);

pm.setInt(“Column”, (oc.isMoving() ? frame-1 : 0));

break;

}

}



@Override

protected void controlRender(RenderManager rm, ViewPort vp) {



}



@Override

public void onAction(String name, boolean isPressed, float tpf) {

if (name.equals(“PlayerStrafeLeft”)) {

strafeLeft = isPressed;

}



if (name.equals(“PlayerStrafeRight”)) {

strafeRight = isPressed;

}



if (name.equals(“PlayerMoveForward”)) {

moveForward = isPressed;

}



if (name.equals(“PlayerMoveBackward”)) {

moveBackward = isPressed;

}

}



public void setSpeedMult(float sp) {

speedMult = sp;

}



public boolean isPlayer() {

return player;

}



public void setPlayer(boolean player) {

this.player = player;

}



}

[/java]

Not sure this is the best way but if everything is similarly oriented boxes then you should be able to trivially calculate interpenetration between the boxes… then just push the player back out by the shortest of the 2 axes of penetration.



So, if they overlap in X calculate how much. If they overlap in Y calculate how much. Then, if they overlap in X and Y then you know the two boxes penetrate and you can push it back out in the smallest of X or Y.

Yeah, I guess I could do it that way, but I am worried about the intersection test not being run in between steps. If a garbage collection occurs (and well, it’s Java, so it happens) then a sufficiently long collection pause could cause the player to jump a very large distance to compensate for the time (by multiplying the movement vector by tpf). Calculating a maximum distance before moving prevents this from happening, but there are certain oddities with the way I’ve done the raycasts. Oddities like the inner corner problem, which only happens if you’re moving at a diagonal toward the wall. That’s the major problem.



Considering the game server will kick a client who spends too much time in a weird zone, and the client might get unlucky enough to tell the server it’s sitting in a wall every tick, doing a “push out” is not safe.

I solved this problem by adding steps in my collision code if the time step is too long. I divide it and do some extra incremental steps. Doing sweeping collisions is way more complicated.