Box3 weird/inconsistent behavior

Hello, I’m making a 3d version of Binding of Isaac for uni, I’m using Box3 for hitboxes and at first I was using the entire player model for the hitbox, passing it into setFromObject, and that worked because the player model measured the same on the x and z axes, but I plan on adding things to the model so it won’t work as intended, plus the hitbox is too big so instead I’m adding a BoxGeometry to the isaac model on creation that I will use as a hitbox, like this:

function genIsaac() {
	let isaac_skin_material = new THREE.MeshPhysicalMaterial( { color: 0xffcccc } );
	let isaac_head_geometry = new THREE.SphereGeometry( 0.4, 16, 8 );
	let isaac = new THREE.Mesh( isaac_head_geometry, isaac_skin_material );
	scene.add( isaac );

	let isaac_core_geometry = new THREE.BoxGeometry( 0.5, 1.275, 0.5 );
	let isaac_core_material = new THREE.MeshBasicMaterial( { color: 0x000000, visible: false } );
	let isaac_core = new THREE.Mesh( isaac_core_geometry, isaac_core_material );
	isaac_core.layers.toggle( BLOOM_SCENE );

	isaac.add( isaac_core );
	isaac_core.position.y = -0.2375;

and then I am checking for collisions in the function that updates the player’s position, like this:

isaac_pos.lerp( isaac_speed, 0.15 );
	isaac.position.x += isaac_pos.x / 10 * isaac.userData.speed;
	if ( collidesWithWall( isaac ) || ( collidesWithPedestal( isaac.children[0] ) && !isaac.userData.flying ) || ( collidesWithObstacle( isaac.children[0] ) && !isaac.userData.flying ) ) {
		isaac.position.x -= isaac_pos.x / 10 * isaac.userData.speed;
		while ( true ) {
			isaac.position.x += isaac_pos.x / 1000;
			if ( collidesWithWall( isaac ) || ( collidesWithPedestal( isaac.children[0] ) && !isaac.userData.flying ) || ( collidesWithObstacle( isaac.children[0] ) && !isaac.userData.flying ) ) {
				isaac.position.x -= isaac_pos.x / 1000;
				break;
			}
		}
	}
	isaac.position.z += isaac_pos.y / 10 * isaac.userData.speed;
	if ( collidesWithWall( isaac ) || ( collidesWithPedestal( isaac.children[0] ) && !isaac.userData.flying ) || ( collidesWithObstacle( isaac.children[0] ) && !isaac.userData.flying ) ) {
		isaac.position.z -= isaac_pos.y / 10 * isaac.userData.speed;
		while ( true ) {
			isaac.position.z += isaac_pos.y / 1000;
			if ( collidesWithWall( isaac ) || ( collidesWithPedestal( isaac.children[0] ) && !isaac.userData.flying ) || ( collidesWithObstacle( isaac.children[0] ) && !isaac.userData.flying ) ) {
				isaac.position.z -= isaac_pos.y / 1000;
				break;
			}
		}
	}

now, you may notice how I check for collisions with pedestals and obstacles passing isaac.children[0], which is the BoxGeometry mentioned earlier, but for walls I’m using the full model, and that is because the moment I pass the hitbox, instead of colliding with walls and justnot moving further but still being able to move sideways or backwards, it gets stuck in the wall, and what bothers me is that it also breaks the other two functions, meaning it starts behaving the wrong way when colliding with pedestals and obstacles as well, and I don’t understand why, especially since collidesWithWall and collidesWithObstacle are two practically identical functions, save for the lists used which store the objects that I want to check the collisions for:

function collidesWithWall( object ) {
	let wall_hitbox = new THREE.Box3();
	let object_hitbox = new THREE.Box3();
	if ( object.isObject3D ) {
		object_hitbox.setFromObject( object );
	} else {
		object_hitbox = object;
	}
	for ( let i = 0; i < outer_bounds.length; i++ ) {
		wall_hitbox.setFromObject( outer_bounds[i] );
		if ( wall_hitbox.intersectsBox( object_hitbox ) ) {
			return true;
		}
	}
	return false;
}

function collidesWithObstacle( object ) {
	let obstacle_hitbox = new THREE.Box3();
	let object_hitbox = new THREE.Box3();
	if ( object.isObject3D ) {
		object_hitbox.setFromObject( object );
	} else {
		object_hitbox = object;
	}
	for ( let i = 0; i < obstacle_hitboxes.length; i++ ) {
		obstacle_hitbox.setFromObject( obstacle_hitboxes[i] );
		if ( obstacle_hitbox.intersectsBox( object_hitbox ) ) {
			return true;
		}
	}
	return false;
}

function collidesWithPedestal( object ) {
	let pedestal_hitbox = new THREE.Box3();
	let object_hitbox = new THREE.Box3();
	object_hitbox.setFromObject( object );
	for ( let i = 0; i < pedestals.length; i++ ) {
		pedestal_hitbox.setFromObject( pedestals[i] );
		if ( pedestal_hitbox.intersectsBox( object_hitbox ) ) {
			if ( object.parent.userData.player && !pedestals[i].userData.taken ) {
				pedestals[i].userData.taken = true;
				pedestals[i].clear();
				addItem( pedestals[i].userData.id );
			}
			return true;
		}
	}
	return false;
}

Using isaac.children[0] for the other two functions works just fine, but everything breaks the moment I put it in collidesWithWall.

I can put up some footage of the issue if I haven’t made myself clear enough.

EDIT:
Video footage of the collision system when I pass isaac as argument for collidesWithWall, I made the hitbox visible so it’s more visually clear, the black box deals with the collisions with the rocks and the pedestals, and the full model deals with the collisions with the walls, but it should be all the black box:
2023-12-07 01-04-47.mkv (3.9 MB)

Video footage of the collision system when I pass isaac.children[0] as argument for collidesWithWall, as soon as I collide with a rock, a pedestal, or a wall, I get stuck:
2023-12-07 01-07-25.mkv (4.4 MB)

EDIT 2:
the full project on codepen, the code provided works as intended save for the fact that the full model is being used for wall collisions instead of the black box, to reproduce the issue replace “isaac” with “isaac.children[0]” for the collidesWithWall function at lines 1688, 1692, 1699 and 1703, you’ll find the collidesWith functions at lines 1324, 1399 and 1416:

I’m not sure what your specific issue is, but have you considered looking into something more robust and full featured for collisions, like the occtree approach used in this example:

https://threejs.org/examples/?q=fps#games_fps

Or the fantastic mesh-bvh library:

https://gkjohnson.github.io/three-mesh-bvh/example/bundle/characterMovement.html

Or perhaps the ammo physics library:

https://threejs.org/examples/?q=physics#physics_ammo_break

?

1 Like

Hey, first off, re-reading my post I see that someone other than me might not have a clear image of the issue with the description I gave, I will add some videos to make it more clear.

About using a more developped system for collisions, the thing is this is not really a long term project, I got less than two weeks to finish it and still other things to add to it, and it’s not really meant to be a full fledged functional optimised game either, what the teacher told us is that it’s ultimately meant to be pretty, so I want to keep it relatively simple and not embark in learning libraries or the use of data structures like octrees to make it better on a technical level because it’s just not going to be worth it, AABB hitboxes and vectors are more than enough for my purposes. I do appreciate your suggestions though, and I will keep them in mind for any future projects. (Also, we aren’t allowed to use any libraries that are not included in js by default other than Three.js, which is kind of a bummer but it is what it is, so mesh-bvh and others are not an option)

1 Like

Just to be sure… can you make sure you’re not getting any exceptions in the console?

And also like… your project looks pretty small… can you just dump it into a glitch, or codepen and we can diagnose that way?

Another longshot, maybe try pasting everything into chatgpt and see if it spots any logic bugs.

  1. Avoid using while loops in JS (let alone while (true) { ... }.) There’s quite literally only one viable case I’ve seen of it’s usage - it’s very blocking and runtime crashing on-error, preventing debugging.

  2. If you want to build quick-but-works small collision system, consider raycasting instead - without physics. Calculate movement direction vector (ie. direction normal * character speed), cast a ray from character position in the direction of that vector (and distance of character speed.) Check if the ray intersected with anything else than the character (ex. a wall.) If it did - do not move the character, that way it won’t get stuck in a wall. If it did not intersect anything - move the character as per player request. Unless you have 1000s of objects in the scene - this will not hit the performance even if you intersectObjects(scene.children).

  3. If you absolutely have to stay with Box3s - don’t touch the current code and make the bug displayed in the video happen. Then place console.log statements in your code and see where it gets stuck at - my wild guess is the while loops, since you’re moving the character unconditionally and the move it back only under specific conditions - which may have changed after the first movement you’ve caused.

@manthrax no exceptions in the console, all clean. I will put up the code on codepen so you can take a look and put the link in the original post.

@mjurczyk for now I’ll stick with Box3s, they make more sense in my mind and I can actually have a litteral box to represent the hitbox, but I don’t discard the raycast approach if Box3s just won’t work. But in any case I actually want to know why it’s not working anymore, to my understanding I’m only passing one object3D instead of another, and it’s only one of the three function that doesn’t work, the other two work perfectly fine with either of the two objects.
I actually already tried putting console.logs in the code but, like you, at first I thought the problem was coming from the while loop or at least from within the first if statement, but I tried this time putting console.logs before that, like this:

if ( collidesWithWall( isaac.children[0] ) ) {
		console.log( "new" );
		console.log( isaac.position.z );
	}
	isaac.position.z += isaac_pos.y / 10 * isaac.userData.speed;
	console.log( isaac.position.z );
	if ( collidesWithWall( isaac.children[0] ) || ( collidesWithPedestal( isaac.children[0] ) && !isaac.userData.flying ) || ( collidesWithObstacle( isaac.children[0] ) && !isaac.userData.flying ) ) {
		console.log( isaac.position.z );
		isaac.position.z -= isaac_pos.y / 10 * isaac.userData.speed;
		console.log( isaac.position.z );
		while ( true ) {
			isaac.position.z += isaac_pos.y / 1000;
			if ( collidesWithWall( isaac.children[0] ) || ( collidesWithPedestal( isaac.children[0] ) && !isaac.userData.flying ) || ( collidesWithObstacle( isaac.children[0] ) && !isaac.userData.flying ) ) {
				isaac.position.z -= isaac_pos.y / 1000;
				break;
			}
		}
	}

and this is the result:
image

as you can see the position just before the “new” is the same as just after, but the one after the “new” and the “new” itself only pops up if the object is already colliding with a wall, meaning the first time the hitbox moves and collides, it doesn’t trigger the condition inside the if statement and only does it on the next loop, at which point it is moved back and forth but it’s still colliding. Again, I don’t get why this happens, only with this function and not the other two, why it breaks the other two functions, why it doesn’t work now even though it was working before.

It’s unproductive trying to resolve custom code issue from screenshots. Raycasting solution is simpler - so I’d suggest going that way. Otherwise could you create a codepen / codesandbox that shows the issue? (There’s also quite a high chance that while creating the codepen you’ll figure out the bug yourself.)

I just created the codepen, it’s on the second edit on the original post, but I don’t know how to make three.js be used on codepen, I know there’s a way to import libraries with URLs but we are not allowed to do so, the project needs to work without internet connection, so we have to provide all the library files needed for it to work, that’s why I’m using folder paths for the imports.

Feel free to use this as a template (you can either import via HTML tags, or via Settings > JS > Add Packages.)

nevermind, the codepen is working now, you can go ahead an try it yourself, the problematic code is around line 1700 and the collidesWith functions are at lines 1324, 1399 and 1416.

1 Like

Found something?

Ey, I was running your codepen and the collision seems pretty solid? How do I trigger the bad thing?

I edited some instructions in the original post, to reproduce the issue replace “isaac” with “isaac.children[0]” for the collidesWithWall function at lines 1688, 1692, 1699 and 1703, you’ll find the collidesWith functions at lines 1324, 1399 and 1416. After that run into a wall and you should get stuck.

Update, after some further testing I realised that what was causing problems was the first condition in the if statement if it was using isaac.children[0], regardles of what function that was, so I made a collidesWithDecoy function that behaves the same way as the other three but always returns false:

function collidesWithDecoy( object ) {
	let wall_hitbox = new THREE.Box3();
	let object_hitbox = new THREE.Box3();
	if ( object.isObject3D ) {
		object_hitbox.setFromObject( object );
	} else {
		object_hitbox = object;
	}
	for ( let i = 0; i < outer_bounds.length; i++ ) {
		wall_hitbox.setFromObject( outer_bounds[i] );
	}
	return false;
}

and passed isaac as an argument to it and isaac.children[0] to the other three. I still don’t understand why this happens but it does and now it’s fixed, so I guess it’s time to stop asking questions.