Files
the-island/unity-client/Assets/Scripts/Billboard.cs
empty 64ed46215f feat: add Unity 6 client with 2.5D visual system
Unity client features:
- WebSocket connection via NativeWebSocket
- 2.5D agent visuals with programmatic placeholder sprites
- Billboard system for sprites and UI elements
- Floating UI panels (name, HP, energy bars)
- Speech bubble system with pop-in animation
- RTS-style camera controller (WASD + scroll zoom)
- Editor tools for prefab creation and scene setup

Scripts:
- NetworkManager: WebSocket singleton
- GameManager: Agent spawning and event handling
- AgentVisual: 2.5D sprite and UI creation
- Billboard: Camera-facing behavior
- SpeechBubble: Animated dialogue display
- CameraController: RTS camera with UI input detection
- UIManager: HUD and command input

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 12:17:04 +08:00

134 lines
3.8 KiB
C#

using UnityEngine;
namespace TheIsland.Visual
{
/// <summary>
/// Forces a 2D sprite or UI element to always face the camera.
/// Attach to any GameObject that should billboard towards the main camera.
/// </summary>
public class Billboard : MonoBehaviour
{
#region Configuration
[Header("Billboard Settings")]
[Tooltip("If true, locks the Y-axis rotation (sprite stays upright)")]
[SerializeField] private bool lockYAxis = true;
[Tooltip("If true, uses the main camera. Otherwise, assign a specific camera.")]
[SerializeField] private bool useMainCamera = true;
[Tooltip("Custom camera to face (only used if useMainCamera is false)")]
[SerializeField] private Camera targetCamera;
[Tooltip("Flip the facing direction (useful for some sprite setups)")]
[SerializeField] private bool flipFacing = false;
#endregion
#region Private Fields
private Camera _camera;
private Transform _cameraTransform;
#endregion
#region Unity Lifecycle
private void Start()
{
CacheCamera();
}
private void LateUpdate()
{
if (_cameraTransform == null)
{
CacheCamera();
if (_cameraTransform == null) return;
}
FaceCamera();
}
#endregion
#region Private Methods
private void CacheCamera()
{
_camera = useMainCamera ? Camera.main : targetCamera;
if (_camera != null)
{
_cameraTransform = _camera.transform;
}
}
private void FaceCamera()
{
if (lockYAxis)
{
// Only rotate around Y-axis (sprite stays upright)
Vector3 lookDirection = _cameraTransform.position - transform.position;
lookDirection.y = 0; // Ignore vertical difference
if (lookDirection != Vector3.zero)
{
Quaternion targetRotation = Quaternion.LookRotation(
flipFacing ? lookDirection : -lookDirection
);
transform.rotation = targetRotation;
}
}
else
{
// Full billboard - face camera completely
transform.rotation = flipFacing
? Quaternion.LookRotation(transform.position - _cameraTransform.position)
: _cameraTransform.rotation;
}
}
#endregion
#region Public Methods
/// <summary>
/// Set a custom camera to face (disables useMainCamera).
/// </summary>
public void SetTargetCamera(Camera camera)
{
useMainCamera = false;
targetCamera = camera;
_camera = camera;
_cameraTransform = camera?.transform;
}
/// <summary>
/// Reset to use main camera.
/// </summary>
public void UseMainCamera()
{
useMainCamera = true;
CacheCamera();
}
/// <summary>
/// Configure billboard for UI elements (full facing, no Y-lock).
/// </summary>
public void ConfigureForUI()
{
lockYAxis = false;
flipFacing = false;
}
/// <summary>
/// Configure billboard for sprites (Y-axis locked, stays upright).
/// </summary>
public void ConfigureForSprite()
{
lockYAxis = true;
flipFacing = false;
}
/// <summary>
/// Set whether to lock Y-axis rotation.
/// </summary>
public void SetLockYAxis(bool locked)
{
lockYAxis = locked;
}
#endregion
}
}