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).
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.
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;
}
}
}