6663: New spectate mode (multiview) r=def- a=Vy0x2

<!-- What is the motivation for the changes of this pull request? -->

Adding a new spectator mode, that can spectate a team by moving the camera in the middle and zooming in/out
Works with team 0 or any team, but you have to work with it a bit to get used to it. I tried to make it as intuitive as possible.
Pr is as ready as it can get, please test it.

![screenshot_2023-06-30_19-17-42](https://github.com/ddnet/ddnet/assets/24738662/0446d568-d34b-4d14-8682-dd077f121e91)

Youtube video:<a href="http://www.youtube.com/watch?feature=player_embedded&v=7GM6DA3EYAI" target="_blank">
 <img src="http://img.youtube.com/vi/7GM6DA3EYAI/maxresdefault.jpg" alt="Watch the video" width="192" height="108"/>
</a>


<!-- Note that builds and other checks will be run for your change. Don't feel intimidated by failures in some of the checks. If you can't resolve them yourself, experienced devs can also resolve them before merging your pull request. -->

## Checklist

- [x] Tested the change ingame
- [x] Provided screenshots if it is a visual change
- [x] Tested in combination with possibly related configuration options
- [ ] Written a unit test (especially base/) or added coverage to integration test
- [x] Considered possible null pointers and out of bounds array indexing
- [x] Changed no physics that affect existing maps
- [ ] Tested the change with [ASan+UBSan or valgrind's memcheck](https://github.com/ddnet/ddnet/#using-addresssanitizer--undefinedbehavioursanitizer-or-valgrinds-memcheck) (optional)


Co-authored-by: devdenn <denispaul43@gmail.com>
Co-authored-by: Vy0x2 <denispaul43@gmail.com>
This commit is contained in:
bors[bot] 2023-07-07 18:24:44 +00:00 committed by GitHub
commit 6dcb2d1828
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 355 additions and 17 deletions

View file

@ -31,7 +31,7 @@ float CCamera::ZoomProgress(float CurrentTime) const
void CCamera::ScaleZoom(float Factor)
{
float CurrentTarget = m_Zooming ? m_ZoomSmoothingTarget : m_Zoom;
ChangeZoom(CurrentTarget * Factor);
ChangeZoom(CurrentTarget * Factor, g_Config.m_ClSmoothZoomTime);
}
float CCamera::MaxZoomLevel()
@ -44,7 +44,7 @@ float CCamera::MinZoomLevel()
return 0.01f;
}
void CCamera::ChangeZoom(float Target)
void CCamera::ChangeZoom(float Target, int Smoothness)
{
if(Target > MaxZoomLevel() || Target < MinZoomLevel())
{
@ -64,7 +64,7 @@ void CCamera::ChangeZoom(float Target)
m_ZoomSmoothingTarget = Target;
m_ZoomSmoothing = CCubicBezier::With(Current, Derivative, 0, m_ZoomSmoothingTarget);
m_ZoomSmoothingStart = Now;
m_ZoomSmoothingEnd = Now + (float)g_Config.m_ClSmoothZoomTime / 1000;
m_ZoomSmoothingEnd = Now + (float)Smoothness / 1000;
m_Zooming = true;
}
@ -198,6 +198,9 @@ void CCamera::ConZoomPlus(IConsole::IResult *pResult, void *pUserData)
if(pSelf->m_pClient->m_Snap.m_SpecInfo.m_Active || pSelf->GameClient()->m_GameInfo.m_AllowZoom || pSelf->Client()->State() == IClient::STATE_DEMOPLAYBACK)
{
pSelf->ScaleZoom(ZoomStep);
if(pSelf->GameClient()->m_MultiViewActivated)
pSelf->GameClient()->m_MultiViewPersonalZoom++;
}
}
void CCamera::ConZoomMinus(IConsole::IResult *pResult, void *pUserData)
@ -206,12 +209,19 @@ void CCamera::ConZoomMinus(IConsole::IResult *pResult, void *pUserData)
if(pSelf->m_pClient->m_Snap.m_SpecInfo.m_Active || pSelf->GameClient()->m_GameInfo.m_AllowZoom || pSelf->Client()->State() == IClient::STATE_DEMOPLAYBACK)
{
pSelf->ScaleZoom(1 / ZoomStep);
if(pSelf->GameClient()->m_MultiViewActivated)
pSelf->GameClient()->m_MultiViewPersonalZoom--;
}
}
void CCamera::ConZoom(IConsole::IResult *pResult, void *pUserData)
{
CCamera *pSelf = (CCamera *)pUserData;
float TargetLevel = pResult->NumArguments() ? pResult->GetFloat(0) : g_Config.m_ClDefaultZoom;
((CCamera *)pUserData)->ChangeZoom(std::pow(ZoomStep, TargetLevel - 10));
pSelf->ChangeZoom(std::pow(ZoomStep, TargetLevel - 10), g_Config.m_ClSmoothZoomTime);
if(pSelf->GameClient()->m_MultiViewActivated)
pSelf->GameClient()->m_MultiViewPersonalZoom = 0;
}
void CCamera::ConSetView(IConsole::IResult *pResult, void *pUserData)
{
@ -221,3 +231,8 @@ void CCamera::ConSetView(IConsole::IResult *pResult, void *pUserData)
clamp(pResult->GetInteger(0) * 32.0f, 200.0f, pSelf->Collision()->GetWidth() * 32 - 200.0f),
clamp(pResult->GetInteger(1) * 32.0f, 200.0f, pSelf->Collision()->GetWidth() * 32 - 200.0f));
}
void CCamera::SetZoom(float Target, int Smoothness)
{
ChangeZoom(Target, Smoothness);
}

View file

@ -30,7 +30,7 @@ class CCamera : public CComponent
float m_ZoomSmoothingEnd;
void ScaleZoom(float Factor);
void ChangeZoom(float Target);
void ChangeZoom(float Target, int Smoothness);
float ZoomProgress(float CurrentTime) const;
float MinZoomLevel();
@ -52,6 +52,8 @@ public:
virtual void OnConsoleInit() override;
virtual void OnReset() override;
void SetZoom(float Target, int Smoothness);
private:
static void ConZoomPlus(IConsole::IResult *pResult, void *pUserData);
static void ConZoomMinus(IConsole::IResult *pResult, void *pUserData);

View file

@ -106,7 +106,7 @@ void CHud::RenderGameTimer()
}
else if(m_pClient->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_RACETIME)
{
//The Warmup timer is negative in this case to make sure that incompatible clients will not see a warmup timer
// The Warmup timer is negative in this case to make sure that incompatible clients will not see a warmup timer
Time = (Client()->GameTick(g_Config.m_ClDummy) + m_pClient->m_Snap.m_pGameInfoObj->m_WarmupTimer) / Client()->GameTickSpeed();
}
else
@ -1474,7 +1474,7 @@ void CHud::RenderSpectatorHud()
// draw the text
char aBuf[128];
str_format(aBuf, sizeof(aBuf), "%s: %s", Localize("Spectate"), m_pClient->m_Snap.m_SpecInfo.m_SpectatorID != SPEC_FREEVIEW ? m_pClient->m_aClients[m_pClient->m_Snap.m_SpecInfo.m_SpectatorID].m_aName : Localize("Free-View"));
str_format(aBuf, sizeof(aBuf), "%s: %s", Localize("Spectate"), GameClient()->m_MultiViewActivated ? Localize("Multi-View") : m_pClient->m_Snap.m_SpecInfo.m_SpectatorID != SPEC_FREEVIEW ? m_pClient->m_aClients[m_pClient->m_Snap.m_SpecInfo.m_SpectatorID].m_aName : Localize("Free-View"));
TextRender()->Text(m_Width - 174.0f, m_Height - 15.0f + (15.f - 8.f) / 2.f, 8.0f, aBuf, -1.0f);
}
@ -1527,7 +1527,11 @@ void CHud::OnRender()
{
RenderAmmoHealthAndArmor(&m_pClient->m_Snap.m_aCharacters[SpectatorID].m_Cur);
}
if(SpectatorID != SPEC_FREEVIEW && m_pClient->m_Snap.m_aCharacters[SpectatorID].m_HasExtendedData && g_Config.m_ClShowhudDDRace && GameClient()->m_GameInfo.m_HudDDRace)
if(SpectatorID != SPEC_FREEVIEW &&
m_pClient->m_Snap.m_aCharacters[SpectatorID].m_HasExtendedData &&
g_Config.m_ClShowhudDDRace &&
(!GameClient()->m_MultiViewActivated || GameClient()->m_MultiViewShowHud) &&
GameClient()->m_GameInfo.m_HudDDRace)
{
RenderPlayerState(SpectatorID);
}

View file

@ -149,6 +149,17 @@ void CSpectator::ConSpectateClosest(IConsole::IResult *pResult, void *pUserData)
pSelf->Spectate(NewSpectatorID);
}
void CSpectator::ConMultiView(IConsole::IResult *pResult, void *pUserData)
{
CSpectator *pSelf = (CSpectator *)pUserData;
int Input = pResult->GetInteger(0);
if(Input == -1)
std::fill(std::begin(pSelf->GameClient()->m_aMultiViewId), std::end(pSelf->GameClient()->m_aMultiViewId), false); // remove everyone from multiview
else if(Input < MAX_CLIENTS && Input >= 0)
pSelf->GameClient()->m_aMultiViewId[Input] = !pSelf->GameClient()->m_aMultiViewId[Input]; // activate or deactivate one player from multiview
}
CSpectator::CSpectator()
{
OnReset();
@ -162,6 +173,7 @@ void CSpectator::OnConsoleInit()
Console()->Register("spectate_next", "", CFGFLAG_CLIENT, ConSpectateNext, this, "Spectate the next player");
Console()->Register("spectate_previous", "", CFGFLAG_CLIENT, ConSpectatePrevious, this, "Spectate the previous player");
Console()->Register("spectate_closest", "", CFGFLAG_CLIENT, ConSpectateClosest, this, "Spectate the closest player");
Console()->Register("spectate_multiview", "i[id]", CFGFLAG_CLIENT, ConMultiView, this, "Add/remove Client-IDs to spectate them exclusivly (-1 to reset)");
}
bool CSpectator::OnCursorMove(float x, float y, IInput::ECursorType CursorType)
@ -186,7 +198,12 @@ void CSpectator::OnRender()
if(m_WasActive)
{
if(m_SelectedSpectatorID != NO_SELECTION)
Spectate(m_SelectedSpectatorID);
{
if(m_SelectedSpectatorID != MULTI_VIEW)
Spectate(m_SelectedSpectatorID);
GameClient()->m_MultiViewActivated = m_SelectedSpectatorID == MULTI_VIEW;
}
m_WasActive = false;
}
return;
@ -213,6 +230,7 @@ void CSpectator::OnRender()
float TeeSizeMod = 1.0f;
float RoundRadius = 30.0f;
bool Selected = false;
bool MultiViewSelected = false;
int TotalPlayers = 0;
int PerLine = 8;
float BoxMove = -10.0f;
@ -253,34 +271,48 @@ void CSpectator::OnRender()
if((Client()->State() == IClient::STATE_DEMOPLAYBACK && m_pClient->m_DemoSpecID == SPEC_FREEVIEW) ||
(Client()->State() != IClient::STATE_DEMOPLAYBACK && m_pClient->m_Snap.m_SpecInfo.m_SpectatorID == SPEC_FREEVIEW))
{
Graphics()->DrawRect(Width / 2.0f - (ObjWidth - 20.0f), Height / 2.0f - 280.0f, 270.0f, 60.0f, ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), IGraphics::CORNER_ALL, 20.0f);
Graphics()->DrawRect(Width / 2.0f - (ObjWidth - 20.0f), Height / 2.0f - 280.0f, ((ObjWidth * 2.0f) / 3.0f) - 40.0f, 60.0f, ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), IGraphics::CORNER_ALL, 20.0f);
}
if(GameClient()->m_MultiViewActivated)
{
Graphics()->DrawRect(Width / 2.0f - (ObjWidth - 20.0f) + (ObjWidth * 2.0f / 3.0f), Height / 2.0f - 280.0f, ((ObjWidth * 2.0f) / 3.0f) - 40.0f, 60.0f, ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), IGraphics::CORNER_ALL, 20.0f);
}
if(Client()->State() == IClient::STATE_DEMOPLAYBACK && m_pClient->m_Snap.m_LocalClientID >= 0 && m_pClient->m_DemoSpecID == SPEC_FOLLOW)
{
Graphics()->DrawRect(Width / 2.0f - (ObjWidth - 310.0f), Height / 2.0f - 280.0f, 270.0f, 60.0f, ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), IGraphics::CORNER_ALL, 20.0f);
Graphics()->DrawRect(Width / 2.0f - (ObjWidth - 20.0f) + (ObjWidth * 2.0f * 2.0f / 3.0f), Height / 2.0f - 280.0f, ((ObjWidth * 2.0f) / 3.0f) - 40.0f, 60.0f, ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), IGraphics::CORNER_ALL, 20.0f);
}
if(m_SelectorMouse.x >= -(ObjWidth - 20.0f) && m_SelectorMouse.x <= -(ObjWidth - 290 + 10.0f) &&
if(m_SelectorMouse.x >= -(ObjWidth - 20.0f) && m_SelectorMouse.x <= -(ObjWidth - 20.0f) + ((ObjWidth * 2.0f) / 3.0f) - 40.0f &&
m_SelectorMouse.y >= -280.0f && m_SelectorMouse.y <= -220.0f)
{
m_SelectedSpectatorID = SPEC_FREEVIEW;
Selected = true;
}
TextRender()->TextColor(1.0f, 1.0f, 1.0f, Selected ? 1.0f : 0.5f);
TextRender()->Text(Width / 2.0f - (ObjWidth - 60.0f), Height / 2.0f - 280.f + (60.f - BigFontSize) / 2.f, BigFontSize, Localize("Free-View"), -1.0f);
TextRender()->Text(Width / 2.0f - (ObjWidth - 40.0f), Height / 2.0f - 280.f + (60.f - BigFontSize) / 2.f, BigFontSize, Localize("Free-View"), -1.0f);
if(m_SelectorMouse.x >= -(ObjWidth - 20.0f) + (ObjWidth * 2.0f / 3.0f) && m_SelectorMouse.x <= -(ObjWidth - 20.0f) + (ObjWidth * 2.0f / 3.0f) + ((ObjWidth * 2.0f) / 3.0f) - 40.0f &&
m_SelectorMouse.y >= -280.0f && m_SelectorMouse.y <= -220.0f)
{
m_SelectedSpectatorID = MULTI_VIEW;
MultiViewSelected = true;
}
TextRender()->TextColor(1.0f, 1.0f, 1.0f, MultiViewSelected ? 1.0f : 0.5f);
TextRender()->Text(Width / 2.0f - (ObjWidth - 40.0f) + (ObjWidth * 2.0f / 3.0f), Height / 2.0f - 280.f + (60.f - BigFontSize) / 2.f, BigFontSize, Localize("Multi-View"), -1.0f);
if(Client()->State() == IClient::STATE_DEMOPLAYBACK && m_pClient->m_Snap.m_LocalClientID >= 0)
{
Selected = false;
if(m_SelectorMouse.x > -(ObjWidth - 290.0f) && m_SelectorMouse.x <= -(ObjWidth - 590.0f) &&
if(m_SelectorMouse.x >= -(ObjWidth - 20.0f) + (ObjWidth * 2.0f * 2.0f / 3.0f) && m_SelectorMouse.x <= -(ObjWidth - 20.0f) + (ObjWidth * 2.0f * 2.0f / 3.0f) + ((ObjWidth * 2.0f) / 3.0f) - 40.0f &&
m_SelectorMouse.y >= -280.0f && m_SelectorMouse.y <= -220.0f)
{
m_SelectedSpectatorID = SPEC_FOLLOW;
Selected = true;
}
TextRender()->TextColor(1.0f, 1.0f, 1.0f, Selected ? 1.0f : 0.5f);
TextRender()->Text(Width / 2.0f - (ObjWidth - 350.0f), Height / 2.0f - 280.0f + (60.f - BigFontSize) / 2.f, BigFontSize, Localize("Follow"), -1.0f);
TextRender()->Text(Width / 2.0f - (ObjWidth - 40.0f) + (ObjWidth * 2.0f * 2.0f / 3.0f), Height / 2.0f - 280.0f + (60.f - BigFontSize) / 2.f, BigFontSize, Localize("Follow"), -1.0f);
}
float x = -(ObjWidth - 35.0f), y = StartY;

View file

@ -10,6 +10,7 @@ class CSpectator : public CComponent
{
enum
{
MULTI_VIEW = -4,
NO_SELECTION = -3,
};
@ -30,6 +31,7 @@ class CSpectator : public CComponent
static void ConSpectateNext(IConsole::IResult *pResult, void *pUserData);
static void ConSpectatePrevious(IConsole::IResult *pResult, void *pUserData);
static void ConSpectateClosest(IConsole::IResult *pResult, void *pUserData);
static void ConMultiView(IConsole::IResult *pResult, void *pUserData);
public:
CSpectator();

View file

@ -604,7 +604,11 @@ void CGameClient::UpdatePositions()
// spectator position
if(m_Snap.m_SpecInfo.m_Active)
{
if(Client()->State() == IClient::STATE_DEMOPLAYBACK && m_DemoSpecID != SPEC_FOLLOW && m_Snap.m_SpecInfo.m_SpectatorID != SPEC_FREEVIEW)
if(m_MultiViewActivated)
{
HandleMultiView();
}
else if(Client()->State() == IClient::STATE_DEMOPLAYBACK && m_DemoSpecID != SPEC_FOLLOW && m_Snap.m_SpecInfo.m_SpectatorID != SPEC_FREEVIEW)
{
m_Snap.m_SpecInfo.m_Position = mix(
vec2(m_Snap.m_aCharacters[m_Snap.m_SpecInfo.m_SpectatorID].m_Prev.m_X, m_Snap.m_aCharacters[m_Snap.m_SpecInfo.m_SpectatorID].m_Prev.m_Y),
@ -623,11 +627,28 @@ void CGameClient::UpdatePositions()
}
}
if(!m_MultiViewActivated && m_MultiView.m_IsInit)
ResetMultiView();
UpdateRenderedCharacters();
}
void CGameClient::OnRender()
{
// check if multi view got activated
if(!m_MultiView.m_IsInit && m_MultiViewActivated)
{
int TeamId = 0;
if(m_Snap.m_SpecInfo.m_SpectatorID >= 0)
TeamId = m_Teams.Team(m_Snap.m_SpecInfo.m_SpectatorID);
if(!InitMultiViewFromFreeview(TeamId))
{
dbg_msg("MultiView", "No players found to spectate");
m_MultiViewActivated = false;
}
}
// update the local character and spectate position
UpdatePositions();
@ -843,6 +864,18 @@ void CGameClient::OnMessage(int MsgId, CUnpacker *pUnpacker, int Conn, bool Dumm
pChar->ResetPrediction();
m_GameWorld.ReleaseHooked(pMsg->m_Victim);
}
// if we are spectating a static id set (team 0) and somebody killed, we remove him from the list
if(IsMultiViewIdSet() && m_aMultiViewId[pMsg->m_Victim] && !m_aClients[pMsg->m_Victim].m_Spec)
{
// is multi view even activated and we are not spectating a solo guy
if(m_MultiViewActivated && !m_MultiView.m_Solo && m_MultiView.m_Team == 0)
m_aMultiViewId[pMsg->m_Victim] = false;
// if everyone of a team killed, we have no ids to spectate anymore, so we disable multi view
if(!IsMultiViewIdSet())
m_MultiViewActivated = false;
}
}
}
@ -1668,7 +1701,17 @@ void CGameClient::OnNewSnapshot()
m_aDDRaceMsgSent[i] = true;
}
if(m_aShowOthers[g_Config.m_ClDummy] == SHOW_OTHERS_NOT_SET || m_aShowOthers[g_Config.m_ClDummy] != g_Config.m_ClShowOthers)
if(m_MultiViewActivated)
{
// dont show other teams while spectating in multi view
CNetMsg_Cl_ShowOthers Msg;
Msg.m_Show = SHOW_OTHERS_ONLY_TEAM;
Client()->SendPackMsgActive(&Msg, MSGFLAG_VITAL);
// update state
m_aShowOthers[g_Config.m_ClDummy] = SHOW_OTHERS_ONLY_TEAM;
}
else if(m_aShowOthers[g_Config.m_ClDummy] == SHOW_OTHERS_NOT_SET || m_aShowOthers[g_Config.m_ClDummy] != g_Config.m_ClShowOthers)
{
{
CNetMsg_Cl_ShowOthers Msg;
@ -3351,3 +3394,214 @@ void CGameClient::SnapCollectEntities()
m_vSnapEntities.push_back({Ent.m_Item, Ent.m_pData, pDataEx});
}
}
void CGameClient::HandleMultiView()
{
bool IsTeamZero = IsMultiViewIdSet();
bool Init = false;
int AmountPlayers = 0;
vec2 Minpos, Maxpos;
float TmpVel = 0.0f;
for(int i = 0; i < MAX_CLIENTS; i++)
{
// look at players who are vanished
if(m_MultiView.m_aVanish[i])
{
// not in freeze anymore and the delay is over
if(m_MultiView.m_aLastFreeze[i] + 6.0f <= Client()->LocalTime() && m_aClients[i].m_FreezeEnd == 0)
{
m_MultiView.m_aVanish[i] = false;
m_MultiView.m_aLastFreeze[i] = 0.0f;
}
}
// we look at team 0 and the player is not in the spec list
if(IsTeamZero && !m_aMultiViewId[i])
continue;
// player is vanished
if(m_MultiView.m_aVanish[i])
continue;
// the player is not in the team we are spectating
if(m_Teams.Team(i) != m_MultiView.m_Team)
continue;
vec2 PlayerPos;
if(m_Snap.m_aCharacters[i].m_Active)
PlayerPos = vec2(m_aClients[i].m_RenderPos.x, m_aClients[i].m_RenderPos.y);
else if(m_aClients[i].m_Spec) // tee is in spec
PlayerPos = m_aClients[i].m_SpecChar;
else
continue;
// player is far away and frozen
if(distance(m_MultiView.m_OldPos, PlayerPos) > 1100 && m_aClients[i].m_FreezeEnd != 0)
{
// check if the player is frozen for more than 3 seconds, if so vanish him
if(m_MultiView.m_aLastFreeze[i] == 0.0f)
m_MultiView.m_aLastFreeze[i] = Client()->LocalTime();
else if(m_MultiView.m_aLastFreeze[i] + 3.0f <= Client()->LocalTime())
m_MultiView.m_aVanish[i] = true;
}
else if(m_MultiView.m_aLastFreeze[i] != 0)
m_MultiView.m_aLastFreeze[i] = 0;
// set the minimum and maximum position
if(!Init)
{
Minpos = PlayerPos;
Maxpos = PlayerPos;
Init = true;
}
else
{
Minpos.x = std::min(Minpos.x, PlayerPos.x);
Maxpos.x = std::max(Maxpos.x, PlayerPos.x);
Minpos.y = std::min(Minpos.y, PlayerPos.y);
Maxpos.y = std::max(Maxpos.y, PlayerPos.y);
}
// sum up the velocity of all players we are spectating
const CNetObj_Character &CurrentCharacter = m_Snap.m_aCharacters[i].m_Cur;
TmpVel += (length(vec2(CurrentCharacter.m_VelX / 256.0f, CurrentCharacter.m_VelY / 256.0f)) * 50) / 32.0f;
AmountPlayers++;
}
// if we have found no players, we disable multi view
if(AmountPlayers == 0)
{
m_MultiViewActivated = false;
return;
}
vec2 TargetPos = vec2((Minpos.x + Maxpos.x) / 2.0f, (Minpos.y + Maxpos.y) / 2.0f);
// dont hide the position hud if its only one player
m_MultiViewShowHud = AmountPlayers == 1;
// get the average velocity
float AvgVel = clamp(TmpVel / AmountPlayers ? TmpVel / (float)AmountPlayers : 0.0f, 0.0f, 1000.0f);
if(m_MultiView.m_OldPersonalZoom == m_MultiViewPersonalZoom)
m_Camera.SetZoom(CalculateMultiViewZoom(Minpos, Maxpos, AvgVel), g_Config.m_ClMultiViewZoomSmoothness);
else
m_Camera.SetZoom(CalculateMultiViewZoom(Minpos, Maxpos, AvgVel), 50);
m_Snap.m_SpecInfo.m_Position = m_MultiView.m_OldPos + ((TargetPos - m_MultiView.m_OldPos) * CalculateMultiViewMultiplier(TargetPos));
m_MultiView.m_OldPos = m_Snap.m_SpecInfo.m_Position;
m_Snap.m_SpecInfo.m_UsePosition = true;
}
bool CGameClient::InitMultiViewFromFreeview(int Team)
{
float Width, Height;
CleanMultiViewIds();
m_MultiView.m_IsInit = true;
// get the current view coordinates
RenderTools()->CalcScreenParams(Graphics()->ScreenAspect(), m_Camera.m_Zoom, &Width, &Height);
vec2 AxisX = vec2(m_Camera.m_Center.x - (Width / 2), m_Camera.m_Center.x + (Width / 2));
vec2 AxisY = vec2(m_Camera.m_Center.y - (Height / 2), m_Camera.m_Center.y + (Height / 2));
m_MultiView.m_Team = Team;
if(Team > 0)
return true; // spectating a team, not necessary to search the players in view
else
{
int Count = 0;
for(int i = 0; i < MAX_CLIENTS; i++)
{
vec2 PlayerPos;
// get the position of the player
if(m_Snap.m_aCharacters[i].m_Active)
PlayerPos = vec2(m_Snap.m_aCharacters[i].m_Cur.m_X, m_Snap.m_aCharacters[i].m_Cur.m_Y);
else if(m_aClients[i].m_Spec)
PlayerPos = m_aClients[i].m_SpecChar;
else
continue;
// player isnt in the correct team
if(m_Teams.Team(i) != Team)
continue;
if(PlayerPos.x != 0 && PlayerPos.y != 0)
{
// is the player in view
if(PlayerPos.x > AxisX.x && PlayerPos.x < AxisX.y && PlayerPos.y > AxisY.x && PlayerPos.y < AxisY.y)
{
m_aMultiViewId[i] = true;
Count++;
}
}
}
// we are spectating only one player
m_MultiView.m_Solo = Count == 1;
// found players to spectate
return Count > 0;
}
}
float CGameClient::CalculateMultiViewMultiplier(vec2 CameraPos)
{
float MaxCameraDist = 200.0f;
float MinCameraDist = 20.0f;
float MaxVel = 0.1f;
float MinVel = 0.007f;
float CurrentCameraDistance = distance(m_MultiView.m_OldPos, CameraPos);
return clamp(MapValue(MaxCameraDist, MinCameraDist, MaxVel, MinVel, CurrentCameraDistance), MinVel, 1.0f);
}
float CGameClient::CalculateMultiViewZoom(vec2 MinPos, vec2 MaxPos, float Vel)
{
float Ratio = Graphics()->ScreenAspect();
float ZoomX = 0.0f, ZoomY;
// only calc two axis if the aspect ratio is not 1:1
if(Ratio != 1.0f)
ZoomX = (0.001309f - 0.000328 * Ratio) * (MaxPos.x - MinPos.x) + (0.741413f - 0.032959 * Ratio);
// calculate the according zoom with linear function
ZoomY = 0.001309f * (MaxPos.y - MinPos.y) + 0.741413f;
// choose the highest zoom
float Zoom = std::max(ZoomX, ZoomY);
// zoom out to maximum 10 percent of the current zoom for 70 velocity
float Diff = clamp(MapValue(70.0f, 15.0f, Zoom * 0.10f, 0.0f, Vel), 0.0f, Zoom * 0.10f);
// zoom should stay inbetween 1.1 and 20.0
Zoom = clamp(Zoom + Diff, 1.1f, 20.0f);
// add the user preference
Zoom -= (Zoom * 0.075f) * m_MultiViewPersonalZoom;
m_MultiView.m_OldPersonalZoom = m_MultiViewPersonalZoom;
return Zoom;
}
float CGameClient::MapValue(float MaxValue, float MinValue, float MaxRange, float MinRange, float Value)
{
return (MaxRange - MinRange) / (MaxValue - MinValue) * (Value - MinValue) + MinRange;
}
void CGameClient::ResetMultiView()
{
m_MultiView.m_Solo = false;
m_MultiView.m_IsInit = false;
m_MultiViewActivated = false;
m_MultiViewPersonalZoom = 0;
}
void CGameClient::CleanMultiViewIds()
{
std::fill(std::begin(m_aMultiViewId), std::end(m_aMultiViewId), false);
std::fill(std::begin(m_MultiView.m_aLastFreeze), std::end(m_MultiView.m_aLastFreeze), 0.0f);
std::fill(std::begin(m_MultiView.m_aVanish), std::end(m_MultiView.m_aVanish), false);
}
bool CGameClient::IsMultiViewIdSet()
{
return std::any_of(std::begin(m_aMultiViewId), std::end(m_aMultiViewId), [](bool IsSet) { return IsSet; });
}

View file

@ -698,6 +698,11 @@ public:
const std::vector<CSnapEntities> &SnapEntities() { return m_vSnapEntities; }
int m_MultiViewPersonalZoom;
bool m_MultiViewShowHud;
bool m_MultiViewActivated;
bool m_aMultiViewId[MAX_CLIENTS];
private:
std::vector<CSnapEntities> m_vSnapEntities;
void SnapCollectEntities();
@ -726,6 +731,28 @@ private:
float m_LastZoom;
float m_LastScreenAspect;
bool m_LastDummyConnected;
void ResetMultiView();
void HandleMultiView();
bool IsMultiViewIdSet();
void CleanMultiViewIds();
bool InitMultiViewFromFreeview(int Team);
float CalculateMultiViewMultiplier(vec2 CameraPos);
float CalculateMultiViewZoom(vec2 MinPos, vec2 MaxPos, float Vel);
float MapValue(float MaxValue, float MinValue, float MaxRange, float MinRange, float Value);
struct SMultiView
{
bool m_Solo;
bool m_IsInit;
bool m_aVanish[MAX_CLIENTS];
vec2 m_OldPos;
int m_Team;
int m_OldPersonalZoom;
float m_aLastFreeze[MAX_CLIENTS];
};
SMultiView m_MultiView;
};
ColorRGBA CalculateNameColor(ColorHSLA TextColorHSL);

View file

@ -90,6 +90,8 @@ MACRO_CONFIG_INT(ClDyncamFollowFactor, cl_dyncam_follow_factor, 60, 0, 200, CFGF
MACRO_CONFIG_INT(ClDyncamSmoothness, cl_dyncam_smoothness, 0, 0, 100, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Transition amount of the camera movement, 0=instant, 100=slow and smooth")
MACRO_CONFIG_INT(ClDyncamStabilizing, cl_dyncam_stabilizing, 0, 0, 100, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Amount of camera slowdown during fast cursor movement. High value can cause delay in camera movement")
MACRO_CONFIG_INT(ClMultiViewZoomSmoothness, cl_multi_view_zoom_smoothness, 1300, 0, 5000, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Set the smoothness of the multi view zoom (in ms, higher = slower)")
MACRO_CONFIG_INT(EdAutosaveInterval, ed_autosave_interval, 10, 0, 240, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Interval in minutes at which a copy of the current editor map is automatically saved to the 'auto' folder (0 for off)")
MACRO_CONFIG_INT(EdAutosaveMax, ed_autosave_max, 10, 0, 1000, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Maximum number of autosaves that are kept per map name (0 = no limit)")
MACRO_CONFIG_INT(EdSmoothZoomTime, ed_smooth_zoom_time, 250, 0, 5000, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Time of smooth zoom animation in the editor in ms (0 for off)")