From 49898abfebd8003198f14bfa90555bc165bb4f84 Mon Sep 17 00:00:00 2001 From: James R Date: Tue, 3 Oct 2023 20:18:54 -0700 Subject: [PATCH] Let Director operate on separate splitscreens --- src/g_build_ticcmd.cpp | 6 +- src/k_director.cpp | 472 ++++++++++++++++++++++------------------- src/k_director.h | 2 +- src/p_mobj.c | 36 ++-- src/p_user.c | 5 +- 5 files changed, 281 insertions(+), 240 deletions(-) diff --git a/src/g_build_ticcmd.cpp b/src/g_build_ticcmd.cpp index b81c205e4..1e1f0bee5 100644 --- a/src/g_build_ticcmd.cpp +++ b/src/g_build_ticcmd.cpp @@ -253,13 +253,13 @@ class TiccmdBuilder if (M_MenuButtonPressed(pid, MBT_A)) { G_AdjustView(ssplayer, 1, true); - K_ToggleDirector(false); + K_ToggleDirector(forplayer(), false); } if (M_MenuButtonPressed(pid, MBT_X)) { G_AdjustView(ssplayer, -1, true); - K_ToggleDirector(false); + K_ToggleDirector(forplayer(), false); } if (player()->spectator == true) @@ -272,7 +272,7 @@ class TiccmdBuilder if (M_MenuButtonPressed(pid, MBT_R)) { - K_ToggleDirector(true); + K_ToggleDirector(forplayer(), true); } } diff --git a/src/k_director.cpp b/src/k_director.cpp index 64f82cb59..f04d2dfca 100644 --- a/src/k_director.cpp +++ b/src/k_director.cpp @@ -4,54 +4,30 @@ /// \brief SRB2kart automatic spectator camera. #include +#include + +#include "cxxutil.hpp" #include "k_kart.h" #include "k_respawn.h" #include "doomdef.h" +#include "doomstat.h" #include "g_game.h" #include "v_video.h" #include "k_director.h" #include "d_netcmd.h" #include "p_local.h" #include "g_party.h" +#include "command.h" -#define SWITCHTIME TICRATE * 5 // cooldown between unforced switches -#define BOREDOMTIME 3 * TICRATE / 2 // how long until players considered far apart? -#define TRANSFERTIME TICRATE // how long to delay reaction shots? -#define BREAKAWAYDIST 4000 // how *far* until players considered far apart? -#define WALKBACKDIST 600 // how close should a trailing player be before we switch? -#define PINCHDIST 30000 // how close should the leader be to be considered "end of race"? +extern "C" consvar_t cv_devmode_screen; -struct directorinfo -{ - boolean active; // is view point switching enabled? - tic_t cooldown; // how long has it been since we last switched? - tic_t freeze; // when nonzero, fixed switch pending, freeze logic! - INT32 attacker; // who to switch to when freeze delay elapses - INT32 maxdist; // how far is the closest player from finishing? - - INT32 sortedplayers[MAXPLAYERS]; // position-1 goes in, player index comes out. - INT32 gap[MAXPLAYERS]; // gap between a given position and their closest pursuer - INT32 boredom[MAXPLAYERS]; // how long has a given position had no credible attackers? -} directorinfo; - -void K_InitDirector(void) -{ - INT32 playernum; - - directorinfo.active = false; - directorinfo.cooldown = SWITCHTIME; - directorinfo.freeze = 0; - directorinfo.attacker = 0; - directorinfo.maxdist = 0; - - for (playernum = 0; playernum < MAXPLAYERS; playernum++) - { - directorinfo.sortedplayers[playernum] = -1; - directorinfo.gap[playernum] = INT32_MAX; - directorinfo.boredom[playernum] = 0; - } -} +#define SWITCHTIME TICRATE * 5 // cooldown between unforced switches +#define BOREDOMTIME 3 * TICRATE / 2 // how long until players considered far apart? +#define TRANSFERTIME TICRATE // how long to delay reaction shots? +#define BREAKAWAYDIST 4000 // how *far* until players considered far apart? +#define WALKBACKDIST 600 // how close should a trailing player be before we switch? +#define PINCHDIST 30000 // how close should the leader be to be considered "end of race"? static fixed_t K_GetFinishGap(INT32 leader, INT32 follower) { @@ -68,116 +44,257 @@ static fixed_t K_GetFinishGap(INT32 leader, INT32 follower) } } -static void K_UpdateDirectorPositions(void) +struct DirectorInfo { - INT32 playernum; - INT32 position; - player_t* target; + UINT8 viewnum; // which screen does this director apply to? + boolean active = false; // is view point switching enabled? + tic_t cooldown = SWITCHTIME; // how long has it been since we last switched? + tic_t freeze = 0; // when nonzero, fixed switch pending, freeze logic! + INT32 attacker = 0; // who to switch to when freeze delay elapses + INT32 maxdist = 0; // how far is the closest player from finishing? - memset(directorinfo.sortedplayers, -1, sizeof(directorinfo.sortedplayers)); - - for (playernum = 0; playernum < MAXPLAYERS; playernum++) + struct PlayerStat { - target = &players[playernum]; + INT32 sorted = -1; // position-1 goes in, player index comes out. + INT32 gap = INT32_MAX; // gap between a given position and their closest pursuer + INT32 boredom = 0; // how long has a given position had no credible attackers? + } + playerstat[MAXPLAYERS]; - if (playeringame[playernum] && !target->spectator && target->position > 0) + DirectorInfo(UINT8 viewnum_) : viewnum(viewnum_) {} + + INT32 viewplayernum() const { return displayplayers[viewnum]; } + player_t* viewplayer() const { return &players[viewplayernum()]; } + + void update() + { + INT32 targetposition; + + update_positions(); + + if (cooldown > 0) { + cooldown--; + } + + // handle pending forced switches + if (freeze > 0) { - directorinfo.sortedplayers[target->position - 1] = playernum; + if (!(--freeze)) + change(attacker, true); + + return; + } + + // if there's only one player left in the list, just switch to that player + if (playerstat[0].sorted != -1 && playerstat[1].sorted == -1) + { + change(playerstat[0].sorted, false); + return; + } + + // aaight, time to walk through the standings to find the first interesting pair + // NB: targetposition/PlayerStat::sorted is 0-indexed, aiming at the "back half" of a given pair by default. + // we adjust for this when comparing to player->position or when looking at the leading player, Don't Freak Out + for (targetposition = 1; targetposition < MAXPLAYERS; targetposition++) + { + INT32 target; + + // you are out of players, try again + if (playerstat[targetposition].sorted == -1) + { + break; + } + + // pair too far apart? try the next one + if (playerstat[targetposition - 1].boredom >= BOREDOMTIME) + { + continue; + } + + // pair finished? try the next one + if (players[playerstat[targetposition].sorted].exiting) + { + continue; + } + + // don't risk switching away from forward pairs at race end, might miss something! + if (maxdist > PINCHDIST) + { + // if the "next" player is close enough, they should be able to see everyone fine! + // walk back through the standings to find a vantage that gets everyone in frame. + // (also creates a pretty cool effect w/ overtakes at speed) + while (targetposition < MAXPLAYERS && playerstat[targetposition].gap < WALKBACKDIST) + { + targetposition++; + } + } + + target = playerstat[targetposition].sorted; + + // stop here since we're already viewing this player + if (viewplayernum() == target) + { + break; + } + + // if this is a splitscreen player, try next pair + if (P_IsDisplayPlayer(&players[target])) + { + continue; + } + + // if we're certain the back half of the pair is actually in this position, try to switch + if (!players[target].positiondelay) + { + change(target, false); + } + + // even if we're not certain, if we're certain we're watching the WRONG player, try to switch + if (viewplayer()->position != targetposition+1 && !viewplayer()->positiondelay) + { + change(target, false); + } + + break; } } - for (position = 0; position < MAXPLAYERS - 1; position++) + void force_switch(INT32 player, INT32 time) { - directorinfo.gap[position] = INT32_MAX; - - if (directorinfo.sortedplayers[position] == -1 || directorinfo.sortedplayers[position + 1] == -1) + if (players[player].exiting) { - continue; + return; } - directorinfo.gap[position] = P_ScaleFromMap(K_GetFinishGap(directorinfo.sortedplayers[position], directorinfo.sortedplayers[position + 1]), FRACUNIT); - - if (directorinfo.gap[position] >= BREAKAWAYDIST) - { - directorinfo.boredom[position] = std::min(BOREDOMTIME * 2, directorinfo.boredom[position] + 1); - } - else if (directorinfo.boredom[position] > 0) - { - directorinfo.boredom[position]--; - } + attacker = player; + freeze = time; } - directorinfo.maxdist = P_ScaleFromMap(players[directorinfo.sortedplayers[0]].distancetofinish, FRACUNIT); +private: + void update_positions() + { + INT32 playernum; + INT32 position; + player_t* target; + + for (PlayerStat& stat : playerstat) + { + stat.sorted = -1; + } + + for (playernum = 0; playernum < MAXPLAYERS; playernum++) + { + target = &players[playernum]; + + if (playeringame[playernum] && !target->spectator && target->position > 0) + { + playerstat[target->position - 1].sorted = playernum; + } + } + + for (position = 0; position < MAXPLAYERS - 1; position++) + { + playerstat[position].gap = INT32_MAX; + + if (playerstat[position].sorted == -1 || playerstat[position + 1].sorted == -1) + { + continue; + } + + playerstat[position].gap = P_ScaleFromMap( + K_GetFinishGap(playerstat[position].sorted, playerstat[position + 1].sorted), + FRACUNIT + ); + + if (playerstat[position].gap >= BREAKAWAYDIST) + { + playerstat[position].boredom = std::min(BOREDOMTIME * 2, playerstat[position].boredom + 1); + } + else if (playerstat[position].boredom > 0) + { + playerstat[position].boredom--; + } + } + + maxdist = P_ScaleFromMap(players[playerstat[0].sorted].distancetofinish, FRACUNIT); + } + + bool can_change() const + { + if (viewplayer()->trickpanel > 0) + { + return false; + } + + if (cooldown > 0) + { + return false; + } + + return true; + } + + void change(INT32 player, boolean force) + { + if (!active) + { + return; + } + + if (players[player].exiting) + { + return; + } + + if (!force && !can_change()) + { + return; + } + + G_ResetView(1 + viewnum, player, true); + cooldown = SWITCHTIME; + } +}; + +struct DirectorInfoManager +{ + DirectorInfo& operator [](UINT8 viewnum) + { + SRB2_ASSERT(viewnum < MAXSPLITSCREENPLAYERS); + return info_[viewnum]; + } + + auto begin() { return info_.begin(); } + auto end() { return std::next(begin(), r_splitscreen + 1); } + +private: + static_assert(MAXSPLITSCREENPLAYERS == 4); + std::array info_{0, 1, 2, 3}; } +g_directorinfo; -static boolean K_CanSwitchDirector(void) +void K_InitDirector(void) { - INT32 *displayplayerp = &displayplayers[0]; - - if (players[*displayplayerp].trickpanel > 0) - { - return false; - } - - if (directorinfo.cooldown > 0) - { - return false; - } - - return true; -} - -static void K_DirectorSwitch(INT32 player, boolean force) -{ - if (!directorinfo.active) - { - return; - } - - if (P_IsDisplayPlayer(&players[player])) - { - return; - } - - if (players[player].exiting) - { - return; - } - - if (!force && !K_CanSwitchDirector()) - { - return; - } - - G_ResetView(1, player, true); - directorinfo.cooldown = SWITCHTIME; -} - -static void K_DirectorForceSwitch(INT32 player, INT32 time) -{ - if (players[player].exiting) - { - return; - } - - directorinfo.attacker = player; - directorinfo.freeze = time; + g_directorinfo = {}; } void K_DirectorFollowAttack(player_t *player, mobj_t *inflictor, mobj_t *source) { - if (!P_IsDisplayPlayer(player)) + for (DirectorInfo& directorinfo : g_directorinfo) { - return; - } + if (directorinfo.viewplayer() != player) + { + continue; + } - if (inflictor && inflictor->player) - { - K_DirectorForceSwitch(inflictor->player - players, TRANSFERTIME); - } - else if (source && source->player) - { - K_DirectorForceSwitch(source->player - players, TRANSFERTIME); + if (inflictor && inflictor->player) + { + directorinfo.force_switch(inflictor->player - players, TRANSFERTIME); + } + else if (source && source->player) + { + directorinfo.force_switch(source->player - players, TRANSFERTIME); + } } } @@ -193,6 +310,8 @@ void K_DrawDirectorDebugger(void) return; } + DirectorInfo& directorinfo = g_directorinfo[cv_devmode_screen.value]; + V_DrawThinString(10, 0, V_70TRANS, va("PLACE")); V_DrawThinString(40, 0, V_70TRANS, va("CONF?")); V_DrawThinString(80, 0, V_70TRANS, va("GAP")); @@ -203,8 +322,8 @@ void K_DrawDirectorDebugger(void) for (position = 0; position < MAXPLAYERS - 1; position++) { ytxt = 10 * (position + 1); - leader = directorinfo.sortedplayers[position]; - follower = directorinfo.sortedplayers[position + 1]; + leader = directorinfo.playerstat[position].sorted; + follower = directorinfo.playerstat[position + 1].sorted; if (leader == -1 || follower == -1) break; @@ -217,15 +336,15 @@ void K_DrawDirectorDebugger(void) V_DrawThinString(40, ytxt, V_70TRANS, va("NG")); } - V_DrawThinString(80, ytxt, V_70TRANS, va("%d", directorinfo.gap[position])); + V_DrawThinString(80, ytxt, V_70TRANS, va("%d", directorinfo.playerstat[position].gap)); - if (directorinfo.boredom[position] >= BOREDOMTIME) + if (directorinfo.playerstat[position].boredom >= BOREDOMTIME) { V_DrawThinString(120, ytxt, V_70TRANS, va("BORED")); } else { - V_DrawThinString(120, ytxt, V_70TRANS, va("%d", directorinfo.boredom[position])); + V_DrawThinString(120, ytxt, V_70TRANS, va("%d", directorinfo.playerstat[position].boredom)); } V_DrawThinString(150, ytxt, V_70TRANS, va("%s", player_names[leader])); @@ -235,100 +354,16 @@ void K_DrawDirectorDebugger(void) void K_UpdateDirector(void) { - INT32 *displayplayerp = &displayplayers[0]; - INT32 targetposition; - - K_UpdateDirectorPositions(); - - if (directorinfo.cooldown > 0) { - directorinfo.cooldown--; - } - - // handle pending forced switches - if (directorinfo.freeze > 0) + for (DirectorInfo& directorinfo : g_directorinfo) { - if (!(--directorinfo.freeze)) - K_DirectorSwitch(directorinfo.attacker, true); - - return; - } - - // if there's only one player left in the list, just switch to that player - if (directorinfo.sortedplayers[0] != -1 && directorinfo.sortedplayers[1] == -1) - { - K_DirectorSwitch(directorinfo.sortedplayers[0], false); - return; - } - - // aaight, time to walk through the standings to find the first interesting pair - // NB: targetposition/sortedplayers is 0-indexed, aiming at the "back half" of a given pair by default. - // we adjust for this when comparing to player->position or when looking at the leading player, Don't Freak Out - for (targetposition = 1; targetposition < MAXPLAYERS; targetposition++) - { - INT32 target; - - // you are out of players, try again - if (directorinfo.sortedplayers[targetposition] == -1) - { - break; - } - - // pair too far apart? try the next one - if (directorinfo.boredom[targetposition - 1] >= BOREDOMTIME) - { - continue; - } - - // pair finished? try the next one - if (players[directorinfo.sortedplayers[targetposition]].exiting) - { - continue; - } - - // don't risk switching away from forward pairs at race end, might miss something! - if (directorinfo.maxdist > PINCHDIST) - { - // if the "next" player is close enough, they should be able to see everyone fine! - // walk back through the standings to find a vantage that gets everyone in frame. - // (also creates a pretty cool effect w/ overtakes at speed) - while (targetposition < MAXPLAYERS && directorinfo.gap[targetposition] < WALKBACKDIST) - { - targetposition++; - } - } - - target = directorinfo.sortedplayers[targetposition]; - - // stop here since we're already viewing this player - if (*displayplayerp == target) - { - break; - } - - // if this is a splitscreen player, try next pair - if (P_IsDisplayPlayer(&players[target])) - { - continue; - } - - // if we're certain the back half of the pair is actually in this position, try to switch - if (!players[target].positiondelay) - { - K_DirectorSwitch(target, false); - } - - // even if we're not certain, if we're certain we're watching the WRONG player, try to switch - if (players[*displayplayerp].position != targetposition+1 && !players[*displayplayerp].positiondelay) - { - K_DirectorSwitch(target, false); - } - - break; + directorinfo.update(); } } -void K_ToggleDirector(boolean active) +void K_ToggleDirector(UINT8 viewnum, boolean active) { + DirectorInfo& directorinfo = g_directorinfo[viewnum]; + if (directorinfo.active != active) { directorinfo.cooldown = 0; // switch immediately @@ -339,8 +374,7 @@ void K_ToggleDirector(boolean active) boolean K_DirectorIsEnabled(UINT8 viewnum) { - (void)viewnum; - return directorinfo.active; + return g_directorinfo[viewnum].active; } boolean K_DirectorIsAvailable(UINT8 viewnum) diff --git a/src/k_director.h b/src/k_director.h index 0de45dbc6..eeefb3e02 100644 --- a/src/k_director.h +++ b/src/k_director.h @@ -14,7 +14,7 @@ void K_InitDirector(void); void K_UpdateDirector(void); void K_DrawDirectorDebugger(void); void K_DirectorFollowAttack(player_t *player, mobj_t *inflictor, mobj_t *source); -void K_ToggleDirector(boolean active); +void K_ToggleDirector(UINT8 viewnum, boolean active); boolean K_DirectorIsEnabled(UINT8 viewnum); boolean K_DirectorIsAvailable(UINT8 viewnum); diff --git a/src/p_mobj.c b/src/p_mobj.c index 12fe75e93..d68232db3 100644 --- a/src/p_mobj.c +++ b/src/p_mobj.c @@ -50,6 +50,7 @@ #include "k_director.h" #include "m_easing.h" #include "k_podium.h" +#include "g_party.h" actioncache_t actioncachehead; @@ -12217,22 +12218,29 @@ void P_SpawnPlayer(INT32 playernum) } } - // Spectating when there is literally any other player in - // the level enables director cam. Or if the first player - // enters the game, spectate them. - // TODO: how do we support splitscreen? - if (playernum == consoleplayer || pcount == 1) - { - K_ToggleDirector(players[consoleplayer].spectator && pcount > 0); - } + boolean director = p->spectator && pcount > 0; - // TODO: handle splitscreen - // Spectators can switch to freecam. This should be - // disabled when they enter the race, or when the level - // changes. - if (playernum == consoleplayer && !demo.playback) + if (G_IsPartyLocal(playernum)) { - demo.freecam = false; + // Spectating when there is literally any other + // player in the level enables director cam. + K_ToggleDirector(G_PartyPosition(playernum), director); + + // Spectators can switch to freecam. This should be + // disabled when they enter the race, or when the level + // changes. + if (!demo.playback) + { + demo.freecam = false; + } + } + else if (pcount == 1) + { + // If the first player enters the game, view them. + for (i = 0; i <= r_splitscreen; ++i) + { + K_ToggleDirector(i, director); + } } } diff --git a/src/p_user.c b/src/p_user.c index 30266cb02..feab05130 100644 --- a/src/p_user.c +++ b/src/p_user.c @@ -2802,11 +2802,10 @@ static void P_DeathThink(player_t *player) } } - // TODO: support splitscreen // Spectate another player after 2 seconds - if (player == &players[consoleplayer] && playerGone == true && (gametyperules & GTR_BUMPERS) && player->deadtimer == 2*TICRATE) + if (G_IsPartyLocal(player - players) && playerGone == true && (gametyperules & GTR_BUMPERS) && player->deadtimer == 2*TICRATE) { - K_ToggleDirector(true); + K_ToggleDirector(G_PartyPosition(player - players), true); } // Keep time rolling