I am designing a roguelike and have a vision of the movement style I would like. I have devised a basic implementation of the acceleration and "snap" mechanics, however there are a few things that have me stuck.
The general idea is to have a tile based roguelike but with what I will call "freeform" character movement. Basically the entity ( player, monster, vehicle, etc ) has the ability to move independently from the tile grid while the movement keys are held. When the entity stops moving I would like it to decelerate, then smoothly "snap" to the closest valid grid cell. I would also like to retain the ability to move from tile to tile upon single movement keypresses. ( Normal roguelike movement ) ( Not currently implemented )
My current acceleration based movement works perfectly, smooth movement to the east and to the south, however the west and north movement is causeing the entity to "skip" tiles. I believe it is an issue with my rounding algorithim (last two lines of the stopMove() function ), but my below average math skills prevent me from figuring out if I'm right and if so, why. This same issue persists with the grid "snap" when the entity stops moving. To the east and south it snaps perfectly. West and north cause the entity to "snap" to the tile 7 tiles further than it should. ( instead of snapping to the closest tile )
Here is my relavant movement code so far: ( I'm sure there is room for major code cleansing in these. Sorry if you cringe while reading it. )
void Entity::update(Engine* engine) {
if (!Engine::keyState[SDLK_UP] && !Engine::keyState[SDLK_RIGHT] && !Engine::keyState[SDLK_DOWN] && !Engine::keyState[SDLK_LEFT]) {
stopMove();
}
if ((Engine::keyState[SDLK_UP] && Engine::keyState[SDLK_RIGHT])
|| (Engine::keyState[SDLK_RIGHT] && Engine::keyState[SDLK_DOWN])
|| (Engine::keyState[SDLK_DOWN] && Engine::keyState[SDLK_LEFT])
|| (Engine::keyState[SDLK_LEFT] && Engine::keyState[SDLK_UP]))
{
if (Engine::keyState[SDLK_UP] && Engine::keyState[SDLK_RIGHT]) {
accelX = 5.0;
accelY = -5.0;
}
if (Engine::keyState[SDLK_RIGHT] && Engine::keyState[SDLK_DOWN]) {
accelX = 5.0;
accelY = 5.0;
}
if (Engine::keyState[SDLK_DOWN] && Engine::keyState[SDLK_LEFT]) {
accelX = -5.0;
accelY = 5.0;
}
if (Engine::keyState[SDLK_LEFT] && Engine::keyState[SDLK_UP]) {
accelX = -5.0;
accelY = -5.0;
}
} else {
if (Engine::keyState[SDLK_LEFT]) {
accelX = -5.0;
accelY = 0.0;
speedY = 0;
}
if (Engine::keyState[SDLK_RIGHT]) {
accelX = 5.0;
accelY = 0.0;
speedY = 0;
}
if (Engine::keyState[SDLK_UP]) {
accelY = -5.0;
accelX = 0.0;
speedX = 0;
}
if (Engine::keyState[SDLK_DOWN]) {
accelY = 5.0;
accelX = 0.0;
speedX = 0;
}
}
if ( accelX > maxSpeedX || accelX < -maxSpeedX) { accelX = maxSpeedX; }
if ( accelY > maxSpeedY || accelY < -maxSpeedY) { accelY = maxSpeedY; }
if (flags & ENTITY_FLAG_GRAVITY) {
// accelY = 0.75f;
}
speedX += accelX * FPS::Control.getSpeedFactor();
speedY += accelY * FPS::Control.getSpeedFactor();
if (speedX > maxSpeedX) speedX = maxSpeedX;
if (speedX < -maxSpeedX) speedX = -maxSpeedX;
if (speedY > maxSpeedY) speedY = maxSpeedY;
if (speedY < -maxSpeedY) speedY = -maxSpeedY;
animate();
if ((Engine::keyState[SDLK_LSHIFT]) && (Engine::keyState[SDLK_UP]
|| Engine::keyState[SDLK_RIGHT]
|| Engine::keyState[SDLK_DOWN]
|| Engine::keyState[SDLK_LEFT]))
{
speedX = speedX * 3;
speedY = speedY * 3;
}
move(speedX, speedY);
}
void Entity::move(float moveX, float moveY) {
if (moveX == 0 && moveY == 0) {
return;
}
double newX = 0;
double newY = 0;
moveX *= FPS::Control.getSpeedFactor();
moveY *= FPS::Control.getSpeedFactor();
if (moveX != 0) {
if (moveX >= 0) {
newX = FPS::Control.getSpeedFactor();
} else {
newX = -FPS::Control.getSpeedFactor();
}
}
if (moveY != 0) {
if (moveY >= 0 ) {
newY = FPS::Control.getSpeedFactor();
} else {
newY = -FPS::Control.getSpeedFactor();
}
}
while (true) {
if (flags & ENTITY_FLAG_GHOST) {
positionValid((int)(x + newX), (int)(y + newY));
x += newX;
y += newY;
} else {
if (positionValid((int)(x + newX), (int)(y))) {
x += newX;
} else {
speedX = 0;
}
if (positionValid((int)(x), (int)(y + newY))) {
y += newY;
} else {
speedY = 0;
}
}
moveX += -newX;
moveY += -newY;
if (newX > 0 && moveX <= 0) {
newX = 0;
}
if (newX < 0 && moveX >= 0) {
newX = 0;
}
if (newY > 0 && moveY <= 0) {
newY = 0;
}
if (newY < 0 && moveY >= 0) {
newY = 0;
}
if (moveX == 0) {
newX = 0;
}
if (moveY == 0) {
newY = 0;
}
if (moveX == 0 && moveY == 0) {
break;
}
if (newX == 0 && newY == 0) {
break;
}
}
}
void Entity::stopMove() {
if (speedX > 0) {
accelX = -0.5;
}
if (speedY > 0) {
accelY = -0.5;
}
if (speedX < 0) {
accelX = 0.5;
}
if (speedY < 0) {
accelY = 0.5;
}
if (speedX < 3.0f && speedX > -3.0f) {
accelX = 0;
speedX = 0;
}
if (speedY < 3.0f && speedY > -3.0f) {
accelY = 0;
speedY = 0;
}
x = ((int)x / TILE_SIZE) * TILE_SIZE;
y = ((int)y / TILE_SIZE) * TILE_SIZE;
}
EDIT:: Forgot to give the values of the constants. TILE_SIZE is 16, maxSpeedX and maxSpeedY are both 5, and for FPS::Control.getSpeedFactor() it calculates ((SDL_GetTicks() - lastTime) / 1000.0f) * FRAMES_PER_SECOND with FRAMES_PER_SECOND being 32.