Added better GPS handling, Improved test mode, Fixed naming of players not being broadcast until a new HelloClient, Fixed lobby restarts, Fixed task handling

This commit is contained in:
Bandwidth
2026-04-27 23:18:13 +02:00
parent d886f97e14
commit 207f997254
8 changed files with 1175 additions and 77 deletions

View File

@@ -66,10 +66,39 @@ public class GameManager : MonoBehaviour
[Header("Debug")]
public bool testMode = false;
private GameClient _secondClient;
private GameClient _thirdClient;
private GameManager_Network _secondNetwork;
private GameManager_Network _thirdNetwork;
/// <summary>
/// When true, draw a small GPS status banner across the top of every
/// screen. Useful for diagnosing why CreateLobby is blocked or why a
/// joiner's position isn't updating - failures otherwise only show up
/// in logcat which most users can't reach. Toggle off for release.
/// </summary>
public bool showGPSDebugOverlay = true;
/// <summary>
/// Number of in-process test client bots to spawn alongside the host
/// when testMode is on. Each gets its own GameClient + Network and
/// joins the host's lobby automatically. Bots are switchable via
/// number keys 1..N (host = 0). Default 3 keeps memory reasonable;
/// bump for stress-testing voting / sabotage flows.
/// </summary>
public int testClientCount = 3;
/// <summary>
/// Per-bot network + display-name + sim-position state. The active slot
/// (host = 0, bots = 1..N) gets WASD on the next tick.
/// </summary>
private class TestBot
{
public GameClient Client;
public GameManager_Network Network;
public string DisplayName;
public GeoSus.Client.Position SimPosition;
public bool Joined;
public float LastSendTime;
}
private System.Collections.Generic.List<TestBot> _testBots = new System.Collections.Generic.List<TestBot>();
/// <summary>Slot 0 = host (real player), 1..N = test bot index.</summary>
private int _activeClientSlot = 0;
void Awake()
{
@@ -80,11 +109,71 @@ public class GameManager : MonoBehaviour
}
Instance = this;
DontDestroyOnLoad(gameObject);
// Keep the screen on while the player is in the app. A geographic
// social-deduction game asks the user to walk around for 5-15 minutes
// staring at the map; default Android sleep timeout (15-60s) blacks
// the screen out mid-round, drops GPS updates, and requires the
// player to re-unlock the phone. Two layers of belt-and-suspenders:
// (1) Unity's Screen.sleepTimeout, which works on most devices and
// is one line, but is overridden by some MIUI/EMUI ROMs.
// (2) Android FLAG_KEEP_SCREEN_ON on the activity window, harder for
// OEM ROMs to override and the standard pattern for navigation/maps
// apps. Wrapped in #if UNITY_ANDROID so editor/iOS skip it.
Screen.sleepTimeout = SleepTimeout.NeverSleep;
AcquireAndroidWakelock();
}
/// <summary>
/// Set FLAG_KEEP_SCREEN_ON on the Unity activity's window. This is the
/// standard navigation/maps-app pattern and survives ROM-level overrides
/// of Unity's Screen.sleepTimeout. No-op on non-Android platforms.
/// </summary>
private static void AcquireAndroidWakelock()
{
#if UNITY_ANDROID && !UNITY_EDITOR
try
{
using (var player = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
using (var activity = player.GetStatic<AndroidJavaObject>("currentActivity"))
{
// addFlags must run on the UI thread. Capture activity into a
// local for the closure - AndroidJavaObject can be reused.
var act = activity;
act.Call("runOnUiThread", new AndroidJavaRunnable(() =>
{
try
{
using (var window = act.Call<AndroidJavaObject>("getWindow"))
{
// WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
const int FLAG_KEEP_SCREEN_ON = 0x00000080;
window.Call("addFlags", FLAG_KEEP_SCREEN_ON);
}
}
catch (System.Exception ex)
{
Debug.LogWarning("[Wakelock] addFlags failed: " + ex.Message);
}
}));
}
}
catch (System.Exception ex)
{
Debug.LogWarning("[Wakelock] Android JNI bridge failed: " + ex.Message);
}
#endif
}
void Start()
{
if (string.IsNullOrEmpty(displayName))
// The prefab default in SampleScene.unity is "Hrac" (Czech for
// "player"). Treat it as equivalent to "no name set" so users who
// never customize their name don't all show up identically. This
// override only fires at startup; users who explicitly type "Hrac"
// into the nickname field will still send "Hrac" via the live
// DisplayName payload field.
if (string.IsNullOrEmpty(displayName) || displayName == "Hrac")
displayName = PlayerPrefs.GetString("PlayerName", GenerateUsername());
gameClient = new GameClient(GenerateUUID(), displayName);
@@ -96,12 +185,18 @@ public class GameManager : MonoBehaviour
if (testMode)
{
_secondClient = new GameClient(GenerateUUID(), GenerateUsername());
_secondNetwork = new GameManager_Network(_secondClient, null);
_thirdClient = new GameClient(GenerateUUID(), GenerateUsername());
_thirdNetwork = new GameManager_Network(_thirdClient, null);
_secondNetwork.OpenConnection();
_thirdNetwork.OpenConnection();
int n = Mathf.Max(0, testClientCount);
for (int i = 0; i < n; i++)
{
var bot = new TestBot
{
DisplayName = "TestBot" + (i + 1),
};
bot.Client = new GameClient(GenerateUUID(), bot.DisplayName);
bot.Network = new GameManager_Network(bot.Client, null);
bot.Network.OpenConnection();
_testBots.Add(bot);
}
}
networkSubsystem.OpenConnection();
@@ -118,14 +213,103 @@ public class GameManager : MonoBehaviour
SceneManager.LoadScene(firstMenuScene, LoadSceneMode.Single);
}
/// <summary>
/// Draws a GPS status banner across the top of every screen. We use OnGUI
/// rather than a uGUI Canvas element because OnGUI works without any
/// scene wiring - we want this visible from the very first frame, on
/// every screen, even if the lobby canvas hasn't been bound yet. This is
/// a debug overlay; toggle showGPSDebugOverlay off for release builds.
/// </summary>
private void OnGUI()
{
if (!showGPSDebugOverlay) return;
if (inputSubsystem == null) return;
var diag = inputSubsystem.GpsDiagnostic;
var label = "GPS: " + diag;
// Scale font size to screen so it's legible on phones (HDPI) and
// editor (lower DPI) alike. Phones tend to have ~400dpi; the
// editor game view runs at ~100dpi.
int fontSize = Mathf.Max(14, Screen.width / 50);
var style = new GUIStyle(GUI.skin.label)
{
fontSize = fontSize,
fontStyle = FontStyle.Bold,
alignment = TextAnchor.MiddleLeft,
wordWrap = false,
normal = { textColor = Color.white }
};
// Width covers most of the screen so longer error strings don't get
// clipped. Height auto-fits the chosen font size.
float pad = fontSize * 0.5f;
float bannerH = fontSize * 2f;
var rect = new Rect(pad, pad, Screen.width - pad * 2, bannerH);
// Translucent black background for legibility against the map.
var prevColor = GUI.color;
GUI.color = new Color(0f, 0f, 0f, 0.65f);
GUI.Box(rect, GUIContent.none);
GUI.color = prevColor;
// Indent the label inside the box.
var textRect = new Rect(rect.x + pad, rect.y, rect.width - pad * 2, rect.height);
GUI.Label(textRect, label, style);
// Second row: position-source picker (tap to cycle) + active client
// indicator (testMode only). Both are diagnostic; the source picker
// is the recovery path when one backend silently fails on a phone.
float row2Y = rect.y + bannerH + pad * 0.5f;
var btnStyle = new GUIStyle(GUI.skin.button)
{
fontSize = Mathf.Max(12, fontSize - 2),
fontStyle = FontStyle.Bold,
alignment = TextAnchor.MiddleCenter,
};
// Source button: shows current source name + invites tap.
string sourceLabel = "Source: " + inputSubsystem.CurrentSourceName + " [tap to cycle]";
// Width sized to the text so the touch area matches the label.
Vector2 sourceSize = btnStyle.CalcSize(new GUIContent(sourceLabel));
float sourceW = Mathf.Min(Screen.width - pad * 2, sourceSize.x + pad * 2);
var sourceRect = new Rect(pad, row2Y, sourceW, bannerH);
if (GUI.Button(sourceRect, sourceLabel, btnStyle))
{
inputSubsystem.CycleNextPositionSource();
}
// Active-client indicator (only when we have test bots).
if (testMode && _testBots.Count > 0)
{
string slot = _activeClientSlot == 0 ? "Host" : ("Bot " + _activeClientSlot);
string indicator = $"WASD: {slot} (0..{_testBots.Count} to switch)";
var indStyle = new GUIStyle(GUI.skin.label)
{
fontSize = Mathf.Max(12, fontSize - 2),
fontStyle = FontStyle.Bold,
alignment = TextAnchor.MiddleLeft,
normal = { textColor = new Color(0.9f, 1f, 0.4f) },
};
Vector2 indSize = indStyle.CalcSize(new GUIContent(indicator));
var indRect = new Rect(sourceRect.xMax + pad, row2Y, indSize.x + pad * 2, bannerH);
GUI.color = new Color(0f, 0f, 0f, 0.65f);
GUI.Box(indRect, GUIContent.none);
GUI.color = prevColor;
GUI.Label(new Rect(indRect.x + pad, indRect.y, indRect.width, indRect.height), indicator, indStyle);
}
}
private void Update()
{
// Tick the SDK dispatcher so callbacks fire on main thread
gameClient?.Update();
if (testMode)
{
_secondClient?.Update();
_thirdClient?.Update();
for (int i = 0; i < _testBots.Count; i++)
_testBots[i].Client?.Update();
HandleTestBotInput();
}
if (gameClient?.CurrentLobbyState != null)
@@ -137,6 +321,102 @@ public class GameManager : MonoBehaviour
UpdateKillCooldown();
inputSubsystem?.positionCheck();
if (testMode) StepActiveTestBot();
}
/// <summary>
/// Number-key handling for slot switching. 0 = host, 1..N = test bot N.
/// Suppress host WASD when a non-host bot is active so the host capsule
/// doesn't drift while the user is moving a bot. Only fires when
/// testMode is on; release builds never see this path.
/// </summary>
private void HandleTestBotInput()
{
// 0 = host. 1..9 = bots (capped by Unity KeyCode.Alpha9).
if (Input.GetKeyDown(KeyCode.Alpha0)) _activeClientSlot = 0;
for (int i = 1; i <= 9 && i <= _testBots.Count; i++)
{
if (Input.GetKeyDown(KeyCode.Alpha0 + i)) _activeClientSlot = i;
}
// Tell the host's input subsystem to ignore WASD when a bot is active.
if (inputSubsystem != null)
inputSubsystem.SuppressWasd = (_activeClientSlot != 0);
}
/// <summary>
/// If the active slot is a bot, step its sim position from WASD axes
/// and send to the server. Idle bots get a periodic keep-alive so their
/// avatars don't time out.
/// </summary>
private void StepActiveTestBot()
{
if (_testBots.Count == 0) return;
var state = gameClient?.CurrentLobbyState;
if (state == null || state.MapData == null) return;
// Lazy-init each bot's sim position to the lobby's map center on
// first lobby state. Until the bot has joined a lobby it can't
// send position updates.
for (int i = 0; i < _testBots.Count; i++)
{
var bot = _testBots[i];
if (!bot.Joined) continue;
if (bot.SimPosition.Lat == 0 && bot.SimPosition.Lon == 0)
{
// Spawn each bot in a small ring around the map center so
// they don't all stack on top of each other on frame one.
double offsetLat = 0.00003 * Mathf.Cos(i * Mathf.PI * 2f / Mathf.Max(1, _testBots.Count));
double offsetLon = 0.00003 * Mathf.Sin(i * Mathf.PI * 2f / Mathf.Max(1, _testBots.Count));
bot.SimPosition = new GeoSus.Client.Position(
state.MapData.Center.Lat + offsetLat,
state.MapData.Center.Lon + offsetLon);
bot.Client.UpdatePosition(bot.SimPosition);
bot.LastSendTime = Time.time;
}
}
// WASD only drives the active bot.
if (_activeClientSlot >= 1 && _activeClientSlot <= _testBots.Count)
{
var bot = _testBots[_activeClientSlot - 1];
if (bot.Joined)
{
float dx = Input.GetAxis("Horizontal");
float dy = Input.GetAxis("Vertical");
const double speed = 0.00001;
bool moved = Mathf.Abs(dx) > 0.001f || Mathf.Abs(dy) > 0.001f;
if (moved)
{
bot.SimPosition = new GeoSus.Client.Position(
bot.SimPosition.Lat + dy * speed,
bot.SimPosition.Lon + dx * speed);
}
// Send on movement OR on keep-alive cadence so the server
// doesn't drop our presence.
bool dueKeepAlive = (Time.time - bot.LastSendTime) >= 1.0f;
if (moved || dueKeepAlive)
{
bot.Client.UpdatePosition(bot.SimPosition);
bot.LastSendTime = Time.time;
}
}
}
else
{
// No bot is active. All bots get keep-alive only.
for (int i = 0; i < _testBots.Count; i++)
{
var bot = _testBots[i];
if (!bot.Joined) continue;
if ((Time.time - bot.LastSendTime) >= 1.0f)
{
bot.Client.UpdatePosition(bot.SimPosition);
bot.LastSendTime = Time.time;
}
}
}
}
@@ -326,9 +606,38 @@ public class GameManager : MonoBehaviour
return "Player" + UnityEngine.Random.Range(1000, 9999).ToString();
}
/// <summary>
/// Pull the nickname input field's current text into displayName +
/// gameClient.DisplayName + PlayerPrefs before sending a network
/// action. Defensive against any TMP_InputField / soft-keyboard race
/// where the user types and immediately taps a button: onValueChanged
/// normally fires before the click handler in the same frame, but
/// some Android keyboards batch text events oddly. Call this at the
/// top of any Create/Join/Rename flow. No-op if the input field
/// doesn't exist in the current scene.
/// </summary>
private void CommitNicknameFromInput()
{
var nameGO = GameObject.Find("name");
if (nameGO == null) return;
var field = nameGO.GetComponent<TMPro.TMP_InputField>();
if (field == null) return;
// Force the InputField to flush any pending soft-keyboard text.
// ForceLabelUpdate() is harmless if there's nothing pending.
field.ForceLabelUpdate();
string typed = (field.text ?? "").Trim();
if (string.IsNullOrEmpty(typed)) return;
if (typed == displayName) return; // already in sync, skip the writes
displayName = typed;
if (gameClient != null) gameClient.DisplayName = typed;
PlayerPrefs.SetString("PlayerName", typed);
PlayerPrefs.Save();
}
// Called by HostLobbyUI
public void CreateLobbyButton()
{
CommitNicknameFromInput();
// Refuse to create a lobby without a real GPS fix. The previous
// behavior of silently using a hardcoded Czechia fallback meant the
// game always started at the same place no matter where the host was,
@@ -340,9 +649,12 @@ public class GameManager : MonoBehaviour
// testMode bypasses the GPS gate entirely so debug runs still work.
if (!testMode)
{
Debug.LogWarning("[GameManager] CreateLobby blocked: no GPS fix yet. " +
"Make sure location permission is granted and you have signal.");
uiSubsystem?.ShowToast("Waiting for GPS fix... grant location permission and try again.");
// Surface the actual GPS state in both logs and the toast
// instead of the generic "Waiting for GPS fix..." that hides
// permission/timeout/device-disabled distinctions.
string diag = inputSubsystem?.GpsDiagnostic ?? "no input subsystem";
Debug.LogWarning("[GameManager] CreateLobby blocked. " + diag);
uiSubsystem?.ShowToast("Cannot create lobby. " + diag);
inputSubsystem?.EnsureGPSStarted();
return;
}
@@ -358,6 +670,7 @@ public class GameManager : MonoBehaviour
// Called by JoinLobbyUI with the code from the input field
public void JoinLobbyButton(string code)
{
CommitNicknameFromInput();
if (!string.IsNullOrEmpty(code))
networkSubsystem.JoinLobby(code);
else
@@ -377,12 +690,14 @@ public class GameManager : MonoBehaviour
void OnApplicationQuit()
{
gameClient?.Disconnect();
_secondClient?.Disconnect();
_thirdClient?.Disconnect();
for (int i = 0; i < _testBots.Count; i++)
_testBots[i].Client?.Disconnect();
}
IEnumerator ConnectTestClients()
{
IEnumerator ConnectTestClients()
{
if (_testBots.Count == 0) yield break;
// Wait until host lobby code exists
float wait = 0f;
while ((gameClient?.CurrentLobbyState == null || string.IsNullOrEmpty(gameClient.CurrentLobbyState.JoinCode)) && wait < 20f)
@@ -394,26 +709,46 @@ public class GameManager : MonoBehaviour
var joinCode = gameClient?.CurrentLobbyState?.JoinCode;
if (string.IsNullOrEmpty(joinCode))
{
Debug.LogWarning("[TestMode] Could not join test clients: join code not available.");
Debug.LogWarning("[TestMode] Could not join test bots: join code not available.");
yield break;
}
// Wait until helper clients are connected and handshake-complete
// Wait until every bot's client has finished its TCP handshake.
// IsReady flips once ClientHello + ClientHelloAck round-trip.
wait = 0f;
while (((_secondClient == null || !_secondClient.IsReady) || (_thirdClient == null || !_thirdClient.IsReady)) && wait < 20f)
bool allReady;
do
{
wait += 0.25f;
yield return new WaitForSeconds(0.25f);
allReady = true;
for (int i = 0; i < _testBots.Count; i++)
{
if (_testBots[i].Client == null || !_testBots[i].Client.IsReady)
{
allReady = false;
break;
}
}
if (!allReady)
{
wait += 0.25f;
yield return new WaitForSeconds(0.25f);
}
} while (!allReady && wait < 20f);
if (!allReady)
{
Debug.LogWarning("[TestMode] Some test bots not ready, joining the ready ones only.");
}
if (_secondClient == null || _thirdClient == null || !_secondClient.IsReady || !_thirdClient.IsReady)
for (int i = 0; i < _testBots.Count; i++)
{
Debug.LogWarning("[TestMode] Helper clients are not ready, skipping auto-join.");
yield break;
var bot = _testBots[i];
if (bot.Client != null && bot.Client.IsReady)
{
bot.Network?.JoinLobby(joinCode);
bot.Joined = true;
}
}
_secondNetwork?.JoinLobby(joinCode);
_thirdNetwork?.JoinLobby(joinCode);
Debug.Log($"[TestMode] Helper clients joined lobby with code {joinCode}.");
Debug.Log($"[TestMode] {_testBots.Count} bot(s) joined lobby with code {joinCode}.");
}
}