Reading Your First Unity Script
If youâre new to Unity and you have never coded before, opening a C# script (pronounced âC sharpâ) can be a little intimidating. Even a relatively small script can contain a lot of unfamiliar words, punctuation, and symbols.
The good news is that you donât need to understand all of C# before you can start reading Unity code. Once you learn a few basic concepts, youâll begin to recognize patterns that appear in script after script.
In this article, weâre going to walk through a simple player movement script. This is the kind of simple script you see in many beginner Unity tutorials.
Weâll look at what each line does, introduce some fundamental C# concepts, and explain how those concepts fit into Unity.
Weâre not going to turn this into a complete C# course. Instead, the goal is to help you understand what youâre looking at when you open a script in your own project.
The Script Weâre Working With
Hereâs the complete script weâll be discussing:
using UnityEngine;
using UnityEngine.InputSystem;
[RequireComponent(typeof(Rigidbody2D))]
public class PlayerMovement : MonoBehaviour
{
// Editor Fields
[SerializeField] private float moveSpeed = 5f;
// Internal Fields
private Vector2 moveInput;
private Rigidbody2D playerRigidbody2D;
// Unity Events
private void Awake()
{
playerRigidbody2D = GetComponent<Rigidbody2D>();
}
private void FixedUpdate()
{
playerRigidbody2D.linearVelocity = moveInput * moveSpeed;
}
// External Functions
public void OnMove(InputAction.CallbackContext context)
{
moveInput = context.ReadValue<Vector2>();
}
}
This script handles basic 2D player movement. It receives input from Unityâs Input System and uses a Rigidbody2D to move the player.
If youâve followed my 2D player movement tutorial, you may recognize this code. If not, donât worry. You donât need to build the project yourself to follow along.
Letâs start at the top.
1. Using Statements
The first two lines are:
using UnityEngine;
using UnityEngine.InputSystem;
These are called using directives.
In C#, related types and functionality are organized into groups called namespaces. You can think of a namespace as a way to organize code and distinguish between different codebases.
Unity provides a large collection of functionality, including physics, game objects, input handling, and much more. Rather than putting everything into one enormous collection, Unity organizes its code into namespaces.
The UnityEngine namespace contains many of the types youâll use when working with Unity. For example, MonoBehaviour, Rigidbody2D, and Vector2 all come from this namespace.
The UnityEngine.InputSystem namespace contains types associated with Unityâs Input System. Our script uses one of those types later when it receives player input.
The using directive tells C# which namespace we want to refer to without having to write its full name every time.
For example, because we have using UnityEngine;, we can write:
Rigidbody2D playerRigidbody2D;
Instead of:
UnityEngine.Rigidbody2D playerRigidbody2D;
Both refer to the same type.
As you develop your own projects, you will be creating your own namespaces and types. You can use using directives to make it easier to refer to those types. This is a common practice in C #, and it helps keep your code distinct from other codebases.
What does the semicolon mean?
Youâll notice that both lines end with a semicolon (;).
In C#, a semicolon marks the end of a statement. Itâs similar to a period at the end of a sentence, although the rules arenât quite the same.
In C# all statements end with a semicolon. Statements are lines of code that perform an action. Not every line of code needs a semicolon, you can write a statement over multiple lines, so whitespace is not a factor.
One important distinction: not everything in C# is a statement. We will point out those exceptions as we encounter them.
2. An Attribute: RequireComponent
Next, we have:
[RequireComponent(typeof(Rigidbody2D))]
This line introduces a feature of C# called an attribute.
Attributes provide additional information about a class, method, or other element of your code. Unity uses several attributes to influence how scripts behave or how they appear in the Editor.
In this case, RequireComponent tells Unity that our script requires a Rigidbody2D component.
Why is that useful?
Our movement script relies on a Rigidbody2D to move the player. Without that component, the script wouldnât have the physics body it needs.
By adding this attribute, we ask Unity to ensure that the required component is present when the script is added to a GameObject. This helps prevent setup mistakes.
Thereâs one detail worth knowing: Unity checks for missing required components when the script component is added. Adding this attribute to a script that is already attached to existing GameObjects doesnât automatically repair every existing instance.
The part inside the parentheses is also worth examining:
typeof(Rigidbody2D)
The typeof operator allows us to refer to a type itself. Here, weâre telling Unity which component type is required.
You donât need to memorize attribute syntax just yet. For now, recognize that square brackets around a declaration often indicate an attribute, and that Unity provides many useful attributes of its own.
3. Declaring a Class
Now we reach the main structure of our script:
public class PlayerMovement : MonoBehaviour
{
This is a class declaration.
A class is one of the fundamental building blocks of C#. It defines a type that can contain data and functionality.
Class declarations are not statements. They can contain statements, but they are not themselves statements.
Class declarations declare a new type in C#. The name that follows identifies the class. You can think of a class as a template for creating objects that share the same structure and behavior.
You enclose the details of a class with curly braces {}. Inside the curly braces you can have statements, variables, and functions which we will describe below.
In Unity, youâll frequently create classes that describe the behavior of a GameObject. For example, you might have a PlayerMovement class, an EnemyAI class, or a HealthSystem class.
Letâs break this declaration down.
public
The keyword public is an access modifier. It determines who can access a particular part of your code.
In this case, public means the class can be accessed from outside the class itself, subject to the usual C# accessibility rules.
Youâll encounter other access modifiers, particularly private, later in this script.
For now, remember that these keywords help control how different parts of a program interact with one another.
class
The class keyword tells C# that weâre declaring a class.
This is how you introduce a new class in C#. The name that follows identifies the class.
PlayerMovement
This is the name of our class.
Naming is important in programming because it helps communicate what the code is responsible for. PlayerMovement is a reasonable name because this class handles the movement of our player.
In Unity, the filename should match the name of the MonoBehaviour class you want to attach as a component. So this script should be saved as PlayerMovement.cs.
Youâll also notice that the class name uses capital letters at the beginning of each word. This is a common C# naming convention called PascalCase.
The variables weâll encounter later use a different convention, called camelCase, where the first word begins with a lowercase letter.
: MonoBehaviour
The colon introduces a relationship between our class and another class. Here, PlayerMovement inherits from MonoBehaviour.
You can think of inheritance as building on an existing class. Rather than implementing everything ourselves, we inherit functionality provided by the class weâre extending.
MonoBehaviour is a Unity class that allows our script to function as a component attached to a GameObject. It also provides access to Unityâs event methods, such as Awake and FixedUpdate.
This is what allows Unity to call those methods as the game runs.
Without this inheritance, our class would still be a valid C# class, but it wouldnât function as a normal Unity MonoBehaviour component.
The opening brace
Finally, we have:
{
The opening curly brace begins the body of the class.
Everything belonging to this class goes inside its braces, including fields and methods.
Later weâll encounter additional pairs of braces around methods. Braces are important because they define the boundaries of code blocks.
Our class ends with a matching closing brace at the bottom of the script.
4. Comments: Notes for Humans
Before we look at the variables, consider these lines:
// Editor Fields
And:
// Internal Fields
These are comments.
In C#, two forward slashes (//) begin a single-line comment. The compiler ignores everything after those slashes on the same line.
Comments are useful for explaining why something exists, grouping related code, or leaving notes for other developers, including your future self.
For example:
// The player's movement speed.
[SerializeField] private float moveSpeed = 5f;
The comment doesnât change how the movement works. It simply helps a person understand the code.
Youâll also encounter comments that span multiple lines, which use /* and */ to mark their beginning and end.
A good comment adds useful context rather than simply repeating what the code already says.
5. Fields: Storing Information
Our first field is:
[SerializeField] private float moveSpeed = 5f;
This is a great line to learn from because it introduces several important C# concepts at once.
A field is a variable declared inside a class. Fields allow an instance of that class to remember information.
In our case, we need to remember how quickly the player should move.
Letâs examine each part.
float
The keyword float specifies the type of data we want to store.
C# has different data types for different kinds of information. For example:
intstores whole numbers, such as10or-3.floatstores floating-point numbers, such as5.0or2.75.boolstores a Boolean value:trueorfalse.stringstores text.Vector2stores a pair of floating-point values, which weâll use for movement.
Choosing the appropriate type helps C# understand what kind of value a variable can hold.
For our movement speed, a float makes sense because we may want to use values such as 3.5 rather than restricting ourselves to whole numbers.
moveSpeed
This is the name of the field.
The name tells us what information it stores. We could technically choose a different name, but descriptive names make code much easier to understand.
Notice the lowercase-first letter. As mentioned earlier, camelCase is a common convention for field and variable names.
= 5f
The equal sign assigns a value to the field.
Weâre setting the initial movement speed to 5.
The f suffix indicates that 5 is a float literal. C# otherwise treats a number such as 5 as an integer literal by default.
This is a small detail, but youâll see it frequently in Unity code:
float jumpHeight = 8f;
float gravityScale = 1.5f;
float cooldown = 0.25f;
The f suffix isnât a special Unity feature. Itâs part of C#.
private
The private keyword is an access modifier.
It means that the field can be accessed directly only from within the class that declares it, subject to the normal rules of C#.
Why not make every field public?
Keeping fields private gives the class control over its internal data. Other scripts canât simply change a private field by accessing it directly.
This is useful for keeping code organized and reducing unintended changes.
But we have a problem: we want to adjust the playerâs speed in Unityâs Inspector.
Thatâs where the next part comes in.
[SerializeField]
SerializeField is a Unity attribute that tells Unity to serialize this field even though it is private.
For our purposes, that means Unity can display the field in the Inspector and preserve its value as part of the serialized component data.
This gives us a useful combination:
privatelimits direct access from other scripts.[SerializeField]allows Unity to expose the field in the Inspector.
You can select the player GameObject and change moveSpeed without needing to edit the source code every time you want to adjust the value.
This pattern is common in Unity projects because it gives developers convenient Editor controls without making every field publicly accessible.
6. Two More Fields
Next, we have:
private Vector2 moveInput;
private Rigidbody2D playerRigidbody2D;
These are also fields, but they serve different purposes.
Vector2 moveInput
The first field stores the movement input we receive from the Input System.
A Vector2 is a Unity type that represents two-dimensional data using an X value and a Y value.
For movement, we can interpret those values as horizontal and vertical direction.
For example:
| Input | Vector |
|---|---|
| Right | (1, 0) |
| Left | (-1, 0) |
| Up | (0, 1) |
| Down | (0, -1) |
| No input | (0, 0) |
When the player presses a movement key, weâll store the resulting vector in moveInput.
The movement code will then use that information to determine the playerâs velocity.
Notice that we havenât assigned an initial value to this field. Thatâs fine. A Vector2 is a value type, and its default value is (0, 0).
This means that, initially, our movement input represents no movement.
Rigidbody2D playerRigidbody2D
The second field stores a reference to a Rigidbody2D component.
Weâve already seen that Unity components provide functionality that can be attached to GameObjects. A Rigidbody2D provides access to the 2D physics body that will control our playerâs movement.
The type of this field is Rigidbody2D, and its name is playerRigidbody2D.
The field lets us remember which physics component we want to work with.
Like the previous field, it doesnât have an explicitly assigned initial value. A reference field of this kind defaults to null, meaning it doesnât initially refer to a component.
Weâll assign the reference in a moment.
Why do we declare these fields separately?
You might wonder why we donât just retrieve the component whenever we need it.
We could write code that looks up the component at the point of use. However, retrieving it once and storing the reference makes our intentions clear and avoids repeating the lookup.
This is a common pattern in Unity: get a reference to a component during initialization, then use that reference later.
7. Unityâs Awake Method
Now we reach our first method:
private void Awake()
{
playerRigidbody2D = GetComponent<Rigidbody2D>();
}
A method is a named block of code that performs an operation. Youâll also hear the term function used for this concept.
In Unity, methods are how we define many of the actions our scripts perform.
Letâs start with the declaration:
private void Awake()
private
Once again, private is an access modifier. Here, it means the method is not directly accessible from outside this class.
Unity can still call Awake because it recognizes this method as part of the MonoBehaviour lifecycle.
void
The void keyword tells C# that the method doesnât return a value.
Some methods calculate a result and return it to the code that called them. Others perform an action without returning anything.
Awake belongs to the second category in our script.
Awake()
Awake is a Unity lifecycle method.
Unity calls it when the script component is initialized under the appropriate conditions, making it a useful place to set up references and prepare our script for use.
The parentheses are part of the method declaration. They contain the methodâs parameters if it has any. Our Awake method doesnât need any parameters, so the parentheses are empty.
You donât call Awake yourself in this script. Unity does that for you.
The method body
Inside the braces, we have:
playerRigidbody2D = GetComponent<Rigidbody2D>();
This is where we retrieve the component reference.
GetComponent is a method provided by Unity. It searches for a component of the specified type on the same GameObject as the script.
The <Rigidbody2D> part tells C# what type of component we want returned. This is an example of C# generic syntax, which allows methods and types to work with specified types of data.
You donât need to understand generics in depth right now. Recognize that GetComponent<Rigidbody2D>() is the standard way to ask Unity for a Rigidbody2D component on the same GameObject.
The method returns a reference to the component if one is found.
We then use the equal sign to assign that reference to playerRigidbody2D.
From this point forward, our script can use playerRigidbody2D to interact with that component.
Notice how this connects to the earlier attribute:
[RequireComponent(typeof(Rigidbody2D))]
The attribute helps ensure that the component is present when the script is added. GetComponent<Rigidbody2D>() retrieves the component so our code can use it.
These two lines serve different purposes, but they work together.
8. FixedUpdate and Moving the Player
The next method is:
private void FixedUpdate()
{
playerRigidbody2D.linearVelocity = moveInput * moveSpeed;
}
FixedUpdate is another Unity lifecycle method.
Unlike Awake, which is used for initialization, FixedUpdate is called at regular intervals as part of Unityâs physics update loop.
Since weâre controlling a Rigidbody2D, this is a natural place to apply our movement logic.
Letâs look at the important statement inside it:
playerRigidbody2D.linearVelocity = moveInput * moveSpeed;
There are several things happening here.
Accessing a componentâs member
First, we have:
playerRigidbody2D.linearVelocity
The dot (.) lets us access a member of an object or type.
In this case, playerRigidbody2D is our reference to the physics component, and linearVelocity is a member of that component.
The linearVelocity property represents the rate at which the Rigidbody2D moves through the world. It uses a Vector2, so it has both horizontal and vertical components.
Unity 6 uses linearVelocity for this purpose.
Multiplying values
Next, we have:
moveInput * moveSpeed
The asterisk (*) is the multiplication operator.
Weâre multiplying our movement vector by the movement speed.
Suppose our input is:
moveInput = (1, 0)
And our movement speed is:
moveSpeed = 5f
Multiplying them gives us:
(5, 0)
This tells the Rigidbody2D to move horizontally at five world units per second, with no vertical velocity.
If the input is (0, 1), the resulting velocity is (0, 5), which moves the player upward.
If the input is (0, 0), the resulting velocity is (0, 0), so the script sets the playerâs velocity to zero.
The multiplication operator is a normal part of C#. Unityâs Vector2 type provides the behavior that allows this multiplication to work with a vector and a floating-point number.
Assignment with =
Finally, the equal sign assigns the calculated result to linearVelocity.
This is an assignment statement. Weâre telling the physics component what velocity to use.
Putting it all together, the line means:
Take the playerâs current movement input, scale it by the desired movement speed, and assign the result to the Rigidbody2Dâs velocity.
Thatâs the core of our movement logic.
Why not move the Transform directly?
Unity provides different ways to move objects. In this example, weâre using a Rigidbody2D because we want the player to participate in the 2D physics system.
Using its velocity allows physics and collisions to influence how the object moves.
This script is intentionally simple. A complete movement controller may need to account for acceleration, knockback, friction, external forces, or other systems that also affect velocity.
For now, weâre focusing on understanding the code in front of us.
9. The OnMove Method
Our final method is:
public void OnMove(InputAction.CallbackContext context)
{
moveInput = context.ReadValue<Vector2>();
}
Unlike Awake and FixedUpdate, this method isnât a Unity lifecycle method.
Instead, itâs a method that we connect to the Input System through the PlayerInput component.
In our setup, the Move action invokes this method when its input changes. The method receives information about that input and stores the resulting movement vector.
Letâs break it down.
public
The method is declared public, which makes it accessible from outside the class.
This is important for our setup because the PlayerInput component needs to be able to invoke the method through its Unity Events configuration.
Not every method needs to be public. We use the access modifier that makes sense for how the method will be used.
OnMove
This is the methodâs name.
The name describes its purpose: it handles movement input.
Unity doesnât automatically call every method just because it has a particular name. Here, the connection to PlayerInput is what makes the method run in response to the input action.
That distinction is important. Awake and FixedUpdate are recognized Unity lifecycle methods. OnMove is a method we have connected to an input event.
The parameter
Now look at the parentheses:
(InputAction.CallbackContext context)
This is a parameter declaration.
A parameter allows a method to receive information from the code that invokes it.
Here, InputAction.CallbackContext is the type, and context is the parameterâs name.
The type comes from the Input System namespace we imported at the beginning of the script.
The context contains information about the input action that triggered the callback, including the ability to read its current value.
Think of it this way: the Input System is calling our method and passing along information about what happened.
We donât need to create this context ourselves. It is supplied when the input callback is invoked.
Reading the input value
Inside the method, we have:
moveInput = context.ReadValue<Vector2>();
Weâve seen assignment before, so letâs focus on the right-hand side:
context.ReadValue<Vector2>()
ReadValue is a method provided by the Input System. The <Vector2> part specifies the type of value we want to read.
Weâre telling the Input System that we want the actionâs value represented as a Vector2.
This makes sense because our movement action is configured to provide two-dimensional directional input.
The returned value is assigned to moveInput, the field we declared earlier.
Notice that OnMove doesnât directly move the player. It simply updates the stored input.
The actual movement happens in FixedUpdate.
This separation gives each method a clear responsibility:
OnMovereceives and remembers the input.FixedUpdateuses that input to control the physics component.
The input system tells us what direction the player wants to move, and the physics update applies that movement.
10. Putting It All Together
Now that weâve walked through the script, letâs look at how the pieces work together.
The player presses a movement key. Unityâs Input System processes that input and produces a Vector2 representing the desired direction.
The PlayerInput component invokes OnMove, which reads the vector and stores it in moveInput.
During FixedUpdate, our script multiplies that vector by moveSpeed and assigns the result to the Rigidbody2Dâs linearVelocity.
The physics system then moves the player according to that velocity, while taking the physics simulation into account.
The overall flow looks like this:
Player presses a key
|
v
Unity Input System
|
v
PlayerInput invokes OnMove()
|
v
moveInput stores the direction
|
v
FixedUpdate()
|
v
Calculate and assign velocity
|
v
Rigidbody2D moves the player
Each part has a specific job. Once you understand that flow, the script becomes much easier to read.
11. A Few C# Concepts Worth Remembering
You donât need to memorize every detail of this script immediately. However, there are a few concepts worth recognizing as you encounter more Unity code.
Classes define types that can contain data and functionality. Our PlayerMovement class defines the behavior of a movement component.
Fields store information that belongs to a class instance. Our moveSpeed, moveInput, and playerRigidbody2D fields let the script remember values and references.
Methods are named blocks of code that perform operations. Our script contains Awake, FixedUpdate, and OnMove, each with a different responsibility.
Types tell C# what kind of data weâre working with. We use float for speed, Vector2 for directional input, and Rigidbody2D for a reference to the physics component.
Access modifiers such as public and private determine where classes, fields, and methods can be accessed.
Attributes such as RequireComponent and SerializeField provide additional information that Unity uses when working with our script.
Assignment and operators let us set values and calculate results. The = operator assigns a value, while * multiplies our input vector by the movement speed.
And finally, Unity lifecycle methods let Unity run specific parts of our code at appropriate times. Awake initializes our component reference, while FixedUpdate handles our physics-related movement.
These concepts appear throughout Unity projects. You donât have to master them all at once. The more scripts you read, the more familiar they will become.
Conclusion
Reading C# code for the first time can feel like learning a new language because, in a sense, thatâs exactly what youâre doing.
But you donât need to understand every feature of the language to make progress. Start by recognizing the structure of a script, identifying its fields and methods, and following how information moves through the code.
Our PlayerMovement script is a small example, but it demonstrates many of the patterns youâll encounter in Unity projects.
As you continue learning, youâll see these same concepts used to control enemies, manage health, handle menus, spawn objects, and build all kinds of gameplay systems.
In future articles, weâll build on these foundations and explore more of C# from a Unity developerâs perspective.
For now, the important thing is to get comfortable reading the code, asking what each part does, and understanding how those parts work together.
You donât have to know everything yet. You need a place to start.