Documentation
Overview
Anti-Wallhack Visibility System is a Unity-based server-side solution designed to prevent wallhacks and unauthorized player detection in competitive or multiplayer games. It validates whether a player truly has line-of-sight to another using precise linecast checks and directional sampling, ensuring visibility is based on legitimate rendering logic and not client-side manipulation.
Core Purpose
- Prevents wallhacks by validating visibility through geometry
- Ensures that visibility logic is server-authoritative
- Uses safe rendering logic based on face-aligned normals and line sampling
What it Offers
- Observer-based visibility sampling
- Dynamic line generation per face
- Field-of-view validation with aspect ratio and pitch control
- Real-time visibility events per observer/player pair
- Editor tools for debugging visibility in play mode
- UnityEvent integration for easy event handling in the Editor
Setup
1. Add the ObserverManager
Create an empty GameObject in your scene named ObserverManager and attach the ObserverManager component. This component manages all observers and players in the visibility system.
Note: ObserverManager uses [DefaultExecutionOrder(-200)] to ensure it initializes early in the Unity update cycle.
2. Add PlayerObserver to Observers
Attach PlayerObserver to the GameObject representing the player camera (camera, AI or player entity). The GameObject must track the position and rotation of the player's camera.
Configuration examples:
[ SerializeField ] private float cameraFOVAngle = 60f;
[ SerializeField ] private float viewDistance = 30f;
[ SerializeField ] private Vector2 aspectRatio = new (16f, 9f);
[ SerializeField ] private bool showGizmos = true;
3. Add PlayerVisibilityDetector to Players
Attach PlayerVisibilityDetector to each player to be detected (preferably at the root of the player GameObject). A BoxCollider (playerBox) is required to calculate face-aligned normals and generate sampling lines.
Layer and Collider notes:
- Place the
BoxCollideron a child GameObject of the player to ensure proper transforms - Configure
obstaclesMaskto include layers for walls/obstacles
4. (Optional) Add VisibilityUnityEventRelay
To handle visibility events without scripting, attach VisibilityUnityEventRelay to the same GameObject as PlayerVisibilityDetector and configure the onVisibilityChanged UnityEvent in the Inspector.
5. Connect Components
PlayerObserver, PlayerVisibilityDetector, and VisibilityUnityEventRelay auto-register with the ObserverManager at runtime.
6. Enable Gizmos for Debugging
In the Scene view, enable Gizmos to visualize FOV cones, face normals, and linecast paths.
Components
ObserverManager
Manages registration and unregistration of observers and players, tracks visibility states and dispatches OnVisibilityChanged events. Reuses observer IDs for efficiency. Uses [DefaultExecutionOrder(-200)] to initialize early.
Key fields and methods:
- Instance (singleton)
- OnVisibilityChanged (event)
- RegisterObserver / RegisterPlayer / UnregisterPlayer
- ChangeVisibility / IsPlayerVisibleToObserver / GetAllPlayersExcept
PlayerObserver
Represents the player's camera, tracking position and rotation. Defines FOV params and performs visibility checks based on distance, horizontal and vertical angles.
Key fields: cameraFOVAngle, viewDistance, aspectRatio, showGizmos.
PlayerVisibilityDetector
Calculates face normals, performs linecasts (fixed and dynamic lines), tracks visibility per observer, and notifies ObserverManager.
VisibilityUnityEventRelay
Relays visibility events to a UnityEvent for easy editor configuration. Requires PlayerVisibilityDetector on the same GameObject.
ObserverManagerEditor
Custom inspector for debugging (editor-only, no runtime impact). Shows lists and provides debug controls.
How It Works
The system uses a combination of Field of View (FOV), face-aligned normals, and line sampling to determine if a player is visible to an observer. Linecast validation ensures no obstacles block sampled lines.
Core concepts
- Field of View (FOV) — vertical and horizontal angles
- Face-Aligned Normals — sample directions from bounding box faces
- Line Sampling — fixed and dynamic lines per face
- Linecast Validation — physics checks for unobstructed paths
- Dynamic Line Speed — scanning speed for dynamic sampling
Example flow
- PlayerObserver detects players in FOV based on cameraFOVAngle and aspectRatio.
- System calculates normals and sampling lines (fixed + dynamic).
- Linecasts are performed for both fixed and dynamic lines.
- If any line reaches the target, visibility is confirmed.
- ObserverManager updates state and triggers events via OnVisibilityChanged or VisibilityUnityEventRelay.
Networking note
For deterministic multiplayer frameworks (e.g., Photon Fusion), call visibility logic in fixed-tick network methods (e.g., FixedUpdateNetwork()) — avoid Render() or Update() for linecast-based checks.
Customization
Field of View settings
Adjust cameraFOVAngle, viewDistance and aspectRatio to match the player's camera.
Line Sampling resolution
Tune fixedLineCount, dynamicLineCount and dynamicLineSpeed to balance accuracy and performance. Lower counts for distant targets.
Obstacle layers
Use obstaclesMask to include relevant layers that block visibility, avoiding false positives.
Event handling
Use VisibilityUnityEventRelay (recommended for editor-level integration) or subscribe programmatically to ObserverManager.OnVisibilityChanged.
Gizmo visualization
Toggle showGizmos to visualize normals, cones, fixed/dynamic lines and detected lines in the Scene view.
Performance tips
- Adjust
dynamicLineSpeedfor accuracy/performance trade-off - Lower line counts for distant targets
- Disable gizmos in builds
- Use layer masks to limit linecasts
- Pool data and profile frequently
Integration Examples (full)
Full integration examples for the Anti-Wallhack Visibility System. Select a framework tab to view the complete classes and notes, and replace where necessary.
Photon Fusion
Full classes adapted for Fusion. Note: use Runner.IsForward + Runner.IsServer to run logic only on valid server forward ticks.
// NetworkPlayerVisibilityHandler.cs
using Fusion;
using UnityEngine;
public class NetworkPlayerVisibilityHandler : MonoBehaviour
{
[SerializeField] private NetworkTRSP networkTransform; // or your network transform wrapper
}
// NetworkPlayerObserver.cs
using Fusion;
using UnityEngine;
public class NetworkPlayerObserver : NetworkBehaviour
{
// FixedUpdateNetwork runs on Fusion's network ticks
public override void FixedUpdateNetwork()
{
// If server, perform detection. Runner.IsForward ensures execution only on forward tick (no rollback execution)
if (Runner.IsServer && Runner.IsForward)
{
PingMS = (float) Runner.GetPlayerRtt(Object.InputAuthority) * 1000f;
CameraDirection = transform.forward;
playerObserver.CalculateDynamicFOV(CameraDirection, PingMS);
playerObserver.DetectVisiblePlayersByFOV();
}
}
private void HandleVisibility(int observerId, PlayerVisibilityDetector player, bool isVisible)
{
// If visibility change affects this observer, send RPC to player owner
if (observerId != playerObserver.ObserverId || !Runner.IsServer) return;
playerObserver.IsVisible = isVisible;
RPC_VisibilityState(player.Object.InputAuthority, isVisible);
}
[Rpc(RpcSources.StateAuthority, RpcTargets.InputAuthority)]
private void RPC_VisibilityState(PlayerRef playerRef, NetworkBool state)
{
// On client side, perform local actions or toggle components
if (Runner.IsServer) return;
if (Runner.TryGetPlayerObject(playerRef, out var networkObject) &&
networkObject.TryGetComponent<PlayerVisibilityHandler>(out var handler))
{
handler.ToggleComponent(state);
}
}
}
// NetworkPlayerVisibilityDetector.cs
using Fusion;
using UnityEngine;
public class NetworkPlayerVisibilityDetector : NetworkBehaviour
{
public override void FixedUpdateNetwork()
{
// Corrected: only execute on server AND on forward ticks to avoid execution during rollbacks
if (Runner.IsServer && Runner.IsForward)
{
playerVisibilityDetector.DeltaTime = Runner.DeltaTime;
playerVisibilityDetector.UpdateVisibilityFromAlignedNormals();
}
}
}
Notes:
- Replace NetworkTRSP with your networking transform wrapper if needed.
- Keep visibility checks on server (host) only. Use Runner.IsForward to ensure valid forward ticks in Fusion.
Mirror
Mirror examples using NetworkBehaviour; run detection only on isServer and while component is enabled.
// PlayerVisibilityHandler.cs
using Mirror;
using UnityEngine;
public class PlayerVisibilityHandler : NetworkBehaviour
{
[SerializeField] private NetworkTransform networkTransform;
private readonly Vector3 _hiddenPosition = new Vector3(0f, 0f, 0f);
public void ToggleComponent(bool state)
{
if (networkTransform != null)
networkTransform.enabled = state;
if (!state)
transform.position = _hiddenPosition;
}
}
// PlayerObserver.cs
using Mirror;
using UnityEngine;
public class PlayerObserver : NetworkBehaviour
{
[SyncVar] public int ObserverId;
private void FixedUpdate()
{
// Mirror: guard by server authority and ensure component active
if (isServer && isActiveAndEnabled)
{
var ping = (float)NetworkTime.rtt * 1000f;
fovLatencyCompensation = GetFOVMultiplierFromPing(ping);
DetectVisiblePlayersByFOV();
}
}
private void HandleVisibility(int observerId, PlayerVisibilityDetector player, bool isVisible)
{
if (observerId != ObserverId || !isServer) return;
// Send target RPC to client to toggle their visibility handler
RpcVisibilityState(player.netIdentity.connectionToClient, isVisible);
}
[TargetRpc]
private void RpcVisibilityState(NetworkConnection target, bool state)
{
if (target.identity != null &&
target.identity.TryGetComponent<PlayerVisibilityHandler>(out var handler))
{
handler.ToggleComponent(state);
}
}
}
// PlayerVisibilityDetector.cs
using Mirror;
using UnityEngine;
public class PlayerVisibilityDetector : NetworkBehaviour
{
private void FixedUpdate()
{
// Mirror: run only on active server
if (isServer && isActiveAndEnabled)
{
_deltaTime = Time.deltaTime;
UpdateVisibilityFromAlignedNormals();
}
}
}
Mirror notes: use isServer plus component enabled checks. Use Mirror RPCs (TargetRpc / ClientRpc) for notifying clients.
Netcode for GameObjects (Unity Netcode)
// PlayerVisibilityHandler.cs
using Unity.Netcode;
using UnityEngine;
public class PlayerVisibilityHandler : NetworkBehaviour
{
[SerializeField] private Unity.Netcode.Components.NetworkTransform networkTransform;
private readonly Vector3 _hiddenPosition = new Vector3(0f,0f,0f);
public void ToggleComponent(bool state)
{
if (networkTransform != null)
networkTransform.enabled = state;
if (!state)
transform.position = _hiddenPosition;
}
}
// PlayerObserver.cs
using Unity.Netcode;
using UnityEngine;
public class PlayerObserver : NetworkBehaviour
{
public NetworkVariable<int> ObserverId = new NetworkVariable<int>(0);
public override void FixedUpdateNetwork()
{
if (IsServer && IsSpawned)
{
var ping = (float)(NetworkManager.Singleton.NetworkTime.Rtt * 1000f);
fovLatencyCompensation = GetFOVMultiplierFromPing(ping);
DetectVisiblePlayersByFOV();
}
}
}
// PlayerVisibilityDetector.cs
using Unity.Netcode;
using UnityEngine;
public class PlayerVisibilityDetector : NetworkBehaviour
{
private void FixedUpdate()
{
// Run on server and only for spawned network objects
if (IsServer && IsSpawned)
{
_deltaTime = Time.fixedDeltaTime;
UpdateVisibilityFromAlignedNormals();
}
}
}
Netcode notes: use FixedUpdateNetwork or server-side guards; use ClientRpc for notifications.
Photon PUN
// PlayerVisibilityHandler.cs
using Photon.Pun;
using UnityEngine;
public class PlayerVisibilityHandler : MonoBehaviourPun
{
[SerializeField] private PhotonTransformView photonTransformView;
private readonly Vector3 _hiddenPosition = new Vector3(0f, 0f, 0f);
public void ToggleComponent(bool state)
{
if (photonTransformView != null)
photonTransformView.enabled = state;
if (!state)
transform.position = _hiddenPosition;
}
}
// PlayerObserver.cs
using Photon.Pun;
using UnityEngine;
public class PlayerObserver : MonoBehaviourPun
{
public int ObserverId { get; private set; }
private void FixedUpdate()
{
if (PhotonNetwork.IsMasterClient)
{
var ping = (float)PhotonNetwork.GetPing();
fovLatencyCompensation = GetFOVMultiplierFromPing(ping);
DetectVisiblePlayersByFOV();
}
}
private void HandleVisibility(int observerId, PlayerVisibilityDetector player, bool isVisible)
{
if (observerId != ObserverId || !PhotonNetwork.IsMasterClient) return;
photonView.RPC(nameof(RPC_VisibilityState), player.photonView.Owner, isVisible);
}
[PunRPC]
private void RPC_VisibilityState(bool state, PhotonMessageInfo info)
{
var sender = PhotonView.Find(info.Sender.ActorNumber);
if (sender != null && sender.TryGetComponent<PlayerVisibilityHandler>(out var handler))
{
handler.ToggleComponent(state);
}
}
}
// PlayerVisibilityDetector.cs
using Photon.Pun;
using UnityEngine;
public class PlayerVisibilityDetector : MonoBehaviourPun
{
private void FixedUpdate()
{
if (PhotonNetwork.IsMasterClient)
{
_deltaTime = Time.deltaTime;
UpdateVisibilityFromAlignedNormals();
}
}
}
PUN notes: master client acts as server; use photonView.RPC to notify clients.
Fish-Networking
// PlayerVisibilityHandler.cs
using FishNet.Object;
using FishNet.Component.Transforming;
using UnityEngine;
public class PlayerVisibilityHandler : NetworkBehaviour
{
[SerializeField] private TransformSynchronizer transformSync;
private readonly Vector3 _hiddenPosition = new Vector3(0f, 0f, 0f);
public void ToggleComponent(bool state)
{
if (transformSync != null)
transformSync.enabled = state;
if (!state)
transform.position = _hiddenPosition;
}
}
// PlayerObserver.cs
using FishNet.Object;
using UnityEngine;
public class PlayerObserver : NetworkBehaviour
{
public int ObserverId;
public override void FixedUpdateNetwork()
{
// FishNet host/server tick execution if available
if (IsServer)
{
var ping = (float)(base.TimeManager.RoundTripTime * 1000f);
fovLatencyCompensation = GetFOVMultiplierFromPing(ping);
DetectVisiblePlayersByFOV();
}
}
private void HandleVisibility(int observerId, PlayerVisibilityDetector player, bool isVisible)
{
if (observerId != ObserverId || !IsServer) return;
if (player.Owner.IsValid)
RpcVisibilityState(player.Owner, isVisible);
}
[TargetRpc]
private void RpcVisibilityState(NetworkConnection conn, bool state)
{
if (conn.FirstObject != null &&
conn.FirstObject.TryGetComponent<PlayerVisibilityHandler>(out var handler))
{
handler.ToggleComponent(state);
}
}
}
// PlayerVisibilityDetector.cs
using FishNet.Object;
using UnityEngine;
public class PlayerVisibilityDetector : NetworkBehaviour
{
private void FixedUpdate()
{
// Run only on server host ticks
if (IsServer)
{
_deltaTime = Time.deltaTime;
UpdateVisibilityFromAlignedNormals();
}
}
}
FishNet notes: adapt RPC/TargetRpc usage according to FishNet API; ensure server-only execution.
Network Footstep Sound (examples per framework)
Examples below show how to trigger footstep sounds across the network when appropriate.
// NetworkFootStepSound (Photon Fusion)
using Fusion;
using UnityEngine;
public class NetworkFootStepSound : NetworkBehaviour
{
[SerializeField] private AudioClip[] footStepClips;
public void PlaySound(int index)
{
if (Object.HasStateAuthority)
RPC_PlaySound(index);
}
[Rpc(RpcSources.StateAuthority, RpcTargets.Proxies)]
private void RPC_PlaySound(int index)
{
if (index >= 0 && index < footStepClips.Length)
AudioSource.PlayClipAtPoint(footStepClips[index], transform.position);
}
}
// NetworkFootStepSound (Mirror)
using Mirror;
using UnityEngine;
public class NetworkFootStepSound : NetworkBehaviour
{
[SerializeField] private AudioClip[] footStepClips;
[Command]
public void CmdPlaySound(int index)
{
RpcPlaySound(index);
}
[ClientRpc(includeOwner = false)]
private void RpcPlaySound(int index)
{
if (index >= 0 && index < footStepClips.Length)
AudioSource.PlayClipAtPoint(footStepClips[index], transform.position);
}
}
// NetworkFootStepSound (Netcode for GameObjects)
using Unity.Netcode;
using UnityEngine;
public class NetworkFootStepSound : NetworkBehaviour
{
[SerializeField] private AudioClip[] footStepClips;
public void PlaySound(int index)
{
if (IsServer)
PlaySoundClientRpc(index);
}
[ClientRpc]
private void PlaySoundClientRpc(int index)
{
if (index >= 0 && index < footStepClips.Length)
AudioSource.PlayClipAtPoint(footStepClips[index], transform.position);
}
}
// NetworkFootStepSound (Photon PUN)
using Photon.Pun;
using UnityEngine;
public class NetworkFootStepSound : MonoBehaviourPun
{
[SerializeField] private AudioClip[] footStepClips;
public void PlaySound(int index)
{
if (PhotonNetwork.IsMasterClient)
photonView.RPC(nameof(RPC_PlaySound), RpcTarget.Others, index);
}
[PunRPC]
private void RPC_PlaySound(int index)
{
if (index >= 0 && index < footStepClips.Length)
AudioSource.PlayClipAtPoint(footStepClips[index], transform.position);
}
}
// NetworkFootStepSound (Fish-Networking)
using FishNet.Object;
using UnityEngine;
public class NetworkFootStepSound : NetworkBehaviour
{
[SerializeField] private AudioClip[] footStepClips;
public void PlaySound(int index)
{
if (IsServer)
RpcPlaySound(index);
}
[ObserversRpc(ExcludeOwner = true)]
private void RpcPlaySound(int index)
{
if (index >= 0 && index < footStepClips.Length)
AudioSource.PlayClipAtPoint(footStepClips[index], transform.position);
}
}
Footstep notes: choose the pattern that matches your networking framework; ensure server authority when triggering sound RPCs.
Gizmos & Editor Tools
The asset provides editor tools for real-time debugging:
- Visualize face normals
- Fixed and dynamic sampling lines
- Detection cone (green for visible, red for not)
- Visibility status indicators with distinct colors for fixed/dynamic/detected lines
ObserverManagerEditor
Custom inspector shows observer/player lists and visibility status for debugging editor workflows.
Troubleshooting
Players not detected — possible causes
- FOV angle or view distance too low
- Aspect ratio incorrect
- Sampling lines blocked by obstaclesMask misconfiguration
- playerBox not correctly placed as a child GameObject
- Insufficient dynamicLineSpeed
- PlayerObserver misaligned with camera
Events not firing — possible causes
- ObserverManager missing or inactive
- Registration failed
- Visibility state not changing due to low dynamicLineSpeed
- VisibilityUnityEventRelay not configured or missing PlayerVisibilityDetector
- Observer registration hierarchy invalid
Gizmos not showing
- Gizmos disabled
- Scene view not focused
showGizmosfalse
Solutions: Verify masks, ensure components are active, tune dynamicLineSpeed, and enable Gizmos for debugging.
FAQ
Does this system work with Photon Fusion or PUN 2?
Yes. Fully compatible. Use server/host logic and sync visibility states across clients. For Fusion prefer FixedUpdateNetwork() integration.
Can I use this for AI agents?
Yes. Observers can represent AI entities and VisibilityUnityEventRelay can trigger AI behaviours.
Is it compatible with URP or HDRP?
Yes. The system is rendering-independent and uses physics and geometry.
Does it support mobile platforms?
Yes. Reduce dynamicLineSpeed and line counts to optimize performance.
Can I customize the visibility logic?
Yes. The system is modular; override sampling or add filters as needed.