More mouse improvements (#1348)

* Check `oldlookdir` before disabling `centering`

When turning off mouselook/padlook, keep view centering enabled until `oldlookdir` is zero, meaning when the player's view actually appears centered.

* Move `viewangleoffset`

* Check dead state set by `P_KillMobj()`

* Smooth composite/mouse turning transitions

Blend interpolated composite input (e.g. keyboard) turning with direct mouse turning to prevent choppy transitions.

* Group inputs by composite/gamepad/mouse

* Update carry/input calculations

This also enables shorttics (lowres_turn) with fast mouse polling.

* Add mouse smoothing toggle

With mouse smoothing disabled (default), mouse movements are immediately reflected in each frame update, with the game simulation following behind. The higher the framerate, the lower the perceived input lag. With mouse smoothing enabled, mouse movements are delayed by up to one tic (~29 ms) due to interpolation. When recording older format demos with reduced turning resolution (or when using `-shorttics`), interpolation may still be preferred for smoother turning.

* Rename "mouse smoothing" to "raw mouse input"

* Skip calculations when using interpolation
This commit is contained in:
ceski 2023-12-23 15:29:10 -08:00 committed by GitHub
parent e0d30bbe6e
commit 1a9d580bd5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 288 additions and 215 deletions

View File

@ -174,9 +174,10 @@ int eventhead, eventtail;
//
void D_PostEvent(event_t *ev)
{
if (ev->type == ev_mouse && !menuactive && gamestate == GS_LEVEL && !paused)
if (ev->type == ev_mouse)
{
G_MouseMovementResponder(ev);
G_PrepTiccmd();
return;
}
@ -239,9 +240,9 @@ void D_Display (void)
// [AM] Figure out how far into the current tic we're in as a fixed_t.
fractionaltic = I_GetFracTime();
if (window_focused)
if (!menuactive && gamestate == GS_LEVEL && !paused && mouse_raw_input)
{
I_ReadMouse();
I_StartDisplay();
}
}

View File

@ -171,19 +171,18 @@ static int mousex;
static int mousey;
boolean dclick;
// Skip mouse if using a controller and recording in strict mode (DSDA rule).
static boolean skip_mouse = true;
typedef struct cmd_carry_s
typedef struct carry_s
{
double angle;
double pitch;
double strafe;
double side;
double vert;
} cmd_carry_t;
short lowres;
} carry_t;
static cmd_carry_t cmd_prev_carry;
static cmd_carry_t cmd_carry;
static carry_t prevcarry;
static carry_t carry;
static ticcmd_t basecmd;
boolean joyarray[MAX_JSB+1]; // [FG] support more joystick buttons
boolean *joybuttons = &joyarray[1]; // allow [-1]
@ -369,93 +368,101 @@ static void G_DemoSkipTics(void)
}
}
static int CarryError(double value, double *prev_carry, double *carry)
static int CarryError(double value, const double *prevcarry, double *carry)
{
const double desired = value + *prev_carry;
const double desired = value + *prevcarry;
const int actual = lround(desired);
*carry = desired - actual;
return actual;
}
static int CalcMouseAngle(int mousex)
static int CarryAngle(double angle)
{
if (mouseSensitivity_horiz)
{
const double angle = (I_AccelerateMouse(mousex) *
(mouseSensitivity_horiz + 5) * 8 / 10);
return CarryError(angle, &cmd_prev_carry.angle, &cmd_carry.angle);
}
else
{
cmd_prev_carry.angle = 0.0;
cmd_carry.angle = 0.0;
return 0;
}
return CarryError(angle, &prevcarry.angle, &carry.angle);
}
static int CalcMousePitch(int mousey)
static int CarryMousePitch(double pitch)
{
if (mouseSensitivity_vert_look)
{
const double pitch = (I_AccelerateMouse(mouse_y_invert ? -mousey : mousey) *
(mouseSensitivity_vert_look + 5) / 10);
return CarryError(pitch, &cmd_prev_carry.pitch, &cmd_carry.pitch);
}
else
{
cmd_prev_carry.pitch = 0.0;
cmd_carry.pitch = 0.0;
return 0;
}
return CarryError(pitch, &prevcarry.pitch, &carry.pitch);
}
static int CalcMouseStrafe(int mousex)
static int CarryMouseVert(double vert)
{
if (mouseSensitivity_horiz_strafe)
{
const double desired = (cmd_prev_carry.strafe + I_AccelerateMouse(mousex) *
(mouseSensitivity_horiz_strafe + 5) * 2 / 10);
const int actual = lround(desired * 0.5) * 2; // Even values only.
cmd_carry.strafe = desired - actual;
return actual;
}
else
{
cmd_prev_carry.strafe = 0.0;
cmd_carry.strafe = 0.0;
return 0;
}
return CarryError(vert, &prevcarry.vert, &carry.vert);
}
static int CalcMouseVert(int mousey)
static int CarryMouseSide(double side)
{
if (mouseSensitivity_vert)
{
const double vert = (I_AccelerateMouse(mousey) *
(mouseSensitivity_vert + 5) / 10);
return CarryError(vert, &cmd_prev_carry.vert, &cmd_carry.vert);
}
else
{
cmd_prev_carry.vert = 0.0;
cmd_carry.vert = 0.0;
return 0;
}
const double desired = side + prevcarry.side;
const int actual = lround(desired * 0.5) * 2; // Even values only.
carry.side = desired - actual;
return actual;
}
void G_MouseMovementResponder(const event_t *ev)
static short CarryLowResAngle(short angle)
{
if (strictmode && demorecording && skip_mouse)
return;
const short desired = angle + prevcarry.lowres;
// Round to nearest 256 for single byte turning. From Chocolate Doom.
const short actual = (desired + 128) & 0xFF00;
carry.lowres = desired - actual;
return actual;
}
mousex += ev->data2;
mousey += ev->data3;
static double CalcMouseAngle(int mousex)
{
if (!mouseSensitivity_horiz)
return 0.0;
return (I_AccelerateMouse(mousex) * (mouseSensitivity_horiz + 5) * 8 / 10);
}
static double CalcMousePitch(int mousey)
{
if (!mouseSensitivity_vert_look)
return 0.0;
return (I_AccelerateMouse(mousey) * direction[mouse_y_invert] *
(mouseSensitivity_vert_look + 5) / 10);
}
static double CalcMouseSide(int mousex)
{
if (!mouseSensitivity_horiz_strafe)
return 0.0;
return (I_AccelerateMouse(mousex) *
(mouseSensitivity_horiz_strafe + 5) * 2 / 10);
}
static double CalcMouseVert(int mousey)
{
if (!mouseSensitivity_vert)
return 0.0;
return (I_AccelerateMouse(mousey) * (mouseSensitivity_vert + 5) / 10);
}
void G_PrepTiccmd(void)
{
ticcmd_t *cmd = &basecmd;
if (!M_InputGameActive(input_strafe))
localview.angle = CalcMouseAngle(mousex);
{
localview.rawangle = -CalcMouseAngle(mousex);
cmd->angleturn = CarryAngle(localview.rawangle);
if (lowres_turn)
{
cmd->angleturn = CarryLowResAngle(cmd->angleturn);
}
localview.angle = cmd->angleturn << 16;
}
if (mouselook)
localview.pitch = CalcMousePitch(mousey);
{
const double pitch = CalcMousePitch(mousey);
cmd->lookdir = CarryMousePitch(pitch);
localview.pitch = cmd->lookdir;
}
}
//
@ -467,98 +474,67 @@ void G_MouseMovementResponder(const event_t *ev)
void G_BuildTiccmd(ticcmd_t* cmd)
{
boolean strafe;
int speed;
int tspeed;
int forward;
int side;
const boolean strafe = M_InputGameActive(input_strafe);
const boolean turnleft = M_InputGameActive(input_turnleft);
const boolean turnright = M_InputGameActive(input_turnright);
// [FG] speed key inverts autorun
const int speed = autorun ^ M_InputGameActive(input_speed); // phares
int angle = 0;
int pitch = 0;
int forward = 0;
int side = 0;
int newweapon; // phares
ticcmd_t *base;
extern boolean boom_weapon_state_injection;
static boolean done_autoswitch = false;
// Assume localview can be used unless mouse input is interrupted by other
// inputs that apply turning or looking up/down (e.g. keyboard or gamepad).
localview.useangle = !lowres_turn;
// inputs that apply looking up/down (e.g. gamepad).
localview.usepitch = true;
G_DemoSkipTics();
base = I_BaseTiccmd(); // empty, or external driver
memcpy(cmd, base, sizeof *cmd);
memcpy(cmd, &basecmd, sizeof(*cmd));
memset(&basecmd, 0, sizeof(basecmd));
cmd->consistancy = consistancy[consoleplayer][maketic%BACKUPTICS];
strafe = M_InputGameActive(input_strafe);
// [FG] speed key inverts autorun
speed = autorun ^ M_InputGameActive(input_speed); // phares
forward = side = 0;
// use two stage accelerative turning
// on the keyboard and joystick
if (M_InputGameActive(input_turnleft) ||
M_InputGameActive(input_turnright))
turnheld += ticdup;
else
turnheld = 0;
if (turnheld < SLOWTURNTICS)
tspeed = 2; // slow turn
else
tspeed = speed;
// Composite input
// turn 180 degrees in one keystroke? // phares
// |
if (STRICTMODE(M_InputGameActive(input_reverse))) // V
{
cmd->angleturn += (short)QUICKREVERSE; // ^
localview.useangle = false;
M_InputGameDeactivate(input_reverse); // |
} // phares
if (STRICTMODE(M_InputGameActive(input_reverse)))
{
angle += QUICKREVERSE;
M_InputGameDeactivate(input_reverse);
}
// let movement keys cancel each other out
if (turnleft || turnright)
{
turnheld += ticdup;
if (strafe)
if (strafe)
{
if (M_InputGameActive(input_turnright))
if (turnright)
side += sidemove[speed];
if (M_InputGameActive(input_turnleft))
if (turnleft)
side -= sidemove[speed];
if (analog_controls && controller_axes[axis_turn])
{
fixed_t x = axis_move_sens * controller_axes[axis_turn] / 10;
x = direction[invert_turn] * x;
side += FixedMul(sidemove[speed], x);
}
}
else
else
{
if (M_InputGameActive(input_turnright))
{
cmd->angleturn -= angleturn[tspeed];
localview.useangle = false;
}
if (M_InputGameActive(input_turnleft))
{
cmd->angleturn += angleturn[tspeed];
localview.useangle = false;
}
// use two stage accelerative turning on the keyboard and joystick
const int tspeed = ((turnheld < SLOWTURNTICS) ? 2 : speed);
if (analog_controls && controller_axes[axis_turn])
{
fixed_t x = controller_axes[axis_turn];
// response curve to compensate for lack of near-centered accuracy
x = FixedMul(FixedMul(x, x), x);
x = direction[invert_turn] * axis_turn_sens * x / 10;
cmd->angleturn -= FixedMul(angleturn[1], x);
localview.useangle = false;
}
if (turnright)
angle -= angleturn[tspeed];
if (turnleft)
angle += angleturn[tspeed];
}
}
else
{
turnheld = 0;
}
if (M_InputGameActive(input_forward))
forward += forwardmove[speed];
@ -569,14 +545,37 @@ void G_BuildTiccmd(ticcmd_t* cmd)
if (M_InputGameActive(input_strafeleft))
side -= sidemove[speed];
// Gamepad
if (analog_controls)
{
if (controller_axes[axis_turn])
{
if (strafe)
{
fixed_t x = axis_move_sens * controller_axes[axis_turn] / 10;
x = direction[invert_turn] * x;
side += FixedMul(sidemove[speed], x);
}
else
{
fixed_t x = controller_axes[axis_turn];
// response curve to compensate for lack of near-centered accuracy
x = FixedMul(FixedMul(x, x), x);
x = direction[invert_turn] * axis_turn_sens * x / 10;
angle -= FixedMul(angleturn[1], x);
}
}
if (controller_axes[axis_forward])
{
fixed_t y = axis_move_sens * controller_axes[axis_forward] / 10;
y = direction[invert_forward] * y;
forward -= FixedMul(forwardmove[speed], y);
}
if (controller_axes[axis_strafe])
{
fixed_t x = axis_move_sens * controller_axes[axis_strafe] / 10;
@ -592,12 +591,63 @@ void G_BuildTiccmd(ticcmd_t* cmd)
y = FixedMul(FixedMul(y, y), y);
y = direction[invert_look] * axis_look_sens * y / 10;
cmd->lookdir -= FixedMul(lookspeed[0], y);
localview.usepitch = false;
pitch -= FixedMul(lookspeed[0], y);
}
}
// buttons
// Mouse
if (strafe)
{
const double mouseside = CalcMouseSide(mousex);
side += CarryMouseSide(mouseside);
}
if (!mouselook && !novert)
{
const double mousevert = CalcMouseVert(mousey);
forward += CarryMouseVert(mousevert);
}
// Update/reset
if (angle)
{
angle = CarryAngle(localview.rawangle + angle);
if (lowres_turn)
{
angle = CarryLowResAngle(angle);
}
localview.ticangleturn = angle - cmd->angleturn;
cmd->angleturn = angle;
}
if (pitch)
{
cmd->lookdir = pitch;
localview.usepitch = false;
}
if (forward > MAXPLMOVE)
forward = MAXPLMOVE;
else if (forward < -MAXPLMOVE)
forward = -MAXPLMOVE;
if (side > MAXPLMOVE)
side = MAXPLMOVE;
else if (side < -MAXPLMOVE)
side = -MAXPLMOVE;
cmd->forwardmove = forward;
cmd->sidemove = side;
mousex = mousey = 0;
localview.angle = 0;
localview.pitch = 0;
localview.rawangle = 0.0;
prevcarry = carry;
// Buttons
cmd->chatchar = HU_dequeueChatChar();
if (M_InputGameActive(input_fire))
@ -716,33 +766,6 @@ void G_BuildTiccmd(ticcmd_t* cmd)
cmd->buttons |= BT_USE;
}
if (strafe)
side += CalcMouseStrafe(mousex);
else
cmd->angleturn -= localview.angle;
if (mouselook)
cmd->lookdir += localview.pitch;
else if (!novert)
forward += CalcMouseVert(mousey);
mousex = mousey = 0;
localview.angle = 0;
localview.pitch = 0;
cmd_prev_carry = cmd_carry;
if (forward > MAXPLMOVE)
forward = MAXPLMOVE;
else if (forward < -MAXPLMOVE)
forward = -MAXPLMOVE;
if (side > MAXPLMOVE)
side = MAXPLMOVE;
else if (side < -MAXPLMOVE)
side = -MAXPLMOVE;
cmd->forwardmove += forward;
cmd->sidemove += side;
// special buttons
if (sendpause)
{
@ -768,26 +791,6 @@ void G_BuildTiccmd(ticcmd_t* cmd)
sendjoin = false;
cmd->buttons |= BT_JOIN;
}
// low-res turning
if (lowres_turn)
{
static signed short carry = 0;
signed short desired_angleturn;
desired_angleturn = cmd->angleturn + carry;
// round angleturn to the nearest 256 unit boundary
// for recording demos with single byte values for turn
cmd->angleturn = (desired_angleturn + 128) & 0xff00;
// Carry forward the error from the reduced resolution to the
// next tic, so that successive small movements can accumulate.
carry = desired_angleturn - cmd->angleturn;
}
}
//
@ -898,8 +901,9 @@ static void G_DoLoadLevel(void)
memset (gamekeydown, 0, sizeof(gamekeydown));
mousex = mousey = 0;
memset(&localview, 0, sizeof(localview));
memset(&cmd_carry, 0, sizeof(cmd_carry));
memset(&cmd_prev_carry, 0, sizeof(cmd_prev_carry));
memset(&carry, 0, sizeof(carry));
memset(&prevcarry, 0, sizeof(prevcarry));
memset(&basecmd, 0, sizeof(basecmd));
sendpause = sendsave = paused = false;
// [FG] array size!
memset (mousearray, 0, sizeof(mousearray));
@ -970,7 +974,6 @@ static boolean G_StrictModeSkipEvent(event_t *ev)
{
first_event = false;
enable_mouse = true;
skip_mouse = false;
}
return !enable_mouse;
@ -991,6 +994,23 @@ static boolean G_StrictModeSkipEvent(event_t *ev)
return false;
}
boolean G_MouseMovementResponder(event_t *ev)
{
if (G_StrictModeSkipEvent(ev))
{
return true;
}
if (ev->type == ev_mouse)
{
mousex += ev->data2;
mousey += ev->data3;
return true;
}
return false;
}
//
// G_Responder
// Get info needed to make ticcmd_ts for the players.
@ -1105,8 +1125,10 @@ boolean G_Responder(event_t* ev)
return true;
}
if (G_StrictModeSkipEvent(ev))
if (G_MouseMovementResponder(ev))
{
return true; // eat events
}
switch (ev->type)
{
@ -1130,10 +1152,6 @@ boolean G_Responder(event_t* ev)
mousebuttons[ev->data1] = false;
return true;
case ev_mouse:
G_MouseMovementResponder(ev);
return true; // eat events
case ev_joyb_down:
if (ev->data1 < MAX_JSB)
joybuttons[ev->data1] = true;

View File

@ -30,7 +30,8 @@
#define MBF21_GAME_OPTION_SIZE (21 + MBF21_COMP_TOTAL)
void G_MouseMovementResponder(const event_t *ev);
void G_PrepTiccmd(void);
boolean G_MouseMovementResponder(event_t *ev);
boolean G_Responder(event_t *ev);
boolean G_CheckDemoStatus(void);
void G_DeathMatchSpawnPlayer(int playernum);

View File

@ -403,7 +403,6 @@ void I_ReadMouse(void)
int x, y;
static event_t ev;
SDL_PumpEvents();
SDL_GetRelativeMouseState(&x, &y);
if (x != 0 || y != 0)

View File

@ -45,6 +45,8 @@ void I_StartFrame (void);
void I_StartTic (void);
void I_StartDisplay(void);
// Asynchronous interrupt functions should maintain private queues
// that are read by the synchronous functions
// to be converted into events.

View File

@ -418,15 +418,25 @@ static void I_GetEvent(void)
//
void I_StartTic (void)
{
I_GetEvent();
if (window_focused)
{
I_ReadMouse();
}
I_GetEvent();
I_UpdateJoystick();
}
void I_StartDisplay(void)
{
if (window_focused)
{
SDL_PumpEvents();
I_ReadMouse();
}
}
//
// I_StartFrame
//

View File

@ -3941,6 +3941,7 @@ enum {
gen5_mouse3,
gen5_mouse_accel,
gen5_mouse_accel_threshold,
gen5_mouse_raw_input,
gen5_end1,
gen5_title2,
@ -4165,6 +4166,9 @@ setup_menu_t gen_settings5[] = { // General Settings screen5
{"Mouse threshold", S_NUM, m_null, M_X,
M_Y + gen5_mouse_accel_threshold * M_SPC, {"mouse_acceleration_threshold"}},
{"Raw mouse input", S_YESNO, m_null, M_X,
M_Y+ gen5_mouse_raw_input * M_SPC, {"mouse_raw_input"}},
{"", S_SKIP, m_null, M_X, M_Y + gen5_end1*M_SPC},
{"Miscellaneous" ,S_SKIP|S_TITLE, m_null, M_X, M_Y + gen5_title2*M_SPC},

View File

@ -2059,6 +2059,13 @@ default_t defaults[] = {
"adjust mouse acceleration threshold"
},
{
"mouse_raw_input",
(config_t *) &mouse_raw_input, NULL,
{1}, {0, 1}, number, ss_none, wad_no,
"Raw mouse input for turning/looking (0 = Interpolate, 1 = Raw)"
},
// [FG] invert vertical axis
{
"mouse_y_invert",

View File

@ -190,6 +190,12 @@ void P_MovePlayer (player_t* player)
mo->angle += cmd->angleturn << 16;
onground = mo->z <= mo->floorz;
if (player == &players[consoleplayer])
{
localview.ticangle += localview.ticangleturn << 16;
localview.ticangleturn = 0;
}
// killough 10/98:
//
// We must apply thrust to the player and bobbing separately, to avoid
@ -353,6 +359,11 @@ void P_PlayerThink (player_t* player)
player->oldlookdir = player->lookdir;
player->oldrecoilpitch = player->recoilpitch;
if (player == &players[consoleplayer])
{
localview.oldticangle = localview.ticangle;
}
// killough 2/8/98, 3/21/98:
// (this code is necessary despite questions raised elsewhere in a comment)
@ -386,7 +397,11 @@ void P_PlayerThink (player_t* player)
if (abs(player->lookdir) < 8 * MLOOKUNIT)
{
player->lookdir = 0;
player->centering = false;
if (player->oldlookdir == 0)
{
player->centering = false;
}
}
player->slope = PLAYER_SLOPE(player);

View File

@ -48,6 +48,7 @@ fixed_t projection;
fixed_t viewx, viewy, viewz;
angle_t viewangle;
localview_t localview;
boolean mouse_raw_input;
fixed_t viewcos, viewsin;
player_t *viewplayer;
extern lighttable_t **walllights;
@ -648,10 +649,12 @@ void R_SetupFrame (player_t *player)
leveltime > oldleveltime)
{
const boolean use_localview = (
// Don't use localview when interpolation is preferred.
mouse_raw_input &&
// Don't use localview if the player is spying.
player == &players[consoleplayer] &&
// Don't use localview if the player is dead.
player->health > 0 &&
player->playerstate != PST_DEAD &&
// Don't use localview if the player just teleported.
!player->mo->reactiontime &&
// Don't use localview if a demo is playing.
@ -667,12 +670,16 @@ void R_SetupFrame (player_t *player)
// Use localview unless the player or game is in an invalid state or if
// mouse input was interrupted, in which case fall back to interpolation.
if (localview.useangle && use_localview)
viewangle = player->mo->angle - ((short)localview.angle << FRACBITS) + viewangleoffset;
if (use_localview)
{
viewangle = (player->mo->angle + localview.angle - localview.ticangle +
R_InterpolateAngle(localview.oldticangle, localview.ticangle,
fractionaltic));
}
else
viewangle = R_InterpolateAngle(player->mo->oldangle, player->mo->angle, fractionaltic) + viewangleoffset;
viewangle = R_InterpolateAngle(player->mo->oldangle, player->mo->angle, fractionaltic);
if (localview.usepitch && use_localview && !player->centering && player->lookdir)
if (localview.usepitch && use_localview && !player->centering)
pitch = (player->lookdir + localview.pitch) / MLOOKUNIT;
else
pitch = (player->oldlookdir + (player->lookdir - player->oldlookdir) * FIXED2DOUBLE(fractionaltic)) / MLOOKUNIT;
@ -682,13 +689,17 @@ void R_SetupFrame (player_t *player)
}
else
{
viewx = player->mo->x;
viewy = player->mo->y;
viewz = player->viewz; // [FG] moved here
viewangle = player->mo->angle + viewangleoffset;
// [crispy] pitch is actual lookdir and weapon pitch
pitch = player->lookdir / MLOOKUNIT + player->recoilpitch;
viewx = player->mo->x;
viewy = player->mo->y;
viewz = player->viewz; // [FG] moved here
viewangle = player->mo->angle;
// [crispy] pitch is actual lookdir and weapon pitch
pitch = player->lookdir / MLOOKUNIT + player->recoilpitch;
}
// 3-screen display mode.
viewangle += viewangleoffset;
extralight = player->extralight;
extralight += STRICTMODE(LIGHTBRIGHT * extra_level_brightness);

View File

@ -81,6 +81,8 @@ extern lighttable_t *fixedcolormap;
// range of [0.0, 1.0). Used for interpolation.
extern fixed_t fractionaltic;
extern boolean mouse_raw_input;
// [AM] Interpolate between two angles.
angle_t R_InterpolateAngle(angle_t oangle, angle_t nangle, fixed_t scale);

View File

@ -90,8 +90,11 @@ extern side_t *sides;
typedef struct localview_s
{
boolean useangle;
boolean usepitch;
angle_t oldticangle;
angle_t ticangle;
int ticangleturn;
double rawangle;
int angle;
int pitch;
} localview_t;