-->
This Project Gal Dev Log provides a brief overview of the first-person aim-control I implemented in a recent development sprint. I will go over my initial goal for this sprint, the challenges that I ran into, and a brief discussion of my thought processes leading to accomplishing this sprint goal.
Since Gal is inspired by the classic carnival gallery shooting games, the initial goal was to allow the player an angle-restricted first-person camera control, using the mouse/gamepad. A sub-challenge emerged after accomplishing this goal, where the player-model's weapon-hand did not follow the player's view as it should in a first-person shooter-type game. A bulk of this sprint was spent solving this aspect.
Above:Just like its carnival counterpart, spring-loaded targets appear from behind various stage props. In Gal, the player traverses the level on a dolly-track. The dolly track aspect is inspired by my love of the Disneyland attraction: Toy Story Midway Mania.
Since Gal is a sort-of fps game, the primary task was to implement fps-style aiming controls.
I came to an initial working-solution for the fps control implementation; however, I never felt satisfied with the overly complicated nature of the technique I was adapting. This solution required the use of a custom Cinemachine Virtual Camera Extension. The extension script constantly threw warning messages in my console.
In an attempt to simplify things, I imported the official Unity Starter Package First-Person Controller. After importing the package, my editor started to crash occasionally.
To eventually overcome the instability caused by the First-Person Controller package, I created a new empty Unity project. I imported the asset package to this project and deleted anything I didn't need. I then exported it as a new package with a trimmed set of assets. The editor crash issue has since ceased.
From then on, player-controlled Camera rotation was made possible using the Unity Input System Package. A Player Input Component on my Input Manager game object is in charge of triggering a Unity Event for each of the registered input action the user performs.
In my case, the OnLook input event trigger was set to call the FirstPersonController.onLook(CallbackContext) function every time the player moves the mouse / joystick.
The player camera's updated pitch and yaw are calculated and applied to the camera's transform each time the OnLook(CallbackContext) function is called. Inside this OnLook(CallbackContext) function is a simple call to a pre-existing function from the Starter Asset First-Person Controller Script I'm modifying:
// Public function for the Unity Event to call when a "Look" occurs
public void OnLook(CallbackConteblockxt context) {
CameraRotation(context.ReadValue<Vector2>());
}
Initially, players had the ability to view in all directions with no angular-limit, which turned out to be the source of a few distinct issues:
As can be seen in the following snippet, camera rotation is being clamped in 4 distinct directions. The original file I am modifying only clamped the two-axes symmetrically (one clamp for horizontal, one clamp for vertical).
I introduced the 4-direction clamp to remedy a couple of issues:
// CameraRotation provided with StarterAsset Package
// I modified it by adding distinct 4-way clamp on camera pitch/yaw
private void CameraRotation(Vector2 lookVec) {
/*
Other Code
*/
// Clamp pitch
// ("Upward" pitch has a separate purpose than "Downward pitch)"
_cinemachineTargetPitch =
ClampAngle(_cinemachineTargetPitch, BottomClamp, TopClamp);
// Clamp yaw
// (Remains symmetrical, but given optional "Leftward"/"Rightward")
_cinemachineTargetYaw =
ClampAngle(_cinemachineTargetYaw, LeftClamp, RightClamp);
// Update Cinemachine camera target pitch and yaw
CinemachineCameraTarget.transform.localRotation =
Quaternion.Euler(_cinemachineTargetPitch, _cinemachineTargetYaw, 0.0f);
}
// Pre-existing code that is called within the CameraRotation function
private static float ClampAngle(float lfAngle, float lfMin, float lfMax) {
if (lfAngle < -360f) lfAngle += 360f;
if (lfAngle > 360f) lfAngle -= 360f;
return Mathf.Clamp(lfAngle, lfMin, lfMax);
}
I was able to determine the optimal clamp values by exposing a float value for each clamp to the editor. This allowed me to adjust during play-mode testing.
// How far in degrees can you move the camera upward
[SerializeField] private float TopClamp = 90.0f;
// How far in degrees can you move the camera downward
[SerializeField] private float BottomClamp = -90.0f;
// How far in degrees can you move the camera leftward
[SerializeField] private float LeftClamp = 90.0f;
// How far in degrees can you move the camera rightward
[SerializeField] private float RightClamp = -90.0f;
Additionally, with this solution in place, I began to see player-model artifacts permeating the camera frustum. These were small cross-sections of the player-model's face. The solution itself didn't cause these artifacts, but since the camera was not in the player's control, new issues such as this began to surface.
I solved the issue with model artifacts bleeding into view quickly by simply translating the neck bone from the player character armature toward the inside of the player character's model.
Above: The model's neck bone being returned to its original position.
Haphazard importation of 3rd-party models and assets throughout the early development stages left some prefab's transforms in awkward states of disorientation. This made it especially difficult to get a consistent set of vectors to use when trying to manipulate the player-model's arm toward the camera/aim.
To address the orientation issues, I aligned the left-arm into a T-pose, ensuring that all arm bones pointed uniformly forward. I then carefully aligned the revolver pistol into the arm's hand, aligning it with the arm's forward vector direction.
Above: The model's left-hand armature in alignment.
This was a key step in attaining the expected arm behavior. I could then easily place an Animation Rigging package Multi-Aim Constraint component in the armature hierarchy to automatically do the work of rotating the shoulder of the arm/hand holding the gun, effectively aiming the revolver!
Check out the Unity docs for more on Multi-Aim Constraint
Above: The MultiAimConstraint Component
Above: The arm aiming as intended.
A raycast was employed to determine the player's aim point. The ray was cast from the player toward the world mouse position. The world mouse position was easily attained via the CallbackContext object passed to the OnLook function when the input asset system invoked the OnLook function.
// transformPos is the transform component of a sphere GameObject
// that is passed in through the editor
// It acts as a visual debugging tool
transformPos = null;
// Calculate screen center point with simple arithmetic
Vector2 screenCenterPoint = new Vector2(Screen.width / 2f, Screen.height / 2f);
// Cast a ray to this calculated point
Ray ray = Camera.main.ScreenPointToRay(screenCenterPoint);
// Check for a ray-hit
// The collider mask is a LayerMask set in editor that helps determine valid objects
if (Physics.Raycast(ray, out RaycastHit raycastHit, 999f, aimColliderMask))
{
// Set our world anchor/debug sphere transform
transformPos = raycastHit.transform;
// Perform Shoot (covered in a later sprint log)
Shoot(raycastHit);
}
Above: The orange sphere is at the same transform as the raycastHit.point
Additionally, an invisible bounding box was placed around the level to give the casted ray a place to hit no matter where the player aims. This was to ensure that there was always a new point to update the debug sphere in the previously seen gif, as well as the arm's aim vector. Otherwise, when aiming in the sky, the sphere/arm would stop moving until the ray cast logic identified another collision.
Initial player feedback indicates the need for a smoother camera control. Players indicated that the camera can move too quickly, making it harder to control and thus potentially less enjoyable.
As of this log, a few issues remain and need attention:
In the next Project Gal Dev Log, I plan to discuss the shoot input control and its callback listener OnShoot(CallbackContext). You can see a preview of my progress in the following gif!
Above: Wait... your guns can shoot?
In conclusion, the development of the aiming mechanic in Project GAL involved overcoming challenges related to package importation, world/local vector orientation issues, camera clipping artifacts, inverse kinematic rigging and its MultiAimConstraint component.
Re-orienting the character model's armature to a consistent forward vector orientation, debugging with raycastHit for world mouse position, and the MultiAimConstraint component technique, are what eventually led me to successfully implementing the view mechanic, and subsequent in solving the challenge of syncing the player-model's arm with that view mechanic!
I could go on for some time more about this sprint, but I'm eager to keep developing!