In our current development phase, we have designed an aiming mechanic for a mobile game using Unity and Photon Fusion. The goal was to create a control system that allows players to aim precisely using a right virtual joystick and throw objects (in this case, eggs). The system needed to be network-compatible and seamlessly integrate with our inventory and weapon system.
Requirements for the Aim System
Since our game is designed for mobile devices with touch controls, we needed a system that:
- Supports dual joystick controls (left joystick for movement, right joystick for aiming and throwing).
- Provides a dynamic trajectory visualization to help players aim accurately.
- Integrates with the inventory system, ensuring that only the currently equipped weapon activates the aim mechanic.
- Synchronizes through Fusion, ensuring lag compensation and hit registration in a multiplayer environment.
The Architecture of the Aim Mechanic
To make the aiming mechanic modular and flexible, we created an abstract base class AimMechanic
that defines all fundamental aiming functions. Each weapon can then have its own aiming system by inheriting from this class.
2.1. The Abstract AimMechanic
Class
The AimMechanic
class defines the basic structure for different weapon mechanics, ensuring all weapons follow a unified approach.
public abstract class AimMechanic : MonoBehaviour, IItemAimMechanic
{
protected IEngine m_engine => Engine.Instance;
protected Transform m_playerRootTransform = default;
protected LineRenderer m_lineRenderer = default;
protected float m_actionStickSensitivity = default;
public void Initalize()
{
m_playerRootTransform = m_engine.AvatarManager.LocalPlayer.gameObject.transform;
m_lineRenderer = GameObject.Find("LineRenderer").GetComponent<LineRenderer>();
m_actionStickSensitivity = m_engine.InputManager.ActionstickSensitivity;
}
public void StartAiming(Vector2 _joystickDirection) => OnStartAiming(_joystickDirection);
public void UpdateAiming(Vector2 _joystickDirection) => OnUpdateAiming(_joystickDirection);
public void EndAiming(Action<Vector3> _endPoint) => OnEndAiming(_endPoint);
protected abstract void OnStartAiming(Vector2 _joystickDirection);
protected abstract void OnUpdateAiming(Vector2 _joystickDirection);
protected abstract void OnEndAiming(Action<Vector3> _endPoint);
}
This class ensures that all aim mechanics use a consistent API and can be easily extended in the future.
2.2. The ThrowAimMechanic
for Throwing Eggs
Since eggs are our primary weapon, we needed a throwing mechanic. The ThrowAimMechanic
class extends AimMechanic
, calculates the throw trajectory, and displays it using a LineRenderer
.
public sealed class ThrowAimMechanic : AimMechanic
{
private float m_throwForce = 10f;
private int m_resolution = 12;
private float m_curveHeight = 2.5f;
private Vector3[] m_points = default;
public Action<Vector3[]> m_actionEndCallback = default;
protected override void OnStartAiming(Vector2 _joystickDirection)
{
m_lineRenderer.enabled = true;
}
protected override void OnUpdateAiming(Vector2 _joystickDirection)
{
Vector3 direction = new Vector3(_joystickDirection.x, 0, _joystickDirection.y).normalized;
float strength = Mathf.Clamp((_joystickDirection.magnitude / 100) * m_actionStickSensitivity, 0f, 1f);
Vector3 velocity = direction * strength * m_throwForce;
DrawTrajectory(velocity);
}
protected override void OnEndAiming(Action<Vector3> _endPoint)
{
_endPoint?.Invoke(m_points[m_points.Length - 1]);
m_actionEndCallback?.Invoke(m_points);
m_points = null;
m_lineRenderer.enabled = false;
}
private void DrawTrajectory(Vector3 velocity)
{
m_lineRenderer.positionCount = m_resolution;
m_points = new Vector3[m_resolution];
Vector3 startPosition = m_playerRootTransform.position;
for (int i = 0; i < m_resolution; i++)
{
float t = i / (float)(m_resolution - 1);
Vector3 point = Vector3.Lerp(startPosition, velocity + startPosition, t);
point.y = Mathf.Lerp(startPosition.y, (velocity + startPosition).y, t) + m_curveHeight * 4 * t * (1 - t);
m_points[i] = point;
}
m_lineRenderer.SetPositions(m_points);
}
}
This class ensures that the player gets a visual preview of the trajectory and that the egg is thrown in the exact calculated direction when the joystick is released.
Integration into the Input System
To allow the player to use the aiming mechanic with touch controls, we integrated it into the InputManagerTouch
. The right joystick controls aiming, and releasing the joystick executes the throw.
void IInputManager.SetItemAimMechanic(IItemAimMechanic _aimMechanic)
{
m_itemAimMechanic = _aimMechanic;
}
When the player moves the right joystick, the aiming mechanic activates:
if (m_itemAimMechanic != null)
{
m_itemAimMechanic.UpdateAiming(touch.position - m_rightStickTouchStartPos);
}
And when the joystick is released, the throw is executed:
if (m_itemAimMechanic != null)
{
m_itemAimMechanic.EndAiming(endPoint => { });
}
Problems & Solutions
During the implementation, we encountered several challenges:
- Multiple weapons were setting the aim mechanic simultaneously.
→ Solution: Only the active weapon should callSetItemAimMechanic()
. - Network synchronization of the trajectory was inconsistent.
→ Solution: Store the aim direction inNetworkInputData
for consistency. - Joystick sensitivity was too inaccurate.
→ Solution: Use progressive scaling withMathf.Pow()
to improve precision.
Conclusion & Next Steps
With this new aiming mechanic, we have built a modular system that can be easily extended. The foundation is in place, but there are still several features we want to implement:
- Different weapons with unique aiming mechanics (e.g., auto-lock targeting or explosive projectiles).
- Network synchronization of the projectile trajectory for precise hit registration in multiplayer matches.
- Visual improvements such as dynamic UI elements for aiming feedback.
This mechanic is a crucial milestone for our game, making touch controls more intuitive and precise.