diff --git a/Example/README.md b/Example/README.md new file mode 100644 index 0000000..0820e6d --- /dev/null +++ b/Example/README.md @@ -0,0 +1,63 @@ +# Unity UDP Example + +This example demonstrates how to use the UnityUDP Flutter app to send commands to a Unity game. + +## Setup + +### Unity Setup + +1. Copy `UDPCommandListener.cs` into your Unity project's `Assets/Scripts/` folder +2. Create a new empty GameObject in your scene (GameObject → Create Empty) +3. Rename it to "UDP Manager" +4. Attach the `UDPCommandListener` script to the UDP Manager GameObject +5. Set the port to 7777 (or any port you prefer) in the inspector + +### Example Scene Setup + +For the example commands to work, create a simple test scene: + +1. Create a Cube (GameObject → 3D Object → Cube) +2. Name it "TestCube" +3. Create a UI Canvas with a Text element showing a timer (optional, for the timer command) + +### Testing the Example + +1. Run your Unity game +2. Open the UnityUDP Flutter app +3. Copy/move the `unityudp_projects.json` file to your Documents folder +4. Select a command and send it to your Unity game (make sure the IP address matches your computer's local IP and port is 7777) + +## Sample Commands + +The example includes these simple commands: + +- **Toggle Cube**: Enables/Disables the TestCube GameObject +- **Start Timer**: Starts a 10-second countdown timer +- **Reset Scene**: Reloads the current scene + +## UDP Protocol + +The UDP commands are sent as JSON strings with the following format: + +```json +{ + "command": "command_name", + "value": "optional_value" +} +``` + +The Unity script parses these JSON commands and executes the corresponding action. + +## Customizing + +You can add your own commands by: + +1. Adding a new case in the `ProcessCommand` method in `UDPCommandListener.cs` +2. Creating a corresponding JSON entry in your project file + +## Troubleshooting + +- **Commands not working**: Check that your firewall allows UDP traffic on the specified port +- **Can't connect**: Verify the IP address is correct (use `ipconfig` on Windows or `ifconfig` on Mac/Linux) +- **Unity crashes**: Make sure all GameObject names match the ones in the script + diff --git a/Example/UDPCommandListener.cs b/Example/UDPCommandListener.cs new file mode 100644 index 0000000..4b0b368 --- /dev/null +++ b/Example/UDPCommandListener.cs @@ -0,0 +1,419 @@ +using UnityEngine; +using UnityEngine.SceneManagement; +using System; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; + +/// +/// Simple UDP Command Listener for Unity +/// Listens for JSON commands on a specified UDP port and executes them +/// +public class UDPCommandListener : MonoBehaviour +{ + [Header("UDP Settings")] + [Tooltip("Port to listen on for UDP commands")] + public int port = 7777; + + [Header("References")] + [Tooltip("GameObject to toggle/manipulate (e.g., a cube)")] + public GameObject testObject; + + [Header("Debug")] + public bool showDebugLogs = true; + + // UDP client for receiving messages + private UdpClient udpClient; + private Thread receiveThread; + private bool isRunning = false; + + // Timer state + private float timerDuration = 0f; + private float timerRemaining = 0f; + private bool timerActive = false; + + // Rotation state + private float rotationSpeed = 0f; + + void Start() + { + // Find the test object if not assigned + if (testObject == null) + { + testObject = GameObject.Find("TestCube"); + if (testObject == null) + { + LogMessage("Warning: TestCube not found in scene. Create a GameObject named 'TestCube' or assign one manually."); + } + } + + // Start listening for UDP messages + StartUDPListener(); + } + + void Update() + { + // Update timer + if (timerActive && timerRemaining > 0) + { + timerRemaining -= Time.deltaTime; + if (timerRemaining <= 0) + { + timerRemaining = 0; + timerActive = false; + LogMessage("Timer finished!"); + } + } + + // Update rotation + if (rotationSpeed != 0 && testObject != null) + { + testObject.transform.Rotate(Vector3.up, rotationSpeed * Time.deltaTime); + } + } + + void OnGUI() + { + // Display timer on screen + if (timerActive && timerRemaining > 0) + { + GUIStyle style = new GUIStyle(); + style.fontSize = 30; + style.normal.textColor = Color.white; + style.alignment = TextAnchor.UpperCenter; + + GUI.Label(new Rect(0, 10, Screen.width, 50), + $"Timer: {timerRemaining:F1}s", style); + } + + // Display UDP status + GUIStyle statusStyle = new GUIStyle(); + statusStyle.fontSize = 14; + statusStyle.normal.textColor = isRunning ? Color.green : Color.red; + statusStyle.alignment = TextAnchor.UpperLeft; + + GUI.Label(new Rect(10, 10, 300, 30), + $"UDP Listener: {(isRunning ? "Running" : "Stopped")} (Port {port})", + statusStyle); + } + + /// + /// Starts the UDP listener thread + /// + void StartUDPListener() + { + try + { + udpClient = new UdpClient(port); + receiveThread = new Thread(new ThreadStart(ReceiveData)); + receiveThread.IsBackground = true; + receiveThread.Start(); + isRunning = true; + + LogMessage($"UDP Listener started on port {port}"); + } + catch (Exception e) + { + LogMessage($"Error starting UDP listener: {e.Message}"); + } + } + + /// + /// Receives data on a separate thread + /// + void ReceiveData() + { + while (isRunning) + { + try + { + IPEndPoint remoteEndPoint = new IPEndPoint(IPAddress.Any, 0); + byte[] data = udpClient.Receive(ref remoteEndPoint); + string message = Encoding.UTF8.GetString(data); + + LogMessage($"Received UDP message: {message}"); + + // Process the command on the main thread + UnityMainThreadDispatcher.Instance().Enqueue(() => ProcessCommand(message)); + } + catch (Exception e) + { + if (isRunning) + { + LogMessage($"Error receiving UDP data: {e.Message}"); + } + } + } + } + + /// + /// Processes a received command + /// + void ProcessCommand(string jsonCommand) + { + try + { + // Parse the JSON command + UDPCommand command = JsonUtility.FromJson(jsonCommand); + + if (command == null || string.IsNullOrEmpty(command.command)) + { + LogMessage("Invalid command format"); + return; + } + + LogMessage($"Processing command: {command.command}"); + + // Execute the command + switch (command.command.ToLower()) + { + case "toggle_cube": + ToggleObject(); + break; + + case "set_object": + SetObjectState(command.target, command.value); + break; + + case "start_timer": + StartTimer(command.value); + break; + + case "stop_timer": + StopTimer(); + break; + + case "reset_scene": + ResetScene(); + break; + + case "rotate_cube": + SetRotation(command.value); + break; + + case "change_color": + ChangeColor(command.value); + break; + + default: + LogMessage($"Unknown command: {command.command}"); + break; + } + } + catch (Exception e) + { + LogMessage($"Error processing command: {e.Message}"); + } + } + + // ========== Command Implementations ========== + + void ToggleObject() + { + if (testObject != null) + { + testObject.SetActive(!testObject.activeSelf); + LogMessage($"TestCube toggled to: {testObject.activeSelf}"); + } + else + { + LogMessage("TestCube not found!"); + } + } + + void SetObjectState(string targetName, string state) + { + GameObject target = string.IsNullOrEmpty(targetName) ? testObject : GameObject.Find(targetName); + + if (target != null) + { + bool active = state.ToLower() == "true" || state == "1"; + target.SetActive(active); + LogMessage($"{target.name} set to: {active}"); + } + else + { + LogMessage($"GameObject '{targetName}' not found!"); + } + } + + void StartTimer(string durationStr) + { + if (float.TryParse(durationStr, out float duration)) + { + timerDuration = duration; + timerRemaining = duration; + timerActive = true; + LogMessage($"Timer started for {duration} seconds"); + } + else + { + LogMessage("Invalid timer duration"); + } + } + + void StopTimer() + { + timerActive = false; + timerRemaining = 0; + LogMessage("Timer stopped"); + } + + void ResetScene() + { + LogMessage("Resetting scene..."); + Scene currentScene = SceneManager.GetActiveScene(); + SceneManager.LoadScene(currentScene.name); + } + + void SetRotation(string speedStr) + { + if (float.TryParse(speedStr, out float speed)) + { + rotationSpeed = speed; + LogMessage($"Rotation speed set to: {speed}"); + } + else + { + LogMessage("Invalid rotation speed"); + } + } + + void ChangeColor(string colorName) + { + if (testObject != null) + { + Renderer renderer = testObject.GetComponent(); + if (renderer != null) + { + Color newColor = Color.white; + + switch (colorName.ToLower()) + { + case "red": + newColor = Color.red; + break; + case "blue": + newColor = Color.blue; + break; + case "green": + newColor = Color.green; + break; + case "yellow": + newColor = Color.yellow; + break; + case "white": + newColor = Color.white; + break; + case "black": + newColor = Color.black; + break; + default: + LogMessage($"Unknown color: {colorName}"); + return; + } + + renderer.material.color = newColor; + LogMessage($"Color changed to: {colorName}"); + } + else + { + LogMessage("TestCube has no Renderer component!"); + } + } + else + { + LogMessage("TestCube not found!"); + } + } + + // ========== Utility Methods ========== + + void LogMessage(string message) + { + if (showDebugLogs) + { + Debug.Log($"[UDP] {message}"); + } + } + + void OnApplicationQuit() + { + StopUDPListener(); + } + + void OnDestroy() + { + StopUDPListener(); + } + + void StopUDPListener() + { + isRunning = false; + + if (receiveThread != null && receiveThread.IsAlive) + { + receiveThread.Abort(); + } + + if (udpClient != null) + { + udpClient.Close(); + } + + LogMessage("UDP Listener stopped"); + } +} + +/// +/// Data class for UDP commands +/// +[System.Serializable] +public class UDPCommand +{ + public string command; + public string target; + public string value; +} + +/// +/// Helper class to execute actions on the main Unity thread +/// This is necessary because UDP receives data on a separate thread +/// +public class UnityMainThreadDispatcher : MonoBehaviour +{ + private static UnityMainThreadDispatcher _instance; + private static readonly System.Collections.Generic.Queue _executionQueue = new System.Collections.Generic.Queue(); + + public static UnityMainThreadDispatcher Instance() + { + if (_instance == null) + { + GameObject go = new GameObject("MainThreadDispatcher"); + _instance = go.AddComponent(); + DontDestroyOnLoad(go); + } + return _instance; + } + + public void Enqueue(Action action) + { + lock (_executionQueue) + { + _executionQueue.Enqueue(action); + } + } + + void Update() + { + lock (_executionQueue) + { + while (_executionQueue.Count > 0) + { + _executionQueue.Dequeue().Invoke(); + } + } + } +} + diff --git a/Example/unityudp_projects.json b/Example/unityudp_projects.json new file mode 100644 index 0000000..6b2c5fe --- /dev/null +++ b/Example/unityudp_projects.json @@ -0,0 +1,81 @@ +{ + "id": "example-project-001", + "name": "Unity Example Project", + "ipAddress": "127.0.0.1", + "port": 7777, + "packages": [ + { + "id": "cmd-001", + "name": "Toggle Cube", + "data": "{\"command\":\"toggle_cube\"}", + "ipAddress": "127.0.0.1" + }, + { + "id": "cmd-002", + "name": "Enable Cube", + "data": "{\"command\":\"set_object\",\"target\":\"TestCube\",\"value\":\"true\"}", + "ipAddress": "127.0.0.1" + }, + { + "id": "cmd-003", + "name": "Disable Cube", + "data": "{\"command\":\"set_object\",\"target\":\"TestCube\",\"value\":\"false\"}", + "ipAddress": "127.0.0.1" + }, + { + "id": "cmd-004", + "name": "Start Timer (10s)", + "data": "{\"command\":\"start_timer\",\"value\":\"10\"}", + "ipAddress": "127.0.0.1" + }, + { + "id": "cmd-005", + "name": "Stop Timer", + "data": "{\"command\":\"stop_timer\"}", + "ipAddress": "127.0.0.1" + }, + { + "id": "cmd-006", + "name": "Reset Scene", + "data": "{\"command\":\"reset_scene\"}", + "ipAddress": "127.0.0.1" + }, + { + "id": "cmd-007", + "name": "Rotate Cube Fast", + "data": "{\"command\":\"rotate_cube\",\"value\":\"100\"}", + "ipAddress": "127.0.0.1" + }, + { + "id": "cmd-008", + "name": "Rotate Cube Slow", + "data": "{\"command\":\"rotate_cube\",\"value\":\"20\"}", + "ipAddress": "127.0.0.1" + }, + { + "id": "cmd-009", + "name": "Stop Rotation", + "data": "{\"command\":\"rotate_cube\",\"value\":\"0\"}", + "ipAddress": "127.0.0.1" + }, + { + "id": "cmd-010", + "name": "Change Color Red", + "data": "{\"command\":\"change_color\",\"value\":\"red\"}", + "ipAddress": "127.0.0.1" + }, + { + "id": "cmd-011", + "name": "Change Color Blue", + "data": "{\"command\":\"change_color\",\"value\":\"blue\"}", + "ipAddress": "127.0.0.1" + }, + { + "id": "cmd-012", + "name": "Change Color Green", + "data": "{\"command\":\"change_color\",\"value\":\"green\"}", + "ipAddress": "127.0.0.1" + } + ] +} +