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:
@@ -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}.");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user