Project Categories
Project Setting | Personal |
Team Size | 1 |
Role(s) | Creator / Developer |
Languages | C# |
Software/Tools Used | Unity (engine), Visual Studio (IDE) |
Status | Released, Further development on hiatus |
Time Period | May 2020 - June 2021 |
About
GW-Std-Unity is a repository of cross-project assets for my games. I noticed that I was re-implementing similar functionality for multiple projects, so I decided to make a universal, independent repo for these functions. Currently, this repo contains a robust tooltip system and management-game camera, extensions to Unity UI Sliders (planned to be refactored as child classes), a highly generalized rigidbody first person controller, and some miscellaneous utilities.
The project has gone under a large refactor to support being used with the Unity package manager. This is a work in progress. The biggest refactor was with the first person controller, which I redesigned after I was unhappy with it being a single large file. I designed it to be as modular as possible, each module being as easy as drag-and-dropping onto the controller's main gameobject and adding itself to the controller's update methods when necessary. I also renamed it to Character Controller with the hopes that it could be used first and third person, even with the ability to go between the two seemlessly.
My Work
Controllers
FirstPersonController
- Highly generalized rigidbody first person controller based on Unity's new Input System using its event-based features (to only read input values when there is input)
- Offers many settings (such as different speed settings depending on type of movement, controller's physical characteristics, force magnitudes, etc), with exposed settings that can be set by players (such as base FOV, whether speed modifiers are toggled or held, camera sensitivity, etc.) or by game context (such as speed multiplier, the ability to move, etc.)
- Invokes UpdateMovementState, a way for other optional systems to be tied to the controller and keep separation of concerns
- Includes a ControllerSoundSystem and ControllerAnimationSystem already made fully functional, prototype-ready (or even production-ready) samples for handling UpdateMovementState
- Includes a defined set of InputActions so that the controller is ready out-of-the-box with support for keyboard/mouse and gamepad inputs
- Full XML documentation
ManagementCamera
- Robust set of camera controllers (currently orbit-style and free-fly-style) for use in management-based games such a city-builders and god-games
- Optional modules such as gamepad cursor movement and camera mode swapping
- Account for collision with large world bodies such as terrain
- Abstract parent ManagementCamera class which provides the core functionality of the cameras
- Child ManagementCamera_ classes which implement specifics, currently differing methods of rotation
- Utilizes Unity's new Input System and provides a default set of controls with the corresponding ManagementCamera package
- Full XML documentation
- Fully functional Unity demo with instructions on usage
FreeFlyCam
- Rudimentary free-flying camera that can move along all three axes and rotate vertically/horizontally
- Ability to lock camera in place so that mouse input can affect other things (such as UI) rather than camera control (assuming included InputActions are used)
- Full XML documentation
UI
TooltipSystem
- Robust tooltipping system inspired by Tippy.js which allows a structure for creating Tooltip Prefabs and assigning Tooltip Enabled Elements to any graphical element
- Fully functional Unity demo showing off the many features of TooltipSystem such as anchoring, attachment, arrowless/arrowed, transitions, timing, etc.
- Allows the ability to manipulate the colors and text of the tooltip at runtime
- Supports passing through style tags through to TextMeshPro
Slider Extensions
- SliderTextLoadExtension: a MonoBehaviour class that is attached to and tied with sliders, is handled externally (either directly through Unity event attachment or a custom event handler)
- SliderTransitionExtension: a class that is associated with a slider and an always-active parent MonoBehaviour, intended to be used with Dictionaries or similar, with the ability to update slider values easily and transitioning them under-the-hood
- Full XML documentation
- Fully functional Unity demo with instructions on usage
Samples
These are some samples from the project.
Videos
Demonstration of the tooltip system, with a walkthrough of the various fields of the tooltips
Management Camera demo, with an indicator of what type of motion / control is being demonstrated
First Person Controller Sound System demonstration; can also see other functions of the controller at play, such as animation and physical movement
Demonstration of the slider extensions
Code
PositionTooltipToElement
is called in MonoBehaviour.Start()
as tooltipInstance.RTransform.anchoredPosition = PositionTooltipToElement();
once the tooltip is initialized. This returns the position the tooltip shall be based on the position of its parent (i.e. thisRect
) in a clean switch
statement block with each case being whatever AnchorMode
is set for the TooltipEnabledElement
. PositionTooltipToCursor
operates on a similar principle; however, it 1. occurs in MonoBehavior.Update()
(assuming cursor anchoring is enabled); 2. must position based on the position of the cursor 3. must do a bit more when accounting for the arrow (if present). Tooltips anchored to elements become children of the element they are anchored to in the Unity transform hierarchy, while tooltips anchored to the cursor become children of the canvas itself so that element positioning doesn't conflict with cursor position.
/// <summary>
/// Positions the tooltip to the element for AnchorMode.AttachedToElement
/// </summary>
/// <returns>The position of the tooltip</returns>
private Vector2 PositionTooltipToElement()
{
RectTransform thisRect = GetComponent<RectTransform>();
Vector2 newPos = Vector2.zero;
switch (anchorPosition)
{
case AnchorPosition.TopLeft:
newPos.x -= ((tooltipInstance.RTransform.sizeDelta.x / 2) + (thisRect.sizeDelta.x / 2)) * tooltipHorizontalFactor;
newPos.y += (tooltipInstance.RTransform.sizeDelta.y / 2) + (thisRect.sizeDelta.y / 2);
if (tooltipInstance.ArrowEnabled)
{
newPos.y += (tooltipInstance.Arrow.rectTransform.sizeDelta.y);
tooltipInstance.Arrow.rectTransform.anchoredPosition = new Vector2(0,
-((tooltipInstance.RTransform.sizeDelta.y / 2) + (tooltipInstance.Arrow.rectTransform.sizeDelta.y / 2)));
tooltipInstance.Arrow.rectTransform.rotation = Quaternion.Euler(0, 0, 0);
}
break;
case AnchorPosition.TopMiddle:
newPos.y += (tooltipInstance.RTransform.sizeDelta.y / 2) + (thisRect.sizeDelta.y / 2);
if (tooltipInstance.ArrowEnabled)
{
newPos.y += (tooltipInstance.Arrow.rectTransform.sizeDelta.y);
tooltipInstance.Arrow.rectTransform.anchoredPosition = new Vector2(0,
-((tooltipInstance.RTransform.sizeDelta.y / 2) + (tooltipInstance.Arrow.rectTransform.sizeDelta.y / 2)));
tooltipInstance.Arrow.rectTransform.rotation = Quaternion.Euler(0, 0, 0);
}
break;
// Repeat for remaining anchor definitions.
// The conditional-blocks checking for ArrowEnabled do change as the switch statement goes on,
// such as an absent negative sign or changing x instead of y or changing the rotation
}
return newPos;
}
The Goldenwere ManagementCamera
starts as an abstract class, with most of the implementation handled there. The current difference between ManagementCameras reside in how they perform rotation. This code sample shows how orbit rotation is handled for the orbit-based camera. Additional comments are added to give a step-by-step walkthrough. Note that variables with the prefix working
indicate private fields at class-level that get manipulated throughout these methods and applied during MonoBehaviour.Update()
/// <summary>
/// Performs camera rotation based on input
/// </summary>
/// <remarks>This method performs rotation around a raycasted or default point</remarks>
/// <param name="input">The current input (modified to account for device sensitivity scaling)</param>
protected override void PerformRotation(Vector2 input)
{
// 1. Get the horizontal and vertical rotations, where workingDesiredRotation is the original transform.rotation before input,
// and is multiplied by (input times sensitivity)
Quaternion horizontal = workingDesiredRotationHorizontal * Quaternion.Euler(0, input.x * settingRotationSensitivity, 0);
Quaternion vertical = workingDesiredRotationVertical * Quaternion.Euler(-input.y * settingRotationSensitivity, 0, 0);
// 2. The developer can vertically clamp the rotation of the camera to prevent it from over-tilting
Quaternion verticalClamped = vertical.VerticalClampEuler(verticalClamping.x, verticalClamping.y);
// 3. We need to do a bit of looking ahead to make sure vertical rotation accounts for camera smoothing.
// This prevents odd behavior like shooting the position too far up/down past clamping.
Vector3 eulerAngles;
if (verticalClamped.eulerAngles.x >= verticalClamping.y - settingRotationSensitivity * Time.deltaTime ||
verticalClamped.eulerAngles.x <= verticalClamping.x + settingRotationSensitivity * Time.deltaTime)
eulerAngles = new Vector3(0, input.x, 0);
else
eulerAngles = (transformTilt.right * -input.y) + (Vector3.up * input.x);
// 4. Orbital rotation requires rotating around a point; RotateSelfAroundPoint is an extension method part of the gw-std-unity CoreAPI Extensions class
Vector3 newPos = workingDesiredPosition.RotateSelfAroundPoint(rotationPoint, eulerAngles);
// 5. Make sure the new position won't collide with something around it in the direction it will be going
if (!WillCollideAtNewPosition(newPos, workingDesiredPosition - newPos))
{
workingDesiredRotationHorizontal = horizontal;
workingDesiredRotationVertical = verticalClamped;
workingDesiredPosition = newPos;
}
// 6. Account for downcasting when doing orbital rotation (downcasting is when the camera follows the height of the objects below it,
// that is, if terrain/objects start getting taller/higher, the camera moves up with it; if they get shorter/lower, the camera moves down with it)
if (downcastEnabled)
{
// 6a. Perform an additional check to determine if rotation is affected by downcasting
if (!downcastEnabledForRotation)
{
workingLostHeight = !Physics.Raycast(new Ray(transform.position, Vector3.down), out RaycastHit hitInfo, downcastMaxDistance);
if (!workingLostHeight)
workingLastHeight = Mathf.Abs(Vector3.Distance(transform.position, hitInfo.point));
}
// 6b. Otherwise, perform regular downcasting (essentially the same as movement's downcasting)
else
{
// I. Check to see if downcast was lost or regained and track the previous frame's loss state (don't perform if current frame lost downcast)
bool prevLostHeight = workingLostHeight;
workingLostHeight = !Physics.Raycast(new Ray(transform.position, Vector3.down), out RaycastHit hitInfo, downcastMaxDistance);
if (!workingLostHeight)
{
// II. Get the distance between the camera and the closest object
float dist = Mathf.Abs(Vector3.Distance(transform.position, hitInfo.point));
// IIIa. If the downcast was lost in the previous frame, the workingLastHeight to apply is just the distance
if (prevLostHeight)
workingLastHeight = dist;
// IIIb. Otherwise, if the distance changed is greater than floating point error, the original
// LastHeight is used along with distance to move DesiredPosition up/down based on downcast
// (this is where the actual result of downcasting takes place), and *then* LastHeight is updated
else if (Mathf.Abs(dist - workingLastHeight) >= float.Epsilon)
{
workingDesiredPosition.y += workingLastHeight - dist;
workingLastHeight = dist;
}
}
}
}
}
The controller sound system subscribes to the FirstPersonController's UpdateMovementState event and determines what to do with the audio (e.g. pitch/volume manipulation) and when to play it. It calls PlayAudio() when it is ready to play. PlayAudio determines what the player is stepping on in order to determine what sound to play. If the player is standing on a GameObject with a TerrainCollider, then it determines at what point on the terrain's texture splatmap the player is on in order to determine which sound(s) to play at what volume (can support multiple, blended sounds because of how the splatmap/returning values work). There is a working dictionary of materials that it checks against if the player is standing on a GameObject with a MeshRenderer which it checks against to determine which sound to play (because materials are instanced, materials names must be used as keys rather than the materials themselves that are set in the Inspector)
/// <summary>
/// Determines which audio to play based on what is underneath the controller
/// </summary>
private void PlayAudio()
{
if (Physics.Raycast(new Ray(transform.position, Vector3.down), out RaycastHit hit, attachedController.SettingsMovement.SettingNormalHeight + 0.1f, Physics.AllLayers))
{
if (hit.collider is TerrainCollider)
{
Terrain t = hit.collider.gameObject.GetComponent<Terrain>();
float[] currentLayerValues = ConvertPositionToTerrain(transform.position, t);
for (int i = 0; i < currentLayerValues.Length; i++)
{
if (currentLayerValues[i] > 0 && i < clipsTerrain.Length)
{
float textureVol = workingSource.volume * currentLayerValues[i];
workingSource.PlayOneShot(clipsTerrain[i], textureVol);
}
}
}
else
{
MeshRenderer mr = hit.collider.gameObject.GetComponent<MeshRenderer>();
if (mr != null)
{
Material mat = mr.material;
if (mat != null)
{
string sanitizedName = mat.name.Replace(" (Instance)", "");
if (workingMaterials.ContainsKey(sanitizedName))
workingSource.PlayOneShot(workingMaterials[sanitizedName]);
else
workingSource.PlayOneShot(clipDefaultMovement);
}
}
else
workingSource.PlayOneShot(clipDefaultMovement);
}
}
}
/// <summary>
/// Converts a world-space position (the controller's) to a position on the alphamap of a designated terrain
/// </summary>
/// <param name="worldPos">The position to check in worldspace</param>
/// <param name="t">The terrain being checked</param>
/// <returns>The strength values of each texture at the world position provided</returns>
private float[] ConvertPositionToTerrain(Vector3 worldPos, Terrain t)
{
Vector3 terrainPos = worldPos - t.transform.position;
Vector3 mapPos = new Vector3(terrainPos.x / t.terrainData.size.x, 0, terrainPos.z / t.terrainData.size.z);
Vector3 scaledPos = new Vector3(mapPos.x * t.terrainData.alphamapWidth, 0, mapPos.z * t.terrainData.alphamapHeight);
float[] layers = new float[t.terrainData.alphamapLayers];
float[,,] aMap = t.terrainData.GetAlphamaps((int)scaledPos.x, (int)scaledPos.z, 1, 1);
for (int i = 0; i < layers.Length; i++)
layers[i] = aMap[0, 0, i];
return layers;
}
Places
Source Code | GitHub Repo |