[SOLVED] Weird problem with setting Vector3f

My program uses rays to detect movement collisions. I was working on a particular enemy’s movement algorithm, when all the movement collisions broke (including the player’s). All the enemies and the player can pass through walls (but only from 2 sides).

I think it might be the rays, because the player’s movement broke as well, even though I was only tweaking enemy movement.

This is the code block I was working on:

// rigging is for special moves
@Override
protected void rigging(Node collidables) {
	Vector3f n = new Vector3f().set(masterNode.getLocalTranslation());
	CollisionResults results = new CollisionResults();
	Ray ray = new Ray(n, getWalkDirection());
	collidables.collideWith(ray, results);
	if (results.size() > 0) {
		Vector3f contact = results.getClosestCollision().getContactPoint();
		if (contact.distance(n.add(getWalkDirection())) < getBound()) {
			setWalkDirection(new Vector3f(
				(float)ThreadLocalRandom.current().nextDouble(-10, 10), 0,
				(float)ThreadLocalRandom.current().nextDouble(-10, 10)
			).normalize().mult(getSpeed()));
		}
	}
}

This is the default movement collision for enemies:

protected void debuncMove(Node collidables) {
	// set rotation
	Quaternion rotToPlr = new Quaternion();
	rotToPlr.lookAt(walkDirection, Vector3f.UNIT_Y);
	masterNode.setLocalRotation(rotToPlr);
	
	// detect collision
	Vector3f n = new Vector3f().set(masterNode.getLocalTranslation());
	CollisionResults results = new CollisionResults();
	Ray ray = new Ray(n, walkDirection);
	collidables.collideWith(ray, results);
	if (results.size() > 0) {
		Vector3f contact = results.getClosestCollision().getContactPoint();
		if (contact.distance(n.add(walkDirection)) < bound) walkDirection = Vector3f.ZERO;
	}
}

And this is the movement collision for the player:

protected void detectCollision(Node collidables) {
	CollisionResults results = new CollisionResults();
	Ray ray = new Ray(node.getLocalTranslation(), walkDirection);
	collidables.collideWith(ray, results);
	if (results.size() > 0) {
		Geometry target = results.getClosestCollision().getGeometry();
		Vector3f collisionPoint = results.getClosestCollision().getContactPoint();
		if (node.getLocalTranslation().distance(collisionPoint) < bound) {
			//node.setLocalTranslation(node.getLocalTranslation().add(walkDirection.negate()));
			walkDirection = Vector3f.ZERO;
		}
	}
}

Do you ever later modify ‘walkDirection’ directly? If so then you are corrupting your ZERO ‘constant’.

Yes

// default move calculation for enemies, this is overriden in Mastermind.
// Mastermind is the enemy type I'm working on right now.
protected void setMove() {
	Vector3f n = new Vector3f().set(masterNode.getLocalTranslation());
	walkDirection = plr.getPos().subtract(n).normalize().mult(speed).setY(0);	
}
// This is done in Mastermind once when a new Mastermind is created
setWalkDirection(Vector3f.UNIT_Z.mult(getSpeed()));

How could one instance setting its moveDirection affect another instance’s moveDirection?

By “modify directly”, I mean make calls like walkDirection.x = 11241245 or walkDirection.set(1, 2, 3) or something.

walkDirection = someOtherThing is changing the reference and not “modifying directly”.

It’s just a weird idiom that you use to set these one-off references and not keep them and is a sign that maybe there is some bad “pointer discipline” going on… which is why I suggested that maybe you are modifying a constant directly. The equivalent of ZERO.x = somethingNonZero through proxy.

Otherwise, we don’t have enough of your code to spot the issue and the code you provided doesn’t show any obvious issues. So you will have to learn to debug it by adding logging or stepping through in the debugger to see what’s wrong.

I found what seems to be causing the problem.

That’s somehow messing up all the collisions, though I can’t imagine why…
When I comment it out, the problem goes away.
Another question: why just walkDirection? I’m changing other things too.

What does setWalkDirection itself look like?

protected void setWalkDirection(Vector3f w) {
    walkDirection.set(w);
}

If I set walkDirection to Vector3f.UNIT_Z on declaration it goes funky in a different totally unexpected way. It starts out normally, then on the next game all the blocks are chopped off into a parallelogram. I don’t know what that has to do with walkDirection.

If this is the same “walkDirection” in your earlier code that you are doing walkDirection = Vector3f.ZERO… then it’s “modifying the walk direction DIRECTLY” as in effectively ZEROR.set(someOtherValue).

If that’s not the same walkDirection field then I don’t know.

Getting little pin-hole views into the code is not helpful. What class was setWalkDirection() on? What class are the other methods on?

In general, code like:
someVector = Vector3f.UNIT_Z;
Or:
someVector = Vector3f.ZERO;
is a huge warning sign. You have to be a super 20+ year experienced developer to keep track of all of the places you mess with someVector.set() or someVector.x= in order not to do global damage. And a super 20+ year experienced developer would just not do it in the first place.

EITHER:
1.) keep your own walkDirection initialized from the beginning:
private final Vector3f walkDirection = new Vector3f();
…then change it like:
walkDirection.set(newValue);
ie:
walkDirection.set(Vector3f.UNIT_Z);
walkDirection.set(Vector3f.ZERO);

OR:
2.) Clean your references all the time:
walkDirection = Vector3f.UNIT_Z.clone();
walkDirection = Vector3f.ZERO.clone();

…most probably prefer the first way.

2 Likes

Here are the 3 most relevant classes. I can add Player Class if you want. If you come across a method that you can’t find the declaration of, it was removed to conserve space and is not important.

Mastermind Class (extends Badguy)
public class Mastermind extends Badguy {
	
	public static final String MASTERMIND_TYPE_ID = "mastermind";
	
	private float spawnDelay = 0;
	
	
	Mastermind() {
		super();
		
		//setSpeed(20);
		//setWalkDirection(Vector3f.UNIT_Z.mult(getSpeed()));
	}
	
	
	@Override
	protected void build(AssetManager manager) {		
		Spatial spat = manager.loadModel("Models/dudes/StickMan.j3o");
		Material mat = new Material(manager, "Common/MatDefs/Light/Lighting.j3md");
		mat.setBoolean("UseMaterialColors", true);
		mat.setColor("Diffuse", ColorRGBA.Blue);
		mat.setColor("Ambient", new ColorRGBA(0, 0, 0.3f, 1));
		mat.setColor("Specular", ColorRGBA.Yellow);
		spat.setMaterial(mat);
		spat.setLocalScale(3);
		spat.setLocalTranslation(0, -1, 0);
		spat.setShadowMode(RenderQueue.ShadowMode.Cast);
		bodyNode.attachChild(spat);
		
		//walkDirection.set(Vector3f.UNIT_Z.mult(getSpeed()));
	}
	@Override
	protected void setMove() {}
	@Override
	protected void rigging(Node collidables) {
		//if (getWalkDirection().x == 0 && getWalkDirection().z == 0) setWalkDirection(Vector3f.UNIT_Z.mult(getSpeed()));
		Vector3f n = new Vector3f().set(masterNode.getLocalTranslation());
		CollisionResults results = new CollisionResults();
		Ray ray = new Ray(n, getWalkDirection());
		collidables.collideWith(ray, results);
		if (results.size() > 0) {
			Vector3f contact = results.getClosestCollision().getContactPoint();
			if (contact.distance(n.add(getWalkDirection())) < getBound()) {
				setWalkDirection(new Vector3f(
					(float)ThreadLocalRandom.current().nextDouble(-10, 10), 0,
					(float)ThreadLocalRandom.current().nextDouble(-10, 10)
				).normalize().mult(getSpeed()));
			}
		}
	}
	@Override
	protected boolean willMinion() {
        // this method decides minion spawning (probably not important)
		spawnDelay--;
		boolean will = spawnDelay <= 0;
		if (will) spawnDelay = 400;
		return will;
	}
	@Override
	protected String getTypeId() {
		return MASTERMIND_TYPE_ID;
	}
}
relevant parts of Badguy Class
public class Badguy {
	
	public static final String BADGUY_TYPE_ID = "badguy";
	
	private Vector3f pos;
	
	private String id = "badguy", masterId = "master", bodyId = "body", exploId = "explosion";
	final Node masterNode = new Node(), bodyNode = new Node(), exploNode = new Node();
	
	private Player plr = null;
	private Vector3f walkDirection = Vector3f.ZERO;
	private float speed = 0.2f, bound = 3;
	private float damage = 0.1f;
	
	private boolean remove = false, exploded = false;
	private ParticleEmitter debrisEffect;
	
	
	Badguy() {}
	Badguy(Vector3f pos) {
		this.pos = pos;
	}
	Badguy(String id, Vector3f pos) {
		this.id = id;
		this.pos = pos;
	}
	
	
	protected void build(AssetManager manager) {		
		Spatial spat = manager.loadModel("Models/dudes/StickMan.j3o");
		Material mat = new Material(manager, "Common/MatDefs/Light/Lighting.j3md");
		mat.setBoolean("UseMaterialColors", true);
		mat.setColor("Diffuse", ColorRGBA.Red);
		mat.setColor("Ambient", new ColorRGBA(0.3f, 0, 0, 1));
		mat.setColor("Specular", ColorRGBA.Orange);
		spat.setMaterial(mat);
		spat.setLocalScale(3);
		spat.setLocalTranslation(0, -1, 0);
		spat.setShadowMode(RenderQueue.ShadowMode.Cast);
		bodyNode.attachChild(spat);
	}
	protected void initNodes() {
		bodyId = id+" "+bodyId;
		bodyNode.setName(bodyId);
		masterNode.attachChild(bodyNode);
		masterId = id+" "+masterId;
		masterNode.setName(masterId);
		masterNode.setLocalTranslation(pos);
	}
	
	protected void setMove() {
		Vector3f n = new Vector3f().set(masterNode.getLocalTranslation());
		walkDirection = plr.getPos().subtract(n).normalize().mult(speed).setY(0);	
	}
	protected void debuncMove(Node collidables) {
		// set rotation
		Quaternion rotToPlr = new Quaternion();
		rotToPlr.lookAt(walkDirection, Vector3f.UNIT_Y);
		masterNode.setLocalRotation(rotToPlr);
		
		// detect collision
		Vector3f n = new Vector3f().set(masterNode.getLocalTranslation());
		CollisionResults results = new CollisionResults();
		Ray ray = new Ray(n, walkDirection);
		collidables.collideWith(ray, results);
		if (results.size() > 0) {
			Vector3f contact = results.getClosestCollision().getContactPoint();
			if (contact.distance(n.add(walkDirection)) < bound) walkDirection = Vector3f.ZERO;
		}
	}
	protected void move() {		
		// move
		masterNode.setLocalTranslation(masterNode.getLocalTranslation().add(walkDirection));
	}
	
	protected void rigging(Node collidables) {
		// for special movement
	}
		
	protected boolean purge() {
		return remove;
	}
	
	protected void setSpeed(float s) {
		speed = s;
	}
	protected void setWalkDirection(Vector3f w) {
		walkDirection.set(w);
	}
	
	protected Node getMasterNode() {
		return masterNode;
	}
	protected float getSpeed() {
		return speed;
	}
	protected Vector3f getWalkDirection() {
		return walkDirection;
	}
	protected String getTypeId() {
		return BADGUY_TYPE_ID;
	}
}
relavent parts of GameState Class
public class GameState extends SceneAppState {
	
	public static final String ENVIRO_NAME = "enviro", ENEMY_NODE_NAME = "enemy node";
	
	private BulletAppState bulletAppState;
	private Node guiNode;
	private Camera cam;
	private AssetManager assetManager;
	private ViewPort viewPort;
	private InputManager inputManager;
	
	private Player player = new Player("player");
	
	private final char BLOCK = '#';
	private final char START = 'P';
	private final char END = '@';
	private final char WINDOW = '+';
	private final char OVER_HEAD = '^';
	private final char ENEMY = '!';
	private final char NONE = '-';
	int size = 20;
	int mapSize = 25;
	int paths = 20;
	int pathLength = 20;
	
	int startPosX = 0;
	int startPosZ = 0;
	
	Node world = new Node("world");
	
	ArrayList<ArrayList> map = new ArrayList<>();
	Node enviro = new Node(ENVIRO_NAME);
	
	ArrayList<Badguy> enemyList = new ArrayList<>();
	Node enemyNode = new Node(ENEMY_NODE_NAME);
	int enemyId = 0;
	
	ArrayList<Goal> goalList = new ArrayList<>();
	Node goalNode = new Node("goals");
	
	private static final String PLAYING = "playing", PAUSED = "paused";
	
	AudioNode background;
	
	
	GameState(String id, Node root) {
		super(id, root);
	}
	
    
    @Override
    protected void initialize(Application app) {		
		bulletAppState = app.getStateManager().getState(BulletAppState.class);
		cam = app.getCamera();
		assetManager = app.getAssetManager();
		viewPort = app.getViewPort();
		inputManager = app.getInputManager();
		//masterNode = ((SimpleApplication)app).getRootNode();
		guiNode = ((SimpleApplication)app).getGuiNode();
		
		//bulletAppState.setDebugEnabled(true);
		
		viewPort.setBackgroundColor(new ColorRGBA(0.7f, 0.8f, 1f, 1f));
		//flyCam.setMoveSpeed(100);
		setUpSky();
		setUpLight();
		
		//reset();
		//initMap();
		
		playBackgroundMusic("Sounds/DST-RailJet-LongSeamlessLoop.ogg");
	}	
    @Override
    protected void cleanup(Application app) {
		clean();
	}	 
    @Override
    protected void onEnable() {
		init();
		//background.play();
	}	
	@Override
    protected void onDisable() {
		clean();
		//background.pause();
	}
    @Override
    public void update(float tpf) {
		if (isEnabled()) {
			player.update(tpf);
			player.setMove(cam);
			player.detectCollision(enviro);
			player.move(cam);
			player.shoot(cam, world);
			player.aim();
			player.detectEnemy(enemyList);
			player.detectGoal(goalList);
			player.displayDashboard(assetManager, guiNode);
			player.pause();
			if (player.getPause()) setEnabled(false);
			ArrayList<Badguy> removables = new ArrayList<>(), addables = new ArrayList<>();
			for (Badguy i : enemyList) {
				// movement
				i.setMove();
				i.rigging(enviro);
				i.debuncMove(enviro);
				i.move();
				
				// special
				if (i.willMinion()) {
					Badguy b = i.minion();
					b.setPos(i.getMasterNode().getLocalTranslation());
					//b.setTarget(player);
					addables.add(b);
				}
				
				// death anim
				i.doExplosion(assetManager);
				i.cleanExplosion();
				if (i.purge()) removables.add(i);
			}
			for (Badguy i : removables) {
				enemyList.remove(i);
				enemyNode.detachChildNamed(i.getMasterId());
			}
			for (Badguy i : addables) spawnEnemy(i);
			if (isComplete()) reset();
		}
	}
	
	private void initMap() {
		setMapData();		
		
		// maze blocks
		for (int i = 0; i < map.size(); i++) {
			ArrayList<Character> row = map.get(i);
			for (int j = 0; j < row.size(); j++) {
				switch (row.get(j)) {
					case BLOCK:
						makeBlock(j*size, i*size, 0);
					break;
					case START:
						startPosX = j*size;
						startPosZ = i*size;
						//makeOverHead(j*size, i*size);
					break;
					case END:
						if (Maze.GAME_TYPE.equals(Maze.TYPE_GET_END)) makeEnd(j*size, i*size);
						else {
							Mastermind mind = new Mastermind();
							mind.setPos(new Vector3f(j*size, 0, i*size));
							//mind.setTarget(player);
							spawnEnemy(mind);
						}
					break;
					case WINDOW:
						makeBlock(j*size, i*size, 0);
					break;
					case OVER_HEAD:
						//makeOverHead(j*size, i*size);
					break;
					case ENEMY:
						Badguy[] enemyTypes = createEnemyArray();
						int type = ThreadLocalRandom.current().nextInt(enemyTypes.length);
						Badguy enemy = enemyTypes[type];
						enemy.setPos(new Vector3f(j*size, 0, i*size));
						//enemy.setTarget(player);
						spawnEnemy(enemy);
					break;
				}
			}
		}
		
		// border blocks
		for (int i = 0; i < map.get(0).size(); i++) makeBlock(i*size, 0, 0);
		for (int i = 0; i < map.get(0).size(); i++) makeBlock(i*size, map.size()*size, 0);
		for (int i = 0; i < map.size(); i++) makeBlock(0, i*size, 0);
		for (int i = 0; i < map.size(); i++) makeBlock(map.get(0).size()*size, i*size, 0);
		
		// floor
		Box b = new Box(map.get(0).size()*size/2, 5, map.size()*size/2);
		b.scaleTextureCoordinates(new Vector2f(100, 100));
		Geometry g = new Geometry("BoxFloor", b);
		g.setLocalTranslation(map.get(0).size()*size/2, -5-5, map.size()*size/2);
		Material m = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
		Texture t = assetManager.loadTexture("Textures/metal_floor.jpg");
		t.setWrap(Texture.WrapMode.Repeat);
		m.setTexture("DiffuseMap", t);
		/*m.setBoolean("UseMaterialColors", true);
		m.setColor("Ambient", ColorRGBA.Gray);
		m.setColor("Diffuse", ColorRGBA.Orange);*/
		g.setMaterial(m);
		g.setShadowMode(RenderQueue.ShadowMode.Receive);
		enviro.attachChild(g);
		
		world.attachChild(enviro);
		world.attachChild(enemyNode);
		world.attachChild(goalNode);
		masterNode.attachChild(world);
		
		player.build(assetManager);
		player.initKeys(inputManager);
		player.setPos(new Vector3f(startPosX, 0, startPosZ));
		masterNode.attachChild(player.getNode());
		//bulletAppState.getPhysicsSpace().add(player.getPhys());
	}
	
	private void spawnEnemy(Badguy b) {
		b.setId("enemy#"+enemyId);
		enemyId++;
		enemyList.add(b);
		enemyNode.attachChild(b.getMasterNode());
		b.build(assetManager);
		b.initNodes();
		b.setTarget(player);
	}
		
	private boolean isComplete() {
		boolean complete = true;
		if (Maze.GAME_TYPE.equals(Maze.TYPE_SHOOT_MASTERS)) {
			if (!player.getStatus().equals(Player.DEAD)) for (Badguy i : enemyList) {
				if (i.getTypeId().equals(Mastermind.MASTERMIND_TYPE_ID)) {
					complete = false;
					break;
				}
			}
		}
		else if (!player.getStatus().equals(Player.DEAD) && !player.getStatus().equals(Player.VICTOR)) {
			complete = false;
		}
		return complete;
	}
	private void reset() {
		clean();
		init();
	}
	private void clean() {
		masterNode.detachAllChildren();
		guiNode.detachAllChildren();
		world.detachAllChildren();
		enviro.detachAllChildren();
		enemyNode.detachAllChildren();
		goalNode.detachAllChildren();
		map.clear();
		enemyList.clear();
		goalList.clear();
		enemyId = 0;
	}
	private void init() {
		player = new Player("player");
		mapSize = ThreadLocalRandom.current().nextInt(15, 30);
		paths = ThreadLocalRandom.current().nextInt(2, mapSize);
		setUpSky();
		initMap();
	}
		
	public static Badguy[] createEnemyArray() {
		Badguy[] list = {
			new Badguy(),
			new Badbot(),
			new Badcopter(),
		};
		return list;
	}
}

To give a concrete example of what’s going wrong

public class CorruptedZero {
    public static void main(String[] args){
        Vector3f someVector = Vector3f.ZERO;

        someVector.set(new Vector3f(1,2,3));

        System.out.println(Vector3f.ZERO); //eeeek! Prints 1,2,3
    }
}

The set method mutates. The reason Vector3f.ZERO and friends don’t protect against this (throwing an exception or similar on mutation) is performance, doing so would mean all other Vector3f’s run slower to be able to support it one way or another

So I guess it isn’t a good idea to use .ZERO, .UNIT_X, etc?

Its never a good idea to take ownership of them (set them to a variable). But things like Vector3f someVector = Vector3f.UNIT_Z.mult(getSpeed()) are fine (as long as nothing else corrupts them, as you’ve seen here, things go totally mad if you corrupt any of these constants).

But this same problem could happen with any object you are passing around. You need to always be clear who owns the object, other things shouldn’t be modifying it behind its owners back. And if you want a copy, you should take a copy, not a reference to the same object

1 Like

Yup, I got a front-row view of what it did. It seems to be working properly now, thanks you guys! :hugs:

2 Likes