Merge pull request #2461 from aws-lumberyard-dev/AzNetworkingMetrics
New AzNetworking metrics display plus connection quality debug widgets using ImGui
This commit is contained in:
@@ -22,6 +22,7 @@ namespace AzNetworking
|
||||
const AZ::TimeMs deltaTimeMs = currentTimeMs - m_lastLoggedTimeMs;
|
||||
|
||||
m_atoms[m_activeAtom].m_bytesTransmitted += byteCount;
|
||||
m_atoms[m_activeAtom].m_packetsSent++;
|
||||
m_atoms[m_activeAtom].m_timeAccumulatorMs += deltaTimeMs;
|
||||
|
||||
if (m_atoms[m_activeAtom].m_timeAccumulatorMs >= m_maxSampleTimeMs)
|
||||
@@ -32,6 +33,11 @@ namespace AzNetworking
|
||||
m_lastLoggedTimeMs = currentTimeMs;
|
||||
}
|
||||
|
||||
void DatarateMetrics::LogPacketLost()
|
||||
{
|
||||
m_atoms[m_activeAtom].m_packetsLost++;
|
||||
}
|
||||
|
||||
float DatarateMetrics::GetBytesPerSecond() const
|
||||
{
|
||||
const uint32_t sampleAtom = 1 - m_activeAtom;
|
||||
@@ -47,6 +53,18 @@ namespace AzNetworking
|
||||
return (bytesLogged * 1000.0f) / sampleTime; // (* 1000) to convert from bytes per millisecond to bytes per second
|
||||
}
|
||||
|
||||
float DatarateMetrics::GetLossRatePercent() const
|
||||
{
|
||||
const uint32_t sampleAtom = 1 - m_activeAtom;
|
||||
|
||||
if (m_atoms[sampleAtom].m_packetsSent == 0)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
return float(m_atoms[sampleAtom].m_packetsLost) / float(m_atoms[sampleAtom].m_packetsSent);
|
||||
}
|
||||
|
||||
void ConnectionComputeRtt::LogPacketSent(PacketId packetId, AZ::TimeMs currentTimeMs)
|
||||
{
|
||||
for (uint32_t i = 0; i < MaxTrackableEntries; i++)
|
||||
|
||||
@@ -19,8 +19,10 @@ namespace AzNetworking
|
||||
{
|
||||
DatarateAtom() = default;
|
||||
|
||||
AZ::TimeMs m_timeAccumulatorMs = AZ::TimeMs{ 0 };
|
||||
uint32_t m_bytesTransmitted = 0;
|
||||
AZ::TimeMs m_timeAccumulatorMs = AZ::TimeMs{0};
|
||||
uint32_t m_packetsSent = 0;
|
||||
uint32_t m_packetsLost = 0;
|
||||
};
|
||||
|
||||
//! @class DatarateMetrics
|
||||
@@ -40,19 +42,26 @@ namespace AzNetworking
|
||||
//! @param currentTimeMs current process time in milliseconds
|
||||
void LogPacket(uint32_t byteCount, AZ::TimeMs currentTimeMs);
|
||||
|
||||
//! Invoked whenever a packet has determined to be lost.
|
||||
void LogPacketLost();
|
||||
|
||||
//! Retrieve a sample of the datarate being incurred by this connection in bytes per second.
|
||||
//! @return datarate for traffic sent to or from the connection in bytes per second
|
||||
float GetBytesPerSecond() const;
|
||||
|
||||
//! Returns the estimated packet loss rate as a percentage of packets.
|
||||
//! @return the estimated percentage loss rate
|
||||
float GetLossRatePercent() const;
|
||||
|
||||
private:
|
||||
|
||||
//! Used internally to swap buffers used for metric gathering.
|
||||
void SwapBuffers();
|
||||
|
||||
static constexpr AZ::TimeMs MaxSampleTimeMs = AZ::TimeMs{500};
|
||||
static constexpr AZ::TimeMs MaxSampleTimeMs = AZ::TimeMs{ 2000 };
|
||||
|
||||
AZ::TimeMs m_maxSampleTimeMs = MaxSampleTimeMs;
|
||||
AZ::TimeMs m_lastLoggedTimeMs = MaxSampleTimeMs;
|
||||
AZ::TimeMs m_maxSampleTimeMs = MaxSampleTimeMs;
|
||||
AZ::TimeMs m_lastLoggedTimeMs = MaxSampleTimeMs;
|
||||
uint32_t m_activeAtom = 0;
|
||||
DatarateAtom m_atoms[2];
|
||||
};
|
||||
@@ -69,7 +78,7 @@ namespace AzNetworking
|
||||
ConnectionPacketEntry(PacketId packetId, AZ::TimeMs sendTimeMs);
|
||||
|
||||
PacketId m_packetId = InvalidPacketId;
|
||||
AZ::TimeMs m_sendTimeMs = AZ::TimeMs{0};
|
||||
AZ::TimeMs m_sendTimeMs = AZ::TimeMs{0};
|
||||
};
|
||||
|
||||
//! @class ConnectionComputeRtt
|
||||
@@ -100,8 +109,8 @@ namespace AzNetworking
|
||||
|
||||
private:
|
||||
|
||||
static constexpr uint32_t MaxTrackableEntries = 4;
|
||||
static constexpr float InitialRoundTripTime = 0.1f; //< Start off with a 100 millisecond estimate for Rtt
|
||||
static constexpr uint32_t MaxTrackableEntries = 8;
|
||||
static constexpr float InitialRoundTripTime = 0.1f; //< Start off with a 100 millisecond estimate for Rtt
|
||||
|
||||
float m_roundTripTime = InitialRoundTripTime;
|
||||
ConnectionPacketEntry m_entries[MaxTrackableEntries];
|
||||
@@ -117,6 +126,11 @@ namespace AzNetworking
|
||||
//! Resets all internal metrics to defaults.
|
||||
void Reset();
|
||||
|
||||
void LogPacketSent(uint32_t byteCount, AZ::TimeMs currentTimeMs);
|
||||
void LogPacketRecv(uint32_t byteCount, AZ::TimeMs currentTimeMs);
|
||||
void LogPacketLost();
|
||||
void LogPacketAcked();
|
||||
|
||||
uint32_t m_packetsSent = 0;
|
||||
uint32_t m_packetsRecv = 0;
|
||||
uint32_t m_packetsLost = 0;
|
||||
|
||||
@@ -40,4 +40,33 @@ namespace AzNetworking
|
||||
{
|
||||
*this = ConnectionMetrics();
|
||||
}
|
||||
|
||||
inline void ConnectionMetrics::LogPacketSent(uint32_t byteCount, AZ::TimeMs currentTimeMs)
|
||||
{
|
||||
if (byteCount > 0)
|
||||
{
|
||||
m_packetsSent++;
|
||||
}
|
||||
m_sendDatarate.LogPacket(byteCount, currentTimeMs);
|
||||
}
|
||||
|
||||
inline void ConnectionMetrics::LogPacketRecv(uint32_t byteCount, AZ::TimeMs currentTimeMs)
|
||||
{
|
||||
if (byteCount > 0)
|
||||
{
|
||||
m_packetsRecv++;
|
||||
}
|
||||
m_recvDatarate.LogPacket(byteCount, currentTimeMs);
|
||||
}
|
||||
|
||||
inline void ConnectionMetrics::LogPacketLost()
|
||||
{
|
||||
m_packetsLost++;
|
||||
m_sendDatarate.LogPacketLost();
|
||||
}
|
||||
|
||||
inline void ConnectionMetrics::LogPacketAcked()
|
||||
{
|
||||
m_packetsAcked++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,11 +95,6 @@ namespace AzNetworking
|
||||
//! @return the max transmission unit for this connection
|
||||
virtual uint32_t GetConnectionMtu() const = 0;
|
||||
|
||||
//! Sets connection quality values for testing poor connection conditions.
|
||||
//! Currently unsupported on TcpConnections
|
||||
//! @param connectionQuality simulated connection quality values to use
|
||||
virtual void SetConnectionQuality(const ConnectionQuality& connectionQuality) = 0;
|
||||
|
||||
//! Returns the connection identifier for this connection instance.
|
||||
//! @return the connection identifier for this connection instance
|
||||
ConnectionId GetConnectionId() const;
|
||||
@@ -128,12 +123,23 @@ namespace AzNetworking
|
||||
//! @return reference to the connection metric info
|
||||
ConnectionMetrics& GetMetrics();
|
||||
|
||||
//! Retrieves debug connection quality settings.
|
||||
//! Currently unsupported on TcpConnections
|
||||
//! @return connection quality structure for this connection
|
||||
const ConnectionQuality& GetConnectionQuality() const;
|
||||
|
||||
//! Retrieves debug connection quality settings, non-const.
|
||||
//! Currently unsupported on TcpConnections
|
||||
//! @return connection quality structure for this connection
|
||||
ConnectionQuality& GetConnectionQuality();
|
||||
|
||||
private:
|
||||
|
||||
// The following data members are here in the interface for performance reasons
|
||||
ConnectionId m_connectionId = InvalidConnectionId;
|
||||
IpAddress m_remoteAddress;
|
||||
ConnectionMetrics m_connectionMetrics;
|
||||
ConnectionQuality m_connectionQuality;
|
||||
void* m_userData = nullptr;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -59,4 +59,14 @@ namespace AzNetworking
|
||||
{
|
||||
return m_connectionMetrics;
|
||||
}
|
||||
|
||||
inline const ConnectionQuality& IConnection::GetConnectionQuality() const
|
||||
{
|
||||
return m_connectionQuality;
|
||||
}
|
||||
|
||||
inline ConnectionQuality& IConnection::GetConnectionQuality()
|
||||
{
|
||||
return m_connectionQuality;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ namespace AzNetworking
|
||||
bool TcpConnection::UpdateRecv()
|
||||
{
|
||||
const AZ::TimeMs startTimeMs = AZ::GetElapsedTimeMs();
|
||||
GetMetrics().m_recvDatarate.LogPacket(0, startTimeMs);
|
||||
GetMetrics().LogPacketRecv(0, startTimeMs);
|
||||
|
||||
// Read new data off the input socket
|
||||
{
|
||||
@@ -261,11 +261,6 @@ namespace AzNetworking
|
||||
return 0; // do nothing, unsupported on TCP connections
|
||||
}
|
||||
|
||||
void TcpConnection::SetConnectionQuality([[maybe_unused]] const ConnectionQuality& connectionQuality)
|
||||
{
|
||||
; // do nothing, unsupported on TCP connections
|
||||
}
|
||||
|
||||
bool TcpConnection::SendPacketInternal(PacketType packetType, TcpPacketEncodingBuffer& payloadBuffer, AZ::TimeMs currentTimeMs)
|
||||
{
|
||||
AZ_Assert(payloadBuffer.GetCapacity() < AZStd::numeric_limits<uint16_t>::max(), "Buffer capacity should be representable using 2 bytes or less");
|
||||
@@ -333,8 +328,7 @@ namespace AzNetworking
|
||||
}
|
||||
|
||||
m_sendRingbuffer.AdvanceWriteBuffer(headerSize + payloadSize);
|
||||
GetMetrics().m_packetsSent++;
|
||||
GetMetrics().m_sendDatarate.LogPacket(headerSize + payloadSize, currentTimeMs);
|
||||
GetMetrics().LogPacketSent(headerSize + payloadSize, currentTimeMs);
|
||||
m_networkInterface.GetMetrics().m_sendPackets++;
|
||||
UpdateSend();
|
||||
return true;
|
||||
@@ -379,8 +373,7 @@ namespace AzNetworking
|
||||
memcpy(dstData, srcData, packetSize);
|
||||
|
||||
m_recvRingbuffer.AdvanceReadBuffer(serializer.GetReadSize() + packetSize);
|
||||
GetMetrics().m_packetsRecv++;
|
||||
GetMetrics().m_recvDatarate.LogPacket(packetSize, currentTimeMs);
|
||||
GetMetrics().LogPacketRecv(packetSize, currentTimeMs);
|
||||
m_networkInterface.GetMetrics().m_recvPackets++;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -102,7 +102,6 @@ namespace AzNetworking
|
||||
bool Disconnect(DisconnectReason reason, TerminationEndpoint endpoint) override;
|
||||
void SetConnectionMtu(uint32_t connectionMtu) override;
|
||||
uint32_t GetConnectionMtu() const override;
|
||||
void SetConnectionQuality(const ConnectionQuality& connectionQuality) override;
|
||||
// @}
|
||||
|
||||
//! Sets the registered socket file descriptor for this TcpConnection in the associated ConnectionSet instance.
|
||||
|
||||
@@ -152,7 +152,7 @@ namespace AzNetworking
|
||||
|
||||
void UdpConnection::ProcessAcked(PacketId packetId, AZ::TimeMs currentTimeMs)
|
||||
{
|
||||
GetMetrics().m_packetsAcked++;
|
||||
GetMetrics().LogPacketAcked();
|
||||
m_reliableQueue.OnPacketAcked(m_networkInterface, *this, packetId);
|
||||
|
||||
// Compute Rtt adjustments
|
||||
@@ -172,8 +172,7 @@ namespace AzNetworking
|
||||
GetMetrics().m_connectionRtt.LogPacketSent(packetId, currentTimeMs);
|
||||
}
|
||||
|
||||
GetMetrics().m_packetsSent++;
|
||||
GetMetrics().m_sendDatarate.LogPacket(packetSize, currentTimeMs);
|
||||
GetMetrics().LogPacketSent(packetSize, currentTimeMs);
|
||||
m_lastSentPacketMs = currentTimeMs;
|
||||
m_unackedPacketCount = 0;
|
||||
}
|
||||
@@ -193,7 +192,7 @@ namespace AzNetworking
|
||||
return PacketTimeoutResult::Acked;
|
||||
|
||||
case PacketAckState::Nacked:
|
||||
GetMetrics().m_packetsLost++;
|
||||
GetMetrics().LogPacketLost();
|
||||
if (reliability == ReliabilityType::Reliable)
|
||||
{
|
||||
m_reliableQueue.OnPacketLost(m_networkInterface, *this, packetId);
|
||||
@@ -224,8 +223,7 @@ namespace AzNetworking
|
||||
return false;
|
||||
}
|
||||
|
||||
GetMetrics().m_packetsRecv++;
|
||||
GetMetrics().m_recvDatarate.LogPacket(packetSize, currentTimeMs);
|
||||
GetMetrics().LogPacketRecv(packetSize, currentTimeMs);
|
||||
|
||||
if (header.GetIsReliable() && !m_reliableQueue.OnPacketReceived(header))
|
||||
{
|
||||
|
||||
@@ -66,13 +66,8 @@ namespace AzNetworking
|
||||
bool Disconnect(DisconnectReason reason, TerminationEndpoint endpoint) override;
|
||||
void SetConnectionMtu(uint32_t connectionMtu) override;
|
||||
uint32_t GetConnectionMtu() const override;
|
||||
void SetConnectionQuality(const ConnectionQuality& connectionQuality) override;
|
||||
// @}
|
||||
|
||||
//! Gets connection quality values for testing poor connection conditions.
|
||||
//! @return connection quality values for this IConnection instance
|
||||
const ConnectionQuality& GetConnectionQuality() const;
|
||||
|
||||
//! Returns a suitable encryption endpoint for this connection type.
|
||||
//! @return reference to the connections encryption endpoint
|
||||
DtlsEndpoint& GetDtlsEndpoint();
|
||||
@@ -146,8 +141,6 @@ namespace AzNetworking
|
||||
UdpFragmentQueue m_fragmentQueue;
|
||||
ConnectionState m_state = ConnectionState::Disconnected;
|
||||
ConnectionRole m_connectionRole = ConnectionRole::Connector;
|
||||
|
||||
ConnectionQuality m_connectionQuality;
|
||||
DtlsEndpoint m_dtlsEndpoint;
|
||||
|
||||
AZ::TimeMs m_lastSentPacketMs;
|
||||
@@ -160,4 +153,3 @@ namespace AzNetworking
|
||||
}
|
||||
|
||||
#include <AzNetworking/UdpTransport/UdpConnection.inl>
|
||||
|
||||
|
||||
@@ -10,16 +10,6 @@
|
||||
|
||||
namespace AzNetworking
|
||||
{
|
||||
inline void UdpConnection::SetConnectionQuality(const ConnectionQuality& connectionQuality)
|
||||
{
|
||||
m_connectionQuality = connectionQuality;
|
||||
}
|
||||
|
||||
inline const ConnectionQuality& UdpConnection::GetConnectionQuality() const
|
||||
{
|
||||
return m_connectionQuality;
|
||||
}
|
||||
|
||||
inline DtlsEndpoint& UdpConnection::GetDtlsEndpoint()
|
||||
{
|
||||
return m_dtlsEndpoint;
|
||||
|
||||
@@ -224,8 +224,7 @@ namespace AzNetworking
|
||||
continue;
|
||||
}
|
||||
|
||||
connection->GetMetrics().m_recvDatarate.LogPacket(packet.m_receivedBytes + UdpPacketHeaderSize, currentTimeMs);
|
||||
connection->GetMetrics().m_packetsRecv++;
|
||||
connection->GetMetrics().LogPacketRecv(packet.m_receivedBytes + UdpPacketHeaderSize, currentTimeMs);
|
||||
|
||||
// Decode the packet flag bitset first since it's always uncompressed
|
||||
UdpPacketHeader header;
|
||||
|
||||
@@ -126,7 +126,7 @@ namespace AzNetworking
|
||||
#ifdef ENABLE_LATENCY_DEBUG
|
||||
if (connectionQuality.m_lossPercentage > 0)
|
||||
{
|
||||
if (int32_t(m_random.GetRandom() % 100) < (connectionQuality.m_lossPercentage / 2))
|
||||
if (int32_t(m_random.GetRandom() % 100) < (connectionQuality.m_lossPercentage))
|
||||
{
|
||||
// Pretend we sent, but don't actually send
|
||||
return true;
|
||||
@@ -157,9 +157,11 @@ namespace AzNetworking
|
||||
#ifdef ENABLE_LATENCY_DEBUG
|
||||
else if ((connectionQuality.m_latencyMs > AZ::TimeMs{ 0 }) || (connectionQuality.m_varianceMs > AZ::TimeMs{ 0 }))
|
||||
{
|
||||
const AZ::TimeMs jitterMs = aznumeric_cast<AZ::TimeMs>(m_random.GetRandom()) % (connectionQuality.m_varianceMs / aznumeric_cast<AZ::TimeMs>(2));
|
||||
const AZ::TimeMs jitterMs = aznumeric_cast<AZ::TimeMs>(m_random.GetRandom()) % (connectionQuality.m_varianceMs > AZ::TimeMs{ 0 }
|
||||
? connectionQuality.m_varianceMs
|
||||
: AZ::TimeMs{ 1 });
|
||||
const AZ::TimeMs currTimeMs = AZ::GetElapsedTimeMs();
|
||||
const AZ::TimeMs deferTimeMs = (connectionQuality.m_latencyMs / aznumeric_cast<AZ::TimeMs>(2)) + jitterMs;
|
||||
const AZ::TimeMs deferTimeMs = (connectionQuality.m_latencyMs) + jitterMs;
|
||||
|
||||
DeferredData deferred = DeferredData(address, data, size, encrypt, dtlsEndpoint);
|
||||
AZ::Interface<AZ::IEventScheduler>::Get()->AddCallback([&, deferredData = deferred]
|
||||
|
||||
@@ -23,36 +23,30 @@ namespace Multiplayer
|
||||
->Version(1);
|
||||
}
|
||||
}
|
||||
|
||||
void MultiplayerDebugSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided)
|
||||
{
|
||||
provided.push_back(AZ_CRC_CE("MultiplayerDebugSystemComponent"));
|
||||
}
|
||||
|
||||
void MultiplayerDebugSystemComponent::GetRequiredServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& required)
|
||||
{
|
||||
;
|
||||
}
|
||||
|
||||
void MultiplayerDebugSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatbile)
|
||||
{
|
||||
incompatbile.push_back(AZ_CRC_CE("MultiplayerDebugSystemComponent"));
|
||||
}
|
||||
|
||||
void MultiplayerDebugSystemComponent::Activate()
|
||||
{
|
||||
#ifdef IMGUI_ENABLED
|
||||
ImGui::ImGuiUpdateListenerBus::Handler::BusConnect();
|
||||
#endif
|
||||
}
|
||||
|
||||
void MultiplayerDebugSystemComponent::Deactivate()
|
||||
{
|
||||
#ifdef IMGUI_ENABLED
|
||||
ImGui::ImGuiUpdateListenerBus::Handler::BusDisconnect();
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef IMGUI_ENABLED
|
||||
void MultiplayerDebugSystemComponent::OnImGuiMainMenuUpdate()
|
||||
{
|
||||
@@ -63,7 +57,6 @@ namespace Multiplayer
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
}
|
||||
|
||||
void AccumulatePerSecondValues(const MultiplayerStats& stats, const MultiplayerStats::Metric& metric, float& outCallsPerSecond, float& outBytesPerSecond)
|
||||
{
|
||||
uint64_t summedCalls = 0;
|
||||
@@ -80,8 +73,9 @@ namespace Multiplayer
|
||||
|
||||
bool DrawMetricsRow(const char* name, bool expandable, uint64_t totalCalls, uint64_t totalBytes, float callsPerSecond, float bytesPerSecond)
|
||||
{
|
||||
const ImGuiTreeNodeFlags flags = expandable ? ImGuiTreeNodeFlags_SpanFullWidth
|
||||
: (ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen | ImGuiTreeNodeFlags_SpanFullWidth);
|
||||
const ImGuiTreeNodeFlags flags = expandable
|
||||
? ImGuiTreeNodeFlags_SpanFullWidth
|
||||
: (ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen | ImGuiTreeNodeFlags_SpanFullWidth);
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableNextColumn();
|
||||
const bool open = ImGui::TreeNodeEx(name, flags);
|
||||
@@ -95,14 +89,12 @@ namespace Multiplayer
|
||||
ImGui::Text("%11.2f", bytesPerSecond);
|
||||
return open;
|
||||
}
|
||||
|
||||
bool DrawSummaryRow(const char* name, const MultiplayerStats& stats)
|
||||
{
|
||||
const MultiplayerStats::Metric propertyUpdatesSent = stats.CalculateTotalPropertyUpdateSentMetrics();
|
||||
const MultiplayerStats::Metric propertyUpdatesRecv = stats.CalculateTotalPropertyUpdateRecvMetrics();
|
||||
const MultiplayerStats::Metric rpcsSent = stats.CalculateTotalRpcsSentMetrics();
|
||||
const MultiplayerStats::Metric rpcsRecv = stats.CalculateTotalRpcsRecvMetrics();
|
||||
|
||||
const uint64_t totalCalls = propertyUpdatesSent.m_totalCalls + propertyUpdatesRecv.m_totalCalls + rpcsSent.m_totalCalls + rpcsRecv.m_totalCalls;
|
||||
const uint64_t totalBytes = propertyUpdatesSent.m_totalBytes + propertyUpdatesRecv.m_totalBytes + rpcsSent.m_totalBytes + rpcsRecv.m_totalBytes;
|
||||
float callsPerSecond = 0.0f;
|
||||
@@ -111,17 +103,14 @@ namespace Multiplayer
|
||||
AccumulatePerSecondValues(stats, propertyUpdatesRecv, callsPerSecond, bytesPerSecond);
|
||||
AccumulatePerSecondValues(stats, rpcsSent, callsPerSecond, bytesPerSecond);
|
||||
AccumulatePerSecondValues(stats, rpcsRecv, callsPerSecond, bytesPerSecond);
|
||||
|
||||
return DrawMetricsRow(name, true, totalCalls, totalBytes, callsPerSecond, bytesPerSecond);
|
||||
}
|
||||
|
||||
bool DrawComponentRow(const char* name, const MultiplayerStats& stats, NetComponentId netComponentId)
|
||||
{
|
||||
const MultiplayerStats::Metric propertyUpdatesSent = stats.CalculateComponentPropertyUpdateSentMetrics(netComponentId);
|
||||
const MultiplayerStats::Metric propertyUpdatesRecv = stats.CalculateComponentPropertyUpdateRecvMetrics(netComponentId);
|
||||
const MultiplayerStats::Metric rpcsSent = stats.CalculateComponentRpcsSentMetrics(netComponentId);
|
||||
const MultiplayerStats::Metric rpcsRecv = stats.CalculateComponentRpcsRecvMetrics(netComponentId);
|
||||
|
||||
const uint64_t totalCalls = propertyUpdatesSent.m_totalCalls + propertyUpdatesRecv.m_totalCalls + rpcsSent.m_totalCalls + rpcsRecv.m_totalCalls;
|
||||
const uint64_t totalBytes = propertyUpdatesSent.m_totalBytes + propertyUpdatesRecv.m_totalBytes + rpcsSent.m_totalBytes + rpcsRecv.m_totalBytes;
|
||||
float callsPerSecond = 0.0f;
|
||||
@@ -130,10 +119,8 @@ namespace Multiplayer
|
||||
AccumulatePerSecondValues(stats, propertyUpdatesRecv, callsPerSecond, bytesPerSecond);
|
||||
AccumulatePerSecondValues(stats, rpcsSent, callsPerSecond, bytesPerSecond);
|
||||
AccumulatePerSecondValues(stats, rpcsRecv, callsPerSecond, bytesPerSecond);
|
||||
|
||||
return DrawMetricsRow(name, true, totalCalls, totalBytes, callsPerSecond, bytesPerSecond);
|
||||
}
|
||||
|
||||
void DrawComponentDetails(const MultiplayerStats& stats, NetComponentId netComponentId)
|
||||
{
|
||||
MultiplayerComponentRegistry* componentRegistry = GetMultiplayerComponentRegistry();
|
||||
@@ -158,7 +145,6 @@ namespace Multiplayer
|
||||
ImGui::TreePop();
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const MultiplayerStats::Metric metric = stats.CalculateComponentPropertyUpdateRecvMetrics(netComponentId);
|
||||
float callsPerSecond = 0.0f;
|
||||
@@ -180,7 +166,6 @@ namespace Multiplayer
|
||||
ImGui::TreePop();
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const MultiplayerStats::Metric metric = stats.CalculateComponentRpcsSentMetrics(netComponentId);
|
||||
float callsPerSecond = 0.0f;
|
||||
@@ -202,7 +187,6 @@ namespace Multiplayer
|
||||
ImGui::TreePop();
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const MultiplayerStats::Metric metric = stats.CalculateComponentRpcsRecvMetrics(netComponentId);
|
||||
float callsPerSecond = 0.0f;
|
||||
@@ -226,45 +210,218 @@ namespace Multiplayer
|
||||
}
|
||||
}
|
||||
|
||||
void MultiplayerDebugSystemComponent::OnImGuiUpdate()
|
||||
void DrawNetworkingStats()
|
||||
{
|
||||
const float TEXT_BASE_WIDTH = ImGui::CalcTextSize("A").x;
|
||||
const float TEXT_BASE_HEIGHT = ImGui::GetTextLineHeightWithSpacing();
|
||||
|
||||
const ImGuiTableFlags flags = ImGuiTableFlags_BordersV
|
||||
| ImGuiTableFlags_BordersOuterH
|
||||
| ImGuiTableFlags_Resizable
|
||||
| ImGuiTableFlags_RowBg
|
||||
| ImGuiTableFlags_NoBordersInBody;
|
||||
|
||||
const ImGuiTreeNodeFlags nodeFlags = (ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen | ImGuiTreeNodeFlags_SpanFullWidth);
|
||||
|
||||
AzNetworking::INetworking* networking = AZ::Interface<AzNetworking::INetworking>::Get();
|
||||
|
||||
ImGui::Text("Total sockets monitored by TcpListenThread: %u", networking->GetTcpListenThreadSocketCount());
|
||||
ImGui::Text("Total time spent updating TcpListenThread: %lld", aznumeric_cast<AZ::s64>(networking->GetTcpListenThreadUpdateTime()));
|
||||
ImGui::Text("Total sockets monitored by UdpReaderThread: %u", networking->GetUdpReaderThreadSocketCount());
|
||||
ImGui::Text("Total time spent updating UdpReaderThread: %lld", aznumeric_cast<AZ::s64>(networking->GetUdpReaderThreadUpdateTime()));
|
||||
ImGui::NewLine();
|
||||
|
||||
for (auto& networkInterface : networking->GetNetworkInterfaces())
|
||||
{
|
||||
if (ImGui::CollapsingHeader(networkInterface.second->GetName().GetCStr()))
|
||||
{
|
||||
const char* protocol = networkInterface.second->GetType() == AzNetworking::ProtocolType::Tcp ? "Tcp" : "Udp";
|
||||
const char* trustZone = networkInterface.second->GetTrustZone() == AzNetworking::TrustZone::ExternalClientToServer ? "ExternalClientToServer" : "InternalServerToServer";
|
||||
const uint32_t port = aznumeric_cast<uint32_t>(networkInterface.second->GetPort());
|
||||
ImGui::Text("%sNetworkInterface open to %s on port %u", protocol, trustZone, port);
|
||||
|
||||
if (ImGui::BeginTable("", 2, flags))
|
||||
{
|
||||
const AzNetworking::NetworkInterfaceMetrics& metrics = networkInterface.second->GetMetrics();
|
||||
ImGui::TableSetupColumn("Stat", ImGuiTableColumnFlags_WidthStretch);
|
||||
ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthFixed, TEXT_BASE_WIDTH * 12.0f);
|
||||
ImGui::TableHeadersRow();
|
||||
ImGui::TableNextRow(); ImGui::TableNextColumn();
|
||||
ImGui::Text("Total time spent updating (ms)");
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::Text("%lld", aznumeric_cast<AZ::s64>(metrics.m_updateTimeMs));
|
||||
ImGui::TableNextRow(); ImGui::TableNextColumn();
|
||||
ImGui::Text("Total number of connections");
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::Text("%llu", aznumeric_cast<AZ::u64>(metrics.m_connectionCount));
|
||||
ImGui::TableNextRow(); ImGui::TableNextColumn();
|
||||
ImGui::Text("Total send time (ms)");
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::Text("%lld", aznumeric_cast<AZ::s64>(metrics.m_sendTimeMs));
|
||||
ImGui::TableNextRow(); ImGui::TableNextColumn();
|
||||
ImGui::Text("Total sent packets");
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::Text("%llu", aznumeric_cast<AZ::s64>(metrics.m_sendPackets));
|
||||
ImGui::TableNextRow(); ImGui::TableNextColumn();
|
||||
ImGui::Text("Total sent bytes after compression");
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::Text("%llu", aznumeric_cast<AZ::u64>(metrics.m_sendBytes));
|
||||
ImGui::TableNextRow(); ImGui::TableNextColumn();
|
||||
ImGui::Text("Total sent bytes before compression");
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::Text("%llu", aznumeric_cast<AZ::u64>(metrics.m_sendBytesUncompressed));
|
||||
ImGui::TableNextRow(); ImGui::TableNextColumn();
|
||||
ImGui::Text("Total sent compressed packets without benefit");
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::Text("%llu", aznumeric_cast<AZ::u64>(metrics.m_sendCompressedPacketsNoGain));
|
||||
ImGui::TableNextRow(); ImGui::TableNextColumn();
|
||||
ImGui::Text("Total gain from packet compression");
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::Text("%lld", aznumeric_cast<AZ::s64>(metrics.m_sendBytesCompressedDelta));
|
||||
ImGui::TableNextRow(); ImGui::TableNextColumn();
|
||||
ImGui::Text("Total packets resent");
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::Text("%llu", aznumeric_cast<AZ::u64>(metrics.m_resentPackets));
|
||||
ImGui::TableNextRow(); ImGui::TableNextColumn();
|
||||
ImGui::Text("Total receive time (ms)");
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::Text("%lld", aznumeric_cast<AZ::s64>(metrics.m_recvTimeMs));
|
||||
ImGui::TableNextRow(); ImGui::TableNextColumn();
|
||||
ImGui::Text("Total received packets");
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::Text("%llu", aznumeric_cast<AZ::u64>(metrics.m_recvPackets));
|
||||
ImGui::TableNextRow(); ImGui::TableNextColumn();
|
||||
ImGui::Text("Total received bytes after compression");
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::Text("%llu", aznumeric_cast<AZ::u64>(metrics.m_recvBytes));
|
||||
ImGui::TableNextRow(); ImGui::TableNextColumn();
|
||||
ImGui::Text("Total received bytes before compression");
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::Text("%llu", aznumeric_cast<AZ::u64>(metrics.m_recvBytesUncompressed));
|
||||
ImGui::TableNextRow(); ImGui::TableNextColumn();
|
||||
ImGui::Text("Total packets discarded due to load");
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::Text("%llu", aznumeric_cast<AZ::u64>(metrics.m_discardedPackets));
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
if (ImGui::BeginTable("", 7, flags))
|
||||
{
|
||||
// The first column will use the default _WidthStretch when ScrollX is Off and _WidthFixed when ScrollX is On
|
||||
ImGui::TableSetupColumn("RemoteAddr", ImGuiTableColumnFlags_WidthStretch);
|
||||
ImGui::TableSetupColumn("Conn. Id", ImGuiTableColumnFlags_WidthFixed, TEXT_BASE_WIDTH * 6.0f);
|
||||
ImGui::TableSetupColumn("Send (Bps)", ImGuiTableColumnFlags_WidthFixed, TEXT_BASE_WIDTH * 10.0f);
|
||||
ImGui::TableSetupColumn("Recv (Bps)", ImGuiTableColumnFlags_WidthFixed, TEXT_BASE_WIDTH * 10.0f);
|
||||
ImGui::TableSetupColumn("RTT (ms)", ImGuiTableColumnFlags_WidthFixed, TEXT_BASE_WIDTH * 8.0f);
|
||||
ImGui::TableSetupColumn("% Lost", ImGuiTableColumnFlags_WidthFixed, TEXT_BASE_WIDTH * 8.0f);
|
||||
ImGui::TableSetupColumn("Debug Settings", ImGuiTableColumnFlags_WidthFixed, TEXT_BASE_WIDTH * 32.0f);
|
||||
ImGui::TableHeadersRow();
|
||||
|
||||
auto displayConnectionRow = [](AzNetworking::IConnection& connection)
|
||||
{
|
||||
ImGui::PushID(&connection);
|
||||
|
||||
const AzNetworking::ConnectionMetrics& metrics = connection.GetMetrics();
|
||||
const AzNetworking::IpAddress::IpString remoteAddr = connection.GetRemoteAddress().GetString();
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TreeNodeEx(remoteAddr.c_str(), nodeFlags);
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::Text("%5llu", aznumeric_cast<AZ::u64>(connection.GetConnectionId()));
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::Text("%9.2f", metrics.m_sendDatarate.GetBytesPerSecond());
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::Text("%9.2f", metrics.m_recvDatarate.GetBytesPerSecond());
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::Text("%7.2f", metrics.m_connectionRtt.GetRoundTripTimeSeconds() * 1000.0f);
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::Text("%7.2f", metrics.m_sendDatarate.GetLossRatePercent());
|
||||
ImGui::TableNextColumn();
|
||||
|
||||
{
|
||||
AzNetworking::ConnectionQuality& quality = connection.GetConnectionQuality();
|
||||
int32_t latency = aznumeric_cast<int32_t>(quality.m_latencyMs);
|
||||
int32_t variance = aznumeric_cast<int32_t>(quality.m_varianceMs);
|
||||
ImGui::SliderInt("Loss %", &quality.m_lossPercentage, 0, 100);
|
||||
if (ImGui::SliderInt("Latency(ms)", &latency, 0, 3000))
|
||||
{
|
||||
quality.m_latencyMs = AZ::TimeMs{ latency };
|
||||
}
|
||||
if (ImGui::SliderInt("Jitter(ms)", &variance, 0, 1000))
|
||||
{
|
||||
quality.m_varianceMs = AZ::TimeMs{ variance };
|
||||
}
|
||||
}
|
||||
ImGui::PopID();
|
||||
};
|
||||
networkInterface.second->GetConnectionSet().VisitConnections(displayConnectionRow);
|
||||
ImGui::EndTable();
|
||||
}
|
||||
}
|
||||
ImGui::NewLine();
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void DrawMultiplayerStats()
|
||||
{
|
||||
const float TEXT_BASE_WIDTH = ImGui::CalcTextSize("A").x;
|
||||
const float TEXT_BASE_HEIGHT = ImGui::GetTextLineHeightWithSpacing();
|
||||
|
||||
IMultiplayer* multiplayer = AZ::Interface<IMultiplayer>::Get();
|
||||
MultiplayerComponentRegistry* componentRegistry = GetMultiplayerComponentRegistry();
|
||||
const Multiplayer::MultiplayerStats& stats = multiplayer->GetStats();
|
||||
ImGui::Text("Multiplayer operating in %s mode", GetEnumString(multiplayer->GetAgentType()));
|
||||
ImGui::Text("Total networked entities: %llu", aznumeric_cast<AZ::u64>(stats.m_entityCount));
|
||||
ImGui::Text("Total client connections: %llu", aznumeric_cast<AZ::u64>(stats.m_clientConnectionCount));
|
||||
ImGui::Text("Total server connections: %llu", aznumeric_cast<AZ::u64>(stats.m_serverConnectionCount));
|
||||
ImGui::NewLine();
|
||||
|
||||
static ImGuiTableFlags flags = ImGuiTableFlags_BordersV
|
||||
| ImGuiTableFlags_BordersOuterH
|
||||
| ImGuiTableFlags_Resizable
|
||||
| ImGuiTableFlags_RowBg
|
||||
| ImGuiTableFlags_NoBordersInBody;
|
||||
|
||||
if (ImGui::BeginTable("", 5, flags))
|
||||
{
|
||||
// The first column will use the default _WidthStretch when ScrollX is Off and _WidthFixed when ScrollX is On
|
||||
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch);
|
||||
ImGui::TableSetupColumn("Total Calls", ImGuiTableColumnFlags_WidthFixed, TEXT_BASE_WIDTH * 12.0f);
|
||||
ImGui::TableSetupColumn("Total Bytes", ImGuiTableColumnFlags_WidthFixed, TEXT_BASE_WIDTH * 12.0f);
|
||||
ImGui::TableSetupColumn("Calls/Sec", ImGuiTableColumnFlags_WidthFixed, TEXT_BASE_WIDTH * 12.0f);
|
||||
ImGui::TableSetupColumn("Bytes/Sec", ImGuiTableColumnFlags_WidthFixed, TEXT_BASE_WIDTH * 12.0f);
|
||||
ImGui::TableHeadersRow();
|
||||
|
||||
if (DrawSummaryRow("Totals", stats))
|
||||
{
|
||||
for (AZStd::size_t index = 0; index < stats.m_componentStats.size(); ++index)
|
||||
{
|
||||
const NetComponentId netComponentId = aznumeric_cast<NetComponentId>(index);
|
||||
using StringLabel = AZStd::fixed_string<128>;
|
||||
const StringLabel gemName = componentRegistry->GetComponentGemName(netComponentId);
|
||||
const StringLabel componentName = componentRegistry->GetComponentName(netComponentId);
|
||||
const StringLabel label = gemName + "::" + componentName;
|
||||
if (DrawComponentRow(label.c_str(), stats, netComponentId))
|
||||
{
|
||||
DrawComponentDetails(stats, netComponentId);
|
||||
ImGui::TreePop();
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::EndTable();
|
||||
ImGui::NewLine();
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void MultiplayerDebugSystemComponent::OnImGuiUpdate()
|
||||
{
|
||||
if (m_displayNetworkingStats)
|
||||
{
|
||||
if (ImGui::Begin("Networking Stats", &m_displayNetworkingStats, ImGuiWindowFlags_None))
|
||||
{
|
||||
AzNetworking::INetworking* networking = AZ::Interface<AzNetworking::INetworking>::Get();
|
||||
|
||||
ImGui::Text("Total sockets monitored by TcpListenThread: %u", networking->GetTcpListenThreadSocketCount());
|
||||
ImGui::Text("Total time spent updating TcpListenThread: %lld", aznumeric_cast<AZ::s64>(networking->GetTcpListenThreadUpdateTime()));
|
||||
ImGui::Text("Total sockets monitored by UdpReaderThread: %u", networking->GetUdpReaderThreadSocketCount());
|
||||
ImGui::Text("Total time spent updating UdpReaderThread: %lld", aznumeric_cast<AZ::s64>(networking->GetUdpReaderThreadUpdateTime()));
|
||||
|
||||
for (auto& networkInterface : networking->GetNetworkInterfaces())
|
||||
{
|
||||
const char* protocol = networkInterface.second->GetType() == AzNetworking::ProtocolType::Tcp ? "Tcp" : "Udp";
|
||||
const char* trustZone = networkInterface.second->GetTrustZone() == AzNetworking::TrustZone::ExternalClientToServer ? "ExternalClientToServer" : "InternalServerToServer";
|
||||
const uint32_t port = aznumeric_cast<uint32_t>(networkInterface.second->GetPort());
|
||||
ImGui::Text("%sNetworkInterface: %s - open to %s on port %u", protocol, networkInterface.second->GetName().GetCStr(), trustZone, port);
|
||||
|
||||
const AzNetworking::NetworkInterfaceMetrics& metrics = networkInterface.second->GetMetrics();
|
||||
ImGui::Text(" - Total time spent updating in milliseconds: %lld", aznumeric_cast<AZ::s64>(metrics.m_updateTimeMs));
|
||||
ImGui::Text(" - Total number of connections: %llu", aznumeric_cast<AZ::u64>(metrics.m_connectionCount));
|
||||
ImGui::Text(" - Total send time in milliseconds: %lld", aznumeric_cast<AZ::s64>(metrics.m_sendTimeMs));
|
||||
ImGui::Text(" - Total sent packets: %llu", aznumeric_cast<AZ::s64>(metrics.m_sendPackets));
|
||||
ImGui::Text(" - Total sent bytes after compression: %llu", aznumeric_cast<AZ::u64>(metrics.m_sendBytes));
|
||||
ImGui::Text(" - Total sent bytes before compression: %llu", aznumeric_cast<AZ::u64>(metrics.m_sendBytesUncompressed));
|
||||
ImGui::Text(" - Total sent compressed packets without benefit: %llu", aznumeric_cast<AZ::u64>(metrics.m_sendCompressedPacketsNoGain));
|
||||
ImGui::Text(" - Total gain from packet compression: %lld", aznumeric_cast<AZ::s64>(metrics.m_sendBytesCompressedDelta));
|
||||
ImGui::Text(" - Total packets resent: %llu", aznumeric_cast<AZ::u64>(metrics.m_resentPackets));
|
||||
ImGui::Text(" - Total receive time in milliseconds: %lld", aznumeric_cast<AZ::s64>(metrics.m_recvTimeMs));
|
||||
ImGui::Text(" - Total received packets: %llu", aznumeric_cast<AZ::u64>(metrics.m_recvPackets));
|
||||
ImGui::Text(" - Total received bytes after compression: %llu", aznumeric_cast<AZ::u64>(metrics.m_recvBytes));
|
||||
ImGui::Text(" - Total received bytes before compression: %llu", aznumeric_cast<AZ::u64>(metrics.m_recvBytesUncompressed));
|
||||
ImGui::Text(" - Total packets discarded due to load: %llu", aznumeric_cast<AZ::u64>(metrics.m_discardedPackets));
|
||||
}
|
||||
DrawNetworkingStats();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,50 +429,7 @@ namespace Multiplayer
|
||||
{
|
||||
if (ImGui::Begin("Multiplayer Stats", &m_displayMultiplayerStats, ImGuiWindowFlags_None))
|
||||
{
|
||||
IMultiplayer* multiplayer = AZ::Interface<IMultiplayer>::Get();
|
||||
MultiplayerComponentRegistry* componentRegistry = GetMultiplayerComponentRegistry();
|
||||
const Multiplayer::MultiplayerStats& stats = multiplayer->GetStats();
|
||||
ImGui::Text("Multiplayer operating in %s mode", GetEnumString(multiplayer->GetAgentType()));
|
||||
ImGui::Text("Total networked entities: %llu", aznumeric_cast<AZ::u64>(stats.m_entityCount));
|
||||
ImGui::Text("Total client connections: %llu", aznumeric_cast<AZ::u64>(stats.m_clientConnectionCount));
|
||||
ImGui::Text("Total server connections: %llu", aznumeric_cast<AZ::u64>(stats.m_serverConnectionCount));
|
||||
ImGui::NewLine();
|
||||
|
||||
static ImGuiTableFlags flags = ImGuiTableFlags_BordersV
|
||||
| ImGuiTableFlags_BordersOuterH
|
||||
| ImGuiTableFlags_Resizable
|
||||
| ImGuiTableFlags_RowBg
|
||||
| ImGuiTableFlags_NoBordersInBody;
|
||||
|
||||
if (ImGui::BeginTable("", 5, flags))
|
||||
{
|
||||
// The first column will use the default _WidthStretch when ScrollX is Off and _WidthFixed when ScrollX is On
|
||||
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch);
|
||||
ImGui::TableSetupColumn("Total Calls", ImGuiTableColumnFlags_WidthFixed, TEXT_BASE_WIDTH * 12.0f);
|
||||
ImGui::TableSetupColumn("Total Bytes", ImGuiTableColumnFlags_WidthFixed, TEXT_BASE_WIDTH * 12.0f);
|
||||
ImGui::TableSetupColumn("Calls/Sec", ImGuiTableColumnFlags_WidthFixed, TEXT_BASE_WIDTH * 12.0f);
|
||||
ImGui::TableSetupColumn("Bytes/Sec", ImGuiTableColumnFlags_WidthFixed, TEXT_BASE_WIDTH * 12.0f);
|
||||
ImGui::TableHeadersRow();
|
||||
|
||||
if (DrawSummaryRow("Totals", stats))
|
||||
{
|
||||
for (AZStd::size_t index = 0; index < stats.m_componentStats.size(); ++index)
|
||||
{
|
||||
const NetComponentId netComponentId = aznumeric_cast<NetComponentId>(index);
|
||||
using StringLabel = AZStd::fixed_string<128>;
|
||||
const StringLabel gemName = componentRegistry->GetComponentGemName(netComponentId);
|
||||
const StringLabel componentName = componentRegistry->GetComponentName(netComponentId);
|
||||
const StringLabel label = gemName + "::" + componentName;
|
||||
if (DrawComponentRow(label.c_str(), stats, netComponentId))
|
||||
{
|
||||
DrawComponentDetails(stats, netComponentId);
|
||||
ImGui::TreePop();
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::EndTable();
|
||||
}
|
||||
ImGui::End();
|
||||
DrawMultiplayerStats();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user