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 FRAME_DELAY = 1 / FRAME_RATE * 1000; // in ms
const DEFAULT_SPRITE_SHEET_COUNT = 1; // number of default sprite sheets const DEFAULT_SPRITE_SHEET_COUNT = 1; // number of default sprite sheets
const WALK_SIZE = 8; // magnitude of position change, in px const WALK_SIZE = 8; // magnitude of position change, in px
const NUMERICAL_TOLERANCE = 0.01;
/** /**
* Enum of directions * Enum of directions
@ -49,17 +48,6 @@ const CreatureState = Object.freeze({
WALK_NORTHWEST: 16, 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 * @typedef {[number, number]} SpriteFrameOffset the client of the sprite with respect to
* the left/top background position client (off by factor of sprite size) * the left/top background position client (off by factor of sprite size)
@ -327,7 +315,7 @@ const getWalkStateFromDirection = (direction) => {
return CreatureState.WALK_WEST; 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; return CreatureState.WALK_NORTHWEST;
} }
@ -342,7 +330,7 @@ const getWalkStateFromDirection = (direction) => {
* @param {Array<Creature>} allCreatures all creatures rendered in the scene * @param {Array<Creature>} allCreatures all creatures rendered in the scene
* @return {(creature: Creature) => Creature} an updateCreature function * @return {(creature: Creature) => Creature} an updateCreature function
*/ */
const updateCreatureWithAllCreaturesContext = (updateCreature, allCreatures) => { const updateCreatureWithContext = (updateCreature, allCreatures) => {
const creaturesMap = Object.fromEntries(allCreatures.map( const creaturesMap = Object.fromEntries(allCreatures.map(
creature => [ creature.name, creature ] creature => [ creature.name, creature ]
)); ));
@ -453,8 +441,8 @@ const beginCreatureAnimation = (
*/ */
const isCreaturesColliding = (creature1, creature2) => { const isCreaturesColliding = (creature1, creature2) => {
return isIntersectingCircles( return isIntersectingCircles(
creature1.positionX, creature1.positionY, CREATURE_WIDTH * 3/4, creature1.positionX, creature1.positionY, CREATURE_WIDTH * 1/2,
creature2.positionX, creature2.positionY, CREATURE_WIDTH * 3/4, creature2.positionX, creature2.positionY, CREATURE_WIDTH * 1/2,
) )
} }
@ -541,63 +529,16 @@ const WALL_DIRECTION_TO_SCRATCH_STATE = Object.freeze({
* @return {Creature} updated creature * @return {Creature} updated creature
*/ */
const updateWalkStateCreature = (creature, allCreatures) => { const updateWalkStateCreature = (creature, allCreatures) => {
const newPositionX = creature.positionX + (creature.walkDirection?.at(0) ?? 0) * WALK_SIZE; // always play for at least 8 frames
const newPositionY = creature.positionY + (creature.walkDirection?.at(1) ?? 0) * WALK_SIZE; if (creature.stateDuration < 8 || !rollForNatN(10)) {
const newPositionCreature = {
...creature,
positionX: newPositionX,
positionY: newPositionY,
stateDuration: creature.stateDuration + 1,
}
const wallCollisionDirection = isCreatureOutOfBounds({
...creature, positionX: newPositionX, positionY: newPositionY
})
if (wallCollisionDirection !== undefined) {
return { return {
...creature, ...creature,
state: WALL_DIRECTION_TO_SCRATCH_STATE[wallCollisionDirection] ?? CreatureState.IDLE, positionX: creature.positionX + (creature.walkDirection?.at(0) ?? 0) * WALK_SIZE,
walkDirection: undefined, positionY: creature.positionY + (creature.walkDirection?.at(1) ?? 0) * WALK_SIZE,
stateDuration: 0, stateDuration: creature.stateDuration + 1,
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,
),
} }
} }
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 }; 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) .filter((otherCreature) => otherCreature.name !== creature.name)
// move away from groups of 2 or more creatures
if (spottedCreatures.length > 1) { if (spottedCreatures.length > 1) {
// run away from the centroid of close creatures // run away from the centroid of close creatures
const centroidX = spottedCreatures.reduce( const centroidX = spottedCreatures.reduce(
@ -629,28 +571,17 @@ const CREATURE_STATE_TO_UPDATE_CREATURE_FUNCTION = Object.freeze({
(sum, spottedCreature) => sum + spottedCreature.positionY, 0 (sum, spottedCreature) => sum + spottedCreature.positionY, 0
) / spottedCreatures.length; ) / spottedCreatures.length;
return startCreatureWalkTowardDirection(creature, [ return startCreatureWalkTowardDirection(creature, [
creature.positionX - centroidX, 2 * creature.positionX - centroidX,
creature.positionY - centroidY, 2 * creature.positionY - centroidY,
]) ]);
} }
// move toward spotted creature, so long as doing so won't cause collision // if there's a creature in the view radius, move toward it 25% of the time
const updatedTowardSpottedCreature = spottedCreatures if (spottedCreatures.length > 0 && rollForNatN(4)) {
.map((otherCreature) => { const spottedCreature = /** @type {Creature} */ getRandomChoice(undefined, spottedCreatures);
const updatedCreature = startCreatureWalkTowardDirection(creature, [ return startCreatureWalkTowardDirection(creature, [
otherCreature.positionX, otherCreature.positionY, spottedCreature.positionX, spottedCreature.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;
} }
const newState = getRandomChoice( const newState = getRandomChoice(
@ -660,7 +591,7 @@ const CREATURE_STATE_TO_UPDATE_CREATURE_FUNCTION = Object.freeze({
CreatureState.SCRATCH_SELF, // 0.1 CreatureState.SCRATCH_SELF, // 0.1
CreatureState.TIRED, // 0.1 CreatureState.TIRED, // 0.1
CreatureState.WALK_NORTH, // 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.clientLeft, creature.container.clientLeft + creature.container.clientWidth),
getRandomInRange(creature.container.clientTop, creature.container.clientTop + creature.container.clientHeight), 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) => { [CreatureState.ALERT]: (creature, allCreatures) => {
if (creature.stateDuration < 3) { if (creature.stateDuration < 3) {
@ -715,7 +635,7 @@ const CREATURE_STATE_TO_UPDATE_CREATURE_FUNCTION = Object.freeze({
} }
if (newState === CreatureState.WALK_NORTH) { if (newState === CreatureState.WALK_NORTH) {
// walk toward center with some random client // walk toward random direction
return startCreatureWalkTowardDirection(creature, [ return startCreatureWalkTowardDirection(creature, [
getRandomInRange(creature.container.clientLeft, creature.container.clientLeft + creature.container.clientWidth), getRandomInRange(creature.container.clientLeft, creature.container.clientLeft + creature.container.clientWidth),
getRandomInRange(creature.container.clientTop, creature.container.clientTop + creature.container.clientHeight), 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, [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 = () => { window.onload = () => {
if ( if (
window.matchMedia(`(prefers-reduced-motion: reduce)`).matches window.matchMedia(`(prefers-reduced-motion: reduce)`).matches
@ -793,7 +779,6 @@ window.onload = () => {
return; return;
} }
// EXAMPLE SCRIPT
const kennelWindowEle = document.querySelector('#kennel-window'); const kennelWindowEle = document.querySelector('#kennel-window');
const creatures = Array(32).fill('test-creature') const creatures = Array(32).fill('test-creature')
.map((name, index) => createCreature( .map((name, index) => createCreature(
@ -809,9 +794,12 @@ window.onload = () => {
getRandomInRange(kennelWindowEle.clientTop, kennelWindowEle.clientTop + kennelWindowEle.clientHeight), getRandomInRange(kennelWindowEle.clientTop, kennelWindowEle.clientTop + kennelWindowEle.clientHeight),
)) ))
const updateCreature = updateCreatureWithAllCreaturesContext((creature, allCreatures) => { const updateCreature = updateCreatureWithContext((creature, allCreatures) => {
const updatedCreature = CREATURE_STATE_TO_UPDATE_CREATURE_FUNCTION[creature.state](creature, allCreatures); return resolveCollision(
return updatedCreature CREATURE_STATE_TO_UPDATE_CREATURE_FUNCTION[creature.state](creature, allCreatures),
allCreatures,
creature,
);
}, creatures); }, creatures);
creatures.forEach((creature) => beginCreatureAnimation(creature, updateCreature)) creatures.forEach((creature) => beginCreatureAnimation(creature, updateCreature))