fix: collision and movement distribution fixes

This commit is contained in:
B Wu 2024-08-14 11:20:46 -07:00
parent 90a3446bdd
commit 7e98ec63ac
1 changed files with 95 additions and 107 deletions

View File

@ -6,7 +6,6 @@ const FRAME_RATE = 6; // in FPS
const FRAME_DELAY = 1 / FRAME_RATE * 1000; // in ms
const DEFAULT_SPRITE_SHEET_COUNT = 1; // number of default sprite sheets
const WALK_SIZE = 8; // magnitude of position change, in px
const NUMERICAL_TOLERANCE = 0.01;
/**
* Enum of directions
@ -49,17 +48,6 @@ const CreatureState = Object.freeze({
WALK_NORTHWEST: 16,
})
const WALKING_CREATURE_STATES = Object.freeze([
CreatureState.WALK_NORTH,
CreatureState.WALK_NORTHEAST,
CreatureState.WALK_EAST,
CreatureState.WALK_SOUTHEAST,
CreatureState.WALK_SOUTH,
CreatureState.WALK_SOUTHWEST,
CreatureState.WALK_WEST,
CreatureState.WALK_NORTHWEST,
])
/**
* @typedef {[number, number]} SpriteFrameOffset the client of the sprite with respect to
* the left/top background position client (off by factor of sprite size)
@ -327,7 +315,7 @@ const getWalkStateFromDirection = (direction) => {
return CreatureState.WALK_WEST;
}
if (isBetween(directionAngle, -Math.PI * 5/8, -Math.PI * 7/8)) {
if (isBetween(directionAngle, -Math.PI * 7/8, -Math.PI * 5/8)) {
return CreatureState.WALK_NORTHWEST;
}
@ -342,7 +330,7 @@ const getWalkStateFromDirection = (direction) => {
* @param {Array<Creature>} allCreatures all creatures rendered in the scene
* @return {(creature: Creature) => Creature} an updateCreature function
*/
const updateCreatureWithAllCreaturesContext = (updateCreature, allCreatures) => {
const updateCreatureWithContext = (updateCreature, allCreatures) => {
const creaturesMap = Object.fromEntries(allCreatures.map(
creature => [ creature.name, creature ]
));
@ -453,8 +441,8 @@ const beginCreatureAnimation = (
*/
const isCreaturesColliding = (creature1, creature2) => {
return isIntersectingCircles(
creature1.positionX, creature1.positionY, CREATURE_WIDTH * 3/4,
creature2.positionX, creature2.positionY, CREATURE_WIDTH * 3/4,
creature1.positionX, creature1.positionY, CREATURE_WIDTH * 1/2,
creature2.positionX, creature2.positionY, CREATURE_WIDTH * 1/2,
)
}
@ -541,63 +529,16 @@ const WALL_DIRECTION_TO_SCRATCH_STATE = Object.freeze({
* @return {Creature} updated creature
*/
const updateWalkStateCreature = (creature, allCreatures) => {
const newPositionX = creature.positionX + (creature.walkDirection?.at(0) ?? 0) * WALK_SIZE;
const newPositionY = creature.positionY + (creature.walkDirection?.at(1) ?? 0) * WALK_SIZE;
const newPositionCreature = {
...creature,
positionX: newPositionX,
positionY: newPositionY,
stateDuration: creature.stateDuration + 1,
}
const wallCollisionDirection = isCreatureOutOfBounds({
...creature, positionX: newPositionX, positionY: newPositionY
})
if (wallCollisionDirection !== undefined) {
// always play for at least 8 frames
if (creature.stateDuration < 8 || !rollForNatN(10)) {
return {
...creature,
state: WALL_DIRECTION_TO_SCRATCH_STATE[wallCollisionDirection] ?? CreatureState.IDLE,
walkDirection: undefined,
stateDuration: 0,
positionX: constrain(
newPositionX,
creature.container.clientLeft + CREATURE_WIDTH / 2,
creature.container.clientLeft + creature.container.clientWidth - CREATURE_WIDTH / 2,
),
positionY: constrain(
newPositionY,
creature.container.clientTop + CREATURE_HEIGHT / 2,
creature.container.clientTop + creature.container.clientHeight - CREATURE_HEIGHT / 2,
),
positionX: creature.positionX + (creature.walkDirection?.at(0) ?? 0) * WALK_SIZE,
positionY: creature.positionY + (creature.walkDirection?.at(1) ?? 0) * WALK_SIZE,
stateDuration: creature.stateDuration + 1,
}
}
const collidedCreature = allCreatures.reduce((prevCollidedCreature, currentCreature) => {
if (prevCollidedCreature !== undefined) {
return prevCollidedCreature;
}
if (currentCreature.name === creature.name) {
return undefined;
}
if (isCreaturesColliding(newPositionCreature, currentCreature)) {
return currentCreature;
}
return undefined;
}, undefined);
if (collidedCreature !== undefined) {
return { ...creature, walkDirection: undefined, stateDuration: 0, state: CreatureState.ALERT };
}
if (creature.stateDuration < 8 || !rollForNatN(10)) {
// always play for at least 8 frames
return newPositionCreature;
}
return { ...creature, walkDirection: undefined, stateDuration: 0, state: CreatureState.IDLE };
}
@ -620,6 +561,7 @@ const CREATURE_STATE_TO_UPDATE_CREATURE_FUNCTION = Object.freeze({
))
.filter((otherCreature) => otherCreature.name !== creature.name)
// move away from groups of 2 or more creatures
if (spottedCreatures.length > 1) {
// run away from the centroid of close creatures
const centroidX = spottedCreatures.reduce(
@ -629,28 +571,17 @@ const CREATURE_STATE_TO_UPDATE_CREATURE_FUNCTION = Object.freeze({
(sum, spottedCreature) => sum + spottedCreature.positionY, 0
) / spottedCreatures.length;
return startCreatureWalkTowardDirection(creature, [
creature.positionX - centroidX,
creature.positionY - centroidY,
])
2 * creature.positionX - centroidX,
2 * creature.positionY - centroidY,
]);
}
// move toward spotted creature, so long as doing so won't cause collision
const updatedTowardSpottedCreature = spottedCreatures
.map((otherCreature) => {
const updatedCreature = startCreatureWalkTowardDirection(creature, [
otherCreature.positionX, otherCreature.positionY,
]);
if (isCreaturesColliding(updatedCreature, otherCreature)) {
return undefined;
}
return updatedCreature;
})
.filter((updatedCreature) => updatedCreature !== undefined)
.shift();
if (updatedTowardSpottedCreature !== undefined && rollForNatN(4)) {
// if there's a creature in the view radius, move toward it 25% of the time
return updatedTowardSpottedCreature;
// if there's a creature in the view radius, move toward it 25% of the time
if (spottedCreatures.length > 0 && rollForNatN(4)) {
const spottedCreature = /** @type {Creature} */ getRandomChoice(undefined, spottedCreatures);
return startCreatureWalkTowardDirection(creature, [
spottedCreature.positionX, spottedCreature.positionY,
]);
}
const newState = getRandomChoice(
@ -660,7 +591,7 @@ const CREATURE_STATE_TO_UPDATE_CREATURE_FUNCTION = Object.freeze({
CreatureState.SCRATCH_SELF, // 0.1
CreatureState.TIRED, // 0.1
CreatureState.WALK_NORTH, // 0.1
CreatureState.IDLE, // 0.1
CreatureState.IDLE, // 0.6
],
)
@ -676,22 +607,11 @@ const CREATURE_STATE_TO_UPDATE_CREATURE_FUNCTION = Object.freeze({
}
}
const updatedWalkingCreature = startCreatureWalkTowardDirection(creature, [
// transition to walking to random point
return startCreatureWalkTowardDirection(creature, [
getRandomInRange(creature.container.clientLeft, creature.container.clientLeft + creature.container.clientWidth),
getRandomInRange(creature.container.clientTop, creature.container.clientTop + creature.container.clientHeight),
]);
// check for collision
const collidingCreature = allCreatures
.filter((otherCreature) => otherCreature.name !== updatedWalkingCreature.name)
.filter((otherCreature) => isCreaturesColliding(updatedWalkingCreature, otherCreature))
.shift();
if (collidingCreature !== undefined) {
return { ...creature, stateDuration: creature.stateDuration + 1 };
}
return updatedWalkingCreature;
},
[CreatureState.ALERT]: (creature, allCreatures) => {
if (creature.stateDuration < 3) {
@ -715,7 +635,7 @@ const CREATURE_STATE_TO_UPDATE_CREATURE_FUNCTION = Object.freeze({
}
if (newState === CreatureState.WALK_NORTH) {
// walk toward center with some random client
// walk toward random direction
return startCreatureWalkTowardDirection(creature, [
getRandomInRange(creature.container.clientLeft, creature.container.clientLeft + creature.container.clientWidth),
getRandomInRange(creature.container.clientTop, creature.container.clientTop + creature.container.clientHeight),
@ -784,6 +704,72 @@ const CREATURE_STATE_TO_UPDATE_CREATURE_FUNCTION = Object.freeze({
[CreatureState.WALK_NORTHWEST]: updateWalkStateCreature,
})
/**
* Resolve collisions between the updated creature, the container bounds, and the other creatures.
* If the updated creature collides against the wall, move to a scratch direction state.
* If the updated creature collides with another creature, move to alert state.
* @param {Creature} updatedCreature the creature post update
* @param {Array<Creature>} allCreatures all creatures in kennel
* @param {Creature} creature previous creature state
* @return {Creature}
*/
const resolveCollision = (updatedCreature, allCreatures, creature) => {
// resolve creature collision
const collidedCreatures = allCreatures
.filter((otherCreature) => isCreaturesColliding(otherCreature, updatedCreature))
.filter((collidingCreature) => collidingCreature.name !== updatedCreature.name)
const collidedCreature = collidedCreatures.length > 0
? getRandomChoice(undefined, collidedCreatures)
: undefined;
// one third of the time, try to run away from the collided creature
if (collidedCreature !== undefined && rollForNatN(3)) {
const collidedCreature = getRandomChoice(undefined, collidedCreatures);
return resolveCollision(
startCreatureWalkTowardDirection(creature, [
creature.positionX - collidedCreature.positionX,
creature.positionY - collidedCreature.positionY,
]),
allCreatures,
creature,
)
}
if (collidedCreature !== undefined) {
return {
...creature,
state: getRandomChoice(undefined, [CreatureState.ALERT, CreatureState.IDLE, CreatureState.TIRED]),
stateDuration: 0,
walkDirection: undefined,
};
}
// resolve wall collision
const wallCollisionDirection = isCreatureOutOfBounds(updatedCreature);
if (wallCollisionDirection !== undefined) {
return {
...updatedCreature,
state: WALL_DIRECTION_TO_SCRATCH_STATE[wallCollisionDirection] ?? CreatureState.IDLE,
walkDirection: undefined,
stateDuration: 0,
positionX: constrain(
updatedCreature.positionX,
creature.container.clientLeft + CREATURE_WIDTH / 2,
creature.container.clientLeft + creature.container.clientWidth - CREATURE_WIDTH / 2,
),
positionY: constrain(
updatedCreature.positionY,
creature.container.clientTop + CREATURE_HEIGHT / 2,
creature.container.clientTop + creature.container.clientHeight - CREATURE_HEIGHT / 2,
),
}
}
return { ...updatedCreature };
}
window.onload = () => {
if (
window.matchMedia(`(prefers-reduced-motion: reduce)`).matches
@ -793,7 +779,6 @@ window.onload = () => {
return;
}
// EXAMPLE SCRIPT
const kennelWindowEle = document.querySelector('#kennel-window');
const creatures = Array(32).fill('test-creature')
.map((name, index) => createCreature(
@ -809,9 +794,12 @@ window.onload = () => {
getRandomInRange(kennelWindowEle.clientTop, kennelWindowEle.clientTop + kennelWindowEle.clientHeight),
))
const updateCreature = updateCreatureWithAllCreaturesContext((creature, allCreatures) => {
const updatedCreature = CREATURE_STATE_TO_UPDATE_CREATURE_FUNCTION[creature.state](creature, allCreatures);
return updatedCreature
const updateCreature = updateCreatureWithContext((creature, allCreatures) => {
return resolveCollision(
CREATURE_STATE_TO_UPDATE_CREATURE_FUNCTION[creature.state](creature, allCreatures),
allCreatures,
creature,
);
}, creatures);
creatures.forEach((creature) => beginCreatureAnimation(creature, updateCreature))