← back to home

Path Of Chaos

V1.0

A Unity FPS built for a TikTok livestream agency — the game runs a local HTTP listener that receives GET requests from the agency's app, turning viewer gifts into real-time gameplay events (enemy spawns, buffs, sabotage, win/lose).

Engine Unity
Language C#
Platform Tiktok
Platform Windows
Genre Live Event Game
Released July 2026
PLAYABLE DEMO / MEDIA
ABOUT THIS PROJECT

This project is a Unity FPS built for a TikTok LIVE agency, turning viewer gifts into real-time gameplay events during a live broadcast. The game runs a local HTTP listener on a dedicated port, and the agency's own gift-processing application sends simple GET requests to that port whenever a viewer sends a gift — each request is parsed and instantly translated into an in-game action: spawning enemies, granting or draining ammo, unlocking weapons, teleporting the player, or ending the match in a win or loss. On the technical side, the project features a state-machine-driven flying enemy AI (bone-tracked hitboxes, particle-collision-based continuous flame damage, and height/distance-aware flight behavior), NavMesh-based ground enemies, a dynamic weapon inventory system that preserves per-weapon ammo across switches, and a custom local HTTP listener that safely parses incoming requests on a background thread and dispatches them to the main thread to avoid Unity API/thread-safety issues. The game was built to keep latency low enough for real-time, live-broadcast interaction between viewers and the player.

SCREENSHOTS
LINKS
CODE SNIPPETS
WebReceiver.cs
using UnityEngine;
using System;
using System.Net;
using System.Text;
using System.Threading;
using System.Collections.Generic;

public class WebReceiver : MonoBehaviour
{
    [Header("Webhook Settings")]
    [SerializeField] private string listenerUrl = "http://+:7745/";

    [Header("References")]
    [SerializeField] private SpawnManager spawnManager;

    private HttpListener httpListener;
    private Thread listenerThread;
    private bool isRunning = false;

    private readonly object queueLock = new object();
    private readonly Queue<WebhookData> actionQueue = new Queue<WebhookData>();

    private void Start()
    {
        StartServer();
    }

    private void OnDestroy()
    {
        StopServer();
    }

    private void OnApplicationQuit()
    {
        StopServer();
    }

    private void Update()
    {
        lock (queueLock)
        {
            while (actionQueue.Count > 0)
            {
                WebhookData data = actionQueue.Dequeue();
                ProcessWebhookData(data);
            }
        }
    }

    private void StartServer()
    {
        try
        {
            httpListener = new HttpListener();
            httpListener.Prefixes.Add(listenerUrl);
            httpListener.Start();
            isRunning = true;

            listenerThread = new Thread(ListenLoop);
            listenerThread.IsBackground = true;
            listenerThread.Start();

            Debug.Log($"<color=green>[Webhook]</color> Server started, listening on: {listenerUrl}");
        }
        catch (Exception ex)
        {
            Debug.LogError("[Webhook] Failed to start server: " + ex);
        }
    }

    private void StopServer()
    {
        isRunning = false;

        if (httpListener != null)
        {
            try
            {
                httpListener.Stop();
                httpListener.Close();
            }
            catch { }
            httpListener = null;
        }

        if (listenerThread != null && listenerThread.IsAlive)
        {
            try
            {
                listenerThread.Abort();
            }
            catch { }
        }
    }

    private void ListenLoop()
    {
        while (isRunning && httpListener != null && httpListener.IsListening)
        {
            try
            {
                IAsyncResult result = httpListener.BeginGetContext(OnRequestProcessed, httpListener);
                result.AsyncWaitHandle.WaitOne();
            }
            catch (Exception)
            {
                break;
            }
        }
    }

    private void OnRequestProcessed(IAsyncResult result)
    {
        try
        {
            HttpListenerContext context = httpListener.EndGetContext(result);
            WebhookData data = ParseRequest(context.Request);

            RespondOk(context.Response);

            lock (queueLock)
            {
                actionQueue.Enqueue(data);
            }
        }
        catch (Exception ex)
        {
            Debug.LogError("[Webhook Request Error]: " + ex.Message);
        }
    }

    private WebhookData ParseRequest(HttpListenerRequest request)
    {
        WebhookData data = new WebhookData();
        if (!request.QueryString.HasKeys())
            return data;

        if (int.TryParse(request.QueryString["enemies"], out int enemyCount)) data.enemyCount = enemyCount;
        if (int.TryParse(request.QueryString["ammo"], out int ammoChange)) data.ammoChange = ammoChange;
        if (int.TryParse(request.QueryString["health"], out int healthChange)) data.healthChange = healthChange;

        if (!string.IsNullOrEmpty(request.QueryString["action"]))
            data.action = request.QueryString["action"].ToLower();

        string incomingName = request.QueryString["username"];
        if (!string.IsNullOrEmpty(incomingName))
            incomingName = incomingName.Replace("{", "").Replace("}", "");

        data.senderName = string.IsNullOrWhiteSpace(incomingName) ? "Viewer" : incomingName;

        return data;
    }

    private void RespondOk(HttpListenerResponse response)
    {
        byte[] responseBytes = Encoding.UTF8.GetBytes("OK");
        response.ContentLength64 = responseBytes.Length;
        response.OutputStream.Write(responseBytes, 0, responseBytes.Length);
        response.OutputStream.Close();
    }

    private void ProcessWebhookData(WebhookData data)
    {
        try
        {
            Debug.Log($"<color=yellow>[WEBHOOK]</color> Enemies={data.enemyCount}, Ammo={data.ammoChange}, Health={data.healthChange}, Action={data.action}, Sender={data.senderName}");

            ApplyEnemySpawn(data);
            ApplyAmmoChange(data);
            ApplyHealthChange(data);
            ApplyAction(data);
        }
        catch (Exception ex)
        {
            Debug.LogError("Webhook Processing Error: " + ex);
        }
    }

    private void ApplyEnemySpawn(WebhookData data)
    {
        if (data.enemyCount <= 0) return;

        int enemyId = Mathf.Clamp(data.enemyCount, 1, 5);
        if (spawnManager == null)
            spawnManager = FindObjectOfType<SpawnManager>();

        spawnManager?.SpawnEnemyByCoins(enemyId, data.senderName, 1);
    }

    private void ApplyAmmoChange(WebhookData data)
    {
        if (data.ammoChange == 0) return;

        var gunInventory = FindObjectOfType<GunInventory>();
        if (gunInventory == null) return;

        int amount = Math.Abs(data.ammoChange) / 20;
        if (data.ammoChange > 0)
            gunInventory.IncreaseAmmo(amount, data.senderName);
        else
            gunInventory.DecreaseAmmo(amount, data.senderName);
    }

    private void ApplyHealthChange(WebhookData data)
    {
        if (data.healthChange == 0 || GameManager.Instance == null) return;

        if (data.healthChange > 0)
            GameManager.Instance.HealPlayer(data.healthChange, 1, data.senderName);
        else
            GameManager.Instance.DamagePlayer(Math.Abs(data.healthChange), 1, data.senderName);
    }

    private void ApplyAction(WebhookData data)
    {
        if (string.IsNullOrEmpty(data.action)) return;

        var player = GameObject.FindGameObjectWithTag("Player");

        switch (data.action)
        {
            case "unlockweapon":
                player?.GetComponent<GunInventory>()?.UnlockSecondWeapon(data.senderName);
                break;
            case "killall":
                player?.GetComponent<PlayerDevCheats>()?.KillAllEnemies(data.senderName);
                break;
            case "teleportstart":
                GameManager.Instance?.TeleportToStart(data.senderName);
                break;
            case "teleportend":
                GameManager.Instance?.TeleportToEnd(data.senderName);
                break;
            case "win":
                GameManager.Instance?.WinGame(10);
                break;
            case "lose":
                GameManager.Instance?.LoseGame(10);
                break;
        }
    }
}