Unity
Project Settings
At first when opening the Unity editor I was a bit overwhelmed by the many options available, and it can be hard to get going without at least knowing how to configure the most basic of settings for a Unity project. In the sections below, I'll cover some simple settings that are worthwhile to consider when creating a new project in Unity.
Playmode Tint
This option is not found in Project Settings
, but I think it is something everyone entering into Unity for the first time should consider. Navigate to the menu bar at the top of your editor and select Edit->Preferences->Colors
and adjust the Playmode Tint
to something very noticable. This will avoid forgetting you are in play mode and making some changes, only to be forced to revert them all once exiting play mode.
For the rest of these sections, we will be working in the Project Settings
panel opened with Edit->Project Settings...
in the menu bar of the Unity editor.
Official Unity Project Settings Documentation
Project Name
It is not to be assumed that Unity will distribute builds of your game with your local Unity project name as you defined it when creating your project initially. In fact, Unity requires us to specify these details within the Player
section of Project Settings
. See the section below is adjusted to suit the needs of your project.
Game / Application Icons
It's important to change things like this from the default settings, otherwise even a finished project can end up looking incomplete. Navigate to the Player
section and scroll down to adjust icon settings. It's important to be consistent across all platforms, and this can easily be done by checking the Override for PC, Max & Linux Standalone
tick box at the top of the panel. This will apply your Icon settings on all platforms.
Splash Screen
This is for Unity Professional Licenses only
Within the Player
panel we can find the below settings for modifying the splash screen of a game or application created with Unity.
Quality Settings
It's important to adjust quality settings to suit your development environment so you aren't running your game within the Unity editor in max settings.
You can rename quality levels, add new, and adjust platform-specific modes as well. It's important to note that clicking the name of the quality setting in this table (just left of the check-marks) will apply the settings within your editor for testing. The Default
drop-down arrors correspond with each platform at the top level of the table.
Graphics Settings
This is where you'll define the preconfigured graphics settings available to the player. Its important to adjust these to suit the platform the build will be running on. As an example, this feature could be useful when trying to distribute a test build of a Unity game with WebGL. We could reduce the settings to improve performance within the browser to make the game much less demanding. This allows us to build more efficiently to WebGL and not create an unecessarily demanding or slow performing game given this basic platform of WebGL.
Curious what WebGL is or looks like in use? I host some archived examples on my website, click here to check out some Unity WebGL games that I've already built and hosted online for playing.
Input Manager
This section is very useful in configuring controls for your game that can then be used for scripting. For example, the section below I have defined a button for Fire
, which is triggered when the player clicks the left mouse button
By using a custom script that defines global constants, we can reduce the task of changing these values later on. Below, I'll cover an example of using the Input Manager
paired with a few C# scripts to define controls in global variables which can be easily modified in a central location. This avoids a scenario where we have built a complex game and want to change controls later in development, requiring us to change static values across numerous scripts. This is not only tedious but also makes the project more prone to errors.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Controls : MonoBehaviour
{
// Constants used within the game to handle passing control settings to builtin unity functions with string parameters
// Unity parses these strings against settings in Edit->ProjectSettings->InputSettings
// Character walking controls for joystick / keyboard
// Keyboard has boolean movement, joystick has variable 0.0f - 1.0f
public const string c_MoveStrafe = "Horizontal";
public const string c_MoveWalk = "Vertical";
// Character look controls for mouse / joystick
public const string c_LookMouseVertical = "Mouse Y";
public const string c_LookMouseHorizontal = "Mouse X";
public const string c_LookGamePadVertical = "Look Y";
public const string c_LookGamePadHorizontal = "Look X";
// Character movement modifiers
public const string c_ModJump = "Jump";
public const string c_ModSprint = "Sprint";
public const string c_ModCrouch = "Crouch";
// Character weapon controls
public const string c_PrimaryAim = "Aim";
public const string c_PrimaryGamepadAim = "Gamepad Aim";
public const string c_PrimaryFire = "Fire";
public const string c_PrimaryGamepadFire = "Gamepad Fire";
public const string c_PrimarySwitchWeapon = "Mouse ScrollWheel";
public const string c_PrimaryGamepadSwitchWeapon = "Gamepad Switch";
public const string c_PrimaryHide = "Primary Hide";
public const string c_PrimaryNextWeapon = "NextWeapon";
// UI controls
public const string c_UIPauseMenu = "Pause Menu";
public const string c_UISubmit = "Submit";
public const string c_UICancel = "Cancel";
}
These constants can then be used in a relative PlayerInput
class, which can handle recieving input from the player at a higher level so we won't need to refactor all of our scripts in the scenario that we want to modify our controls.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// A script to pass player input to a relative GameObject's Controller script
public class PlayerInput : MonoBehaviour
{
PlayerController playerController;
// Start is called before the first frame update
void Start()
{
playerController = GetComponent<PlayerController>();
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
}
// Update is called once per frame
void Update()
{
// Always check if the player wants to unbind their cursor lock state
UpdateLockState();
}
public bool CanProcessInput()
{
// If the cursor is locked within the game, return true
return Cursor.lockState == CursorLockMode.Locked;
}
public Vector3 GetMovement()
{
// If the cursor is not locked within the game, do nothing
if (!CanProcessInput()) return Vector3.zero;
// Create a file / class (Controls.cs) to hold strings which can be passed to functions in all scripts
// Allows for easy control customization without reefactoring a lot of code
// GetAxis returns 0.0f-1.0f strength of movement (allows joystick variable movement, keyboard movement is 1.0f if key pressed)
// Horizontal = a,d Vertical = w,s
Vector3 move;
switch(playerController.targetPov)
{
case Kamera.pov.Mounted:
move = transform.forward * Input.GetAxis(Controls.c_MoveStrafe) * -1.0f + transform.right * Input.GetAxis(Controls.c_MoveWalk) * 1.0f ;
break;
default:
move = transform.right * Input.GetAxis(Controls.c_MoveStrafe) + transform.forward * Input.GetAxis(Controls.c_MoveWalk);
break;
}
// Return the clamped amnount of movement to apply to a GameObject within some relative Controller script
return move;
}
public float GetLookHorizontal()
{
return GetLookAxis(Controls.c_LookMouseHorizontal, Controls.c_LookGamePadHorizontal);
}
public float GetLookVertical()
{
return GetLookAxis(Controls.c_LookMouseVertical, Controls.c_LookGamePadVertical);
}
// If the player presses the UICancel key, the cursor is unlocked.
void UpdateLockState()
{
if (Input.GetButton(Controls.c_UICancel)) Cursor.lockState = CursorLockMode.None;
else if (Input.GetMouseButton(0)) Cursor.lockState = CursorLockMode.Locked;
}
// Checks whether the look input is via mouse or gamepad and returns a float 0.0f-1.0f of the strength
float GetLookAxis(string mouseLook, string stickLook)
{
if (CanProcessInput())
{
// Check if there is any input from a gamepad controller on the given axis
bool isGamePad = Input.GetAxis(stickLook) != 0.0f;
// If we are using a gamepad use stickLook's strength, otherwise use mouse input
float str = isGamePad ? Input.GetAxis(stickLook) : Input.GetAxis(mouseLook);
if (isGamePad)
{
// since mouse input is already deltaTime-dependant, only scale input with frame time if it's coming from sticks
str *= Time.deltaTime;
}
else
{
// reduce mouse input amount to be equivalent to stick movement
str *= 0.01f;
#if UNITY_WEBGL
// Mouse tends to be even more sensitive in WebGL due to mouse acceleration, so reduce it even more
// str *= webglLookSensitivityMultiplier;
#endif
}
return str;
}
else return 0.0f;
}
public bool GetCrouchDown()
{
return Input.GetButtonDown(Controls.c_ModCrouch);
}
public bool GetCrouchUp()
{
return Input.GetButtonUp(Controls.c_ModCrouch);
}
public bool GetSprintHeld()
{
return Input.GetButton(Controls.c_ModSprint);
}
public bool GetJumpPress()
{
return Input.GetButtonDown(Controls.c_ModJump);
}
public bool GetMouseFire()
{
return Input.GetButtonDown(Controls.c_PrimaryFire) || Input.GetButtonDown(Controls.c_PrimaryGamepadFire);
}
public bool GetMouseAlt()
{
return Input.GetButtonDown(Controls.c_PrimaryAim) || Input.GetButtonDown(Controls.c_PrimaryGamepadAim);
}
public bool GetLowerPrimary()
{
return Input.GetButtonDown(Controls.c_PrimaryHide);
}
public int GetNumberPress()
{
if(Input.GetKeyDown(KeyCode.Alpha0)) return 9;
else if(Input.GetKeyDown(KeyCode.Alpha1)) return 0;
else if(Input.GetKeyDown(KeyCode.Alpha2)) return 1;
else if(Input.GetKeyDown(KeyCode.Alpha3)) return 2;
else if(Input.GetKeyDown(KeyCode.Alpha4)) return 3;
else if(Input.GetKeyDown(KeyCode.Alpha5)) return 4;
else if(Input.GetKeyDown(KeyCode.Alpha6)) return 5;
else if(Input.GetKeyDown(KeyCode.Alpha7)) return 6;
else if(Input.GetKeyDown(KeyCode.Alpha8)) return 7;
else if(Input.GetKeyDown(KeyCode.Alpha9)) return 8;
else return -1;
}
}
If you want to actually be able to apply damage, we need a Target
script. See the simple example below for a script which enables this to occur. Later, within WeaponControl.cs
, we will check if the object we hit has this script attached, and if it does we can deal damage to the set HP amount given to the Target
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Target : MonoBehaviour
{
[SerializeField]
private float health;
[SerializeField]
[Tooltip("If this is true we spawn the broken GameObject on destruction")]
public bool isDestructable = false;
[SerializeField]
[Tooltip("The GameObject to spawn when this object is broken")]
public GameObject broken;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
public void TakeDamage(float amount)
{
health -= amount;
if (health <= 0)
{
if(isDestructable) Instantiate(broken, gameObject.transform, false);
Destroy(gameObject);
}
}
}
We could then use this PlayerInput.cs
script within a WeaponControl.cs
script check if the player presses this button in Update
called once per frame. If they have tried to shoot and have a weapon equipped, we can call the relative weapon's ShootWeapon()
function. Note that you will have to attach the playerInput
and playerWeapons
variables to relative scripts within your editor.
public class WeaponControl : MonoBehaviour
{
[SerializeField]
PlayerInput playerInput;
[SerializeField]
PlayerWeapons playerWeapons;
[SerializeField]
public Transform muzzle;
[SerializeField]
public GameObject projectile;
[SerializeField]
Camera mainCamera;
[SerializeField]
ParticleSystem muzzleFlash;
[SerializeField]
GameObject hitEffect;
private float range = 500.0f;
private float damage = 10.0f;
void Update()
{
if (playerInput.GetMouseFire() && playerWeapons.hasWeapon) ShootWeapon();
}
void ShootWeapon()
{
fireSFX.Play();
GameObject projectileObj = Instantiate(projectile, muzzle.transform.position + mainCamera.transform.forward, mainCamera.transform.rotation);
projectileObj.GetComponent<Rigidbody>().AddForce(transform.forward * 100);
RaycastHit hit;
if (Physics.Raycast(muzzle.transform.position, mainCamera.transform.forward, out hit, range))
{
Target temp = hit.transform.GetComponent<Target>();
if (temp != null) temp.TakeDamage(damage);
Debug.Log(hit.transform.name);
muzzleFlash.Play();
GameObject hitObject = Instantiate(hitEffect, hit.point, Quaternion.LookRotation(hit.normal));
Destroy(hitObject, 1f);
if (hit.rigidbody != null) hit.rigidbody.AddForce(-hit.normal * hitForce);
}
}
}
Scripting
Scripting in Unity uses C# and is very well documented. In the sections below, I'll Provide examples and edge cases where possible, and link to the relative documentation for quick reference.
For a collection of classes and structs that are required for Unity to function, which means they will always be available to you when scripting in Unity, head over to UnityEngine.CoreModule Documentation
Transform
This class controls and tracks object position, rotation, scale.
Official Transform Class Documentation
Local Space
Local space is the transform relative to the object's parent. An example of this can be seen below where I have selected an object and the transform controls are centralized on the exact transform of that object relative to its local position.
Take notice of three things in the above screenshot. First, we have selected Local
position in the top-left near our transform controls. Clicking this button again will toggle between Local and World space. Second, take note of the World Space axis shown at the top-right of the scene view. Third, in contrast to the World space axis, notice the GameObject's axis shown in the scene view are different in orientation. The transform axis shown on the GameObject are modifying and referring to the GameObject's transform within Local space.
World Space
World space is the position of the GameObject rooted within the scene. An example of this is seen in selecting the exact same object in the editor and toggling world space transform view. This makes sure the transform controls are the same as the World Space axis, instead of referring directly to the transform of a local object.
Again, take notice of three things in the above screenshot. First, we have selected Global
position in the top-left near our transform controls. Clicking this button again will toggle between Local and World space. Second, take note of the World Space axis shown at the top-right of the scene view. Third, in contrast to the World space axis, notice the GameObject's axis shown in the scene view are different in orientation. The transform axis shown on the GameObject are modifying and referring to the GameObject's transform within World space.
Vector3
Official Vector3 Struct Documentation
Axis
In Unity 3D you will use the X
, Y
, and Z
axis frequently both in positioning within the editor and scripting. It helps to have a clear understanding of the names these axis can be referred to with, as it will greatly improve your ability to access and modify these values without over complicating things.
The X
axis can be accessed with the right
keyword when accessing any class which stores axis information
The Y
axis can be accessed with the Up
keyword when accessing any class which stores axis information
The Z
axis can be accessed with the forward
keyword when accessing any class which stores axis information
Similarly, when modifying a Vector
, we can easily flip these axis by accessing the opposite of these keywords -
The X
negative axis can be accessed with the left
keyword when accessing any class which stores axis information
The Y
negative axis can be accessed with the down
keyword when accessing any class which stores axis information
The Z
negative axis can be accessed with the back
keyword when accessing any class which stores axis information
So, the Vector3
equivalents to these would be
X
axis, Vector3(1.0f, 0.0f, 0.0f)
, is equivalent to right
Y
axis, Vector3(0.0f, 1.0f, 0.0f)
, is equivalent to up
Z
axis, Vector3(0.0f, 0.0f, 1.0f)
, is equivalent to forward
X
negative axis, Vector3(-1.0f, 0.0f, 0.0f)
, is equivalent to left
Y
negative axis, Vector3(0.0f, -1.0f, 0.0f)
, is equivalent to down
Z
negative axis, Vector3(0.0f, 0.0f, -1.0f)
, is equivalent to back
Quaternion
Official Quaternion Struct Documentation
Shortcuts
Since Unity has many features and shortcuts available that will widen the gap between an experienced developer and a beginner, I'll list some of my most frequently used shortcuts and tricks here. Though these can all be viewed and modified by opening the panel below in Edit->Shortcuts...
, there is a huge amount of shortcuts and this can be a lot to look at.
Transform Controls
Official Positioning Documentation
At the top-left of your Unity editor, you'll notice the transform control buttons where you can switch between Hand
, Move
, Rotate
, Scale
, Rect
, and Universal
controls. Each of these can also be toggled by pressing Q
, W
, E
, R
, T
, and Y
, respectively.
Snapping to Collision
There will be many cases where you want to place an object on a table or ground within your scene. You should not need to manually fumble with axis to do this, but instead given that both objects have collision of some kind you can simply hold Shift+Ctrl
while using the Move
tool and dragging the grey box that appears in the center of the object, NOT the axis themselves. This will immediately snap the object to the collision nearest to your cursor as you drag it around the scene. There may be minor adjustments needed, but overall this should do the trick for most basic items.
Object to Scene View Transform
You will frequently want to move an object to the position and rotation of your current scene view in World space. You could manually drag the object across the scene in unity, adjusting each axis as needed. Alternatively, you can fly to a position near your desired location for the object, select the object, then press Shift-Ctrl-F
to move the object to your exact position and rotation. This is very useful when setting up cameras, as you can just fly to the view you want the camera to display, select the camera, and press Shift-Ctrl-F
to set it to that exact position with a lot less fumbling around.
Unclickable Objects
Tired of clicking in the scene view and selecting the terrain or some other GameObject? Within the scene hierarchy you can toggle whether or not an object should be clickable. Simply click the small hand next to the object's name in the hierarchy.
You can also toggle hiding and showing objects by clicking the eye icon just to the left of this setting
Prefabs
Since the Unity workflow is built around prefabs, I figured I'd document some specific use cases for the many features introduced in the Unity LTS 2019 release which added support for prefab variants and nested prefabs. On this page, I'll cover some good practices and features these prefab features provide.
I'd highly recommend heading over to devassets.com to grab some of the assets you see featured across the Unity pages on Knoats. They are entirely free and give you a lot to work with when learning. If you can afford it, I would recommend donating to the developers. Not only does this unlock more assets you can get with the package you donated to, but it shows support to the developer that organized all of these great assets in one place for you to learn with.
Positioning Prefabs
Being relatively new to Unity, I began by grabbing some assets off the Unity Store. Like most free assets on the store, these did not come entirely assembled for me and required me to work a bit to get things in a state that is usable for even the most basic games. This has been a good learning experience, and required no scripting, so if you are new to Unity and not quite ready to script, doing this will give you experience creating prefabs, working with materials, shaders, lighting, textures, and much more.
At first when creating a prefab of an object that exists within your scene, you may see something like the below when opening the prefab to edit
This is clearly not the orientation that we expect this SciFi_Rover
vehicle to have when initially placed in our scene. To fix this, be sure you are editing the prefab in the prefab editor and NOT directly within your scene. Then adjust the transform to be in the orientation desired.
Below, we see the initial transform settings
First, set all but the Scale
of your object to 0
. Shown in the screenshot below, there will be many cases that this does not produce the desired results, so we still need to modify the transform further
After making some adjustments, the object's final orientation within the prefab editor is seen below
And the final transform properties of the root GameObject are now much cleaner -
Post Processing
Good graphics are good. That's why I was excited to find adding Post Processing to my Unity 3D project was not only easy to do, but a huge improvement to the visuals within my scene. This enables common modern graphics features like Motion Blur, Ambient Occlusion, Depth of Field, and more.
Add Post Processing
Post Processing is added to each scene individually, and not a project as a whole. To add this to a Unity 3D project, we first need to add the Post-process Layer
component to our scene's main camera. Next we'll add a Post Processing Volume that globally effects our entire scene. Then we can add a PostProcessing_Profile
for our scene and add new visual effects accordingly.
Configuring the Camera
Its important that the camera the player views the game from contains this component. Otherwise, if the player can toggle between a camera which has the Post-process Layer
and one that does not, they effects gained by post processing will only be rendered in one view and not the other.
Once we've added the above component to the scene's main camera, we need to adjust the layer of both this component and our main camera to reflect this. Change to the Postprocessing
Layer in the Post-process Layer
component.
Now change the layer of the camera itself to the Postprocessing
Layer as well
Creating the Volume
Now anywhere within your current scene hierarchy, add an empty GameObject and name it PostProcessingVolume
or otherwise something relative to your specific scenario. Set this GameObject's layer to Post Processing
. Select this new GameObject and within the inspector Add Component->Post-processing Volume
as seen below
At a glance, there is not much here. But once we add a Post Process Profile and finish configuring our scene we will use this component to adjust some pretty neat looking visuals.
Be sure to apply the Post Processing
layer to the Camera and Volume GameObjects within your scene before continuing or the effects will not be applied
Creating the Profile
Within the GameObject created for our Volume click New
to the right of the Profile
field in the new Post-processing Volume
component.
Unity will automatically create a Post Processing Profile and place it in a directory relative to your scene. Now we can check the Global
tick box to apply this volume to our entire scene and start adding new effects to our scene!
An example of some effects that I added to my scene to achieve the screenshot at the top of this page
That's it! See the glow coming from the lights in the pictures below for an example of how this can be used to add the Bloom effect to an emissive light source.
New Input System
Setup
Using the default configuration for a keyboard&mouse / Gamepad Input Actions
asset in unity, we can implement universal controls given various input devices.
Issues getting an input device to work? Click Window->Analysis->Input Debugger
and at the top-left of the new window select Options->Add Devices Not Listed in 'Supported Devices'
and any input device that was not previously working should now be passing input to unity.
This was a weird bug for me to figure out, so I thought it was worth a mention. For me, I had to do this in order to get Unity to accept input from my Corsair gaming mouse. I searched up a lot of information on this new input system thinking I was using it wrong, and later found that my mouse was not passing input to unity and my code was correct.
Use
We can call a specific function from a specific component
Using PlayerInput component->Behavior->Invoke Unity Events -
Rigidbody playerRigidbody;
public PlayerControls controls;
Vector2 playerVelocity;
public float playerSpeed = 5.0f;
void Awake() {
playerRigidbody = GetComponent<Rigidbody>();
controls = new PlayerControls();
}
void OnEnable() {
controls.Player.Enable();
}
void OnDisable() {
controls.Player.Disable();
}
public void OnMove(InputAction.CallbackContext context) {
print("Moving: " + context.ReadValue<Vector2>());
playerVelocity = context.ReadValue<Vector2>();
}
public void OnLook(InputAction.CallbackContext context) {
print("Look: " + context.ReadValue<Vector2>());
}
public void OnFire(InputAction.CallbackContext context) {
print("Bang");
}
void Update()
{
playerRigidbody.position += new Vector3(playerVelocity.x * Time.deltaTime * playerSpeed,
0,
playerVelocity.y * Time.deltaTime * playerSpeed);
}
Or we can let Unity call functions defined using the naming convention void On\[ActionName\](InputValue value)
Using PlayerInput component->Behavior->Send Messages -
Rigidbody playerRigidbody;
public PlayerControls controls;
Vector2 playerVelocity;
public float playerSpeed = 5.0f;
void Awake() {
playerRigidbody = GetComponent<Rigidbody>();
controls = new PlayerControls();
}
void OnEnable() {
controls.Player.Enable();
}
void OnDisable() {
controls.Player.Disable();
}
public void OnMove(InputValue value) {
playerVelocity = value.Get<Vector2>();
print("Moving: " + value.Get<Vector2>());
}
public void OnMove(InputValue value) {
playerVelocity = value.Get<Vector2>();
print("Moving: " + value.Get<Vector2>());
}
void Update()
{
playerRigidbody.position += new Vector3(playerVelocity.x * Time.deltaTime * playerSpeed,
0,
playerVelocity.y * Time.deltaTime * playerSpeed);
}
Broadcast messages is the same as Send Messages, except broadcasting invokes the same methods on all child objects who have a component with function definitions that match this naming convention.