Notes
Richard Gesick
The App Ext and Scripting Unity Reference Manual
1 | Introduction and Review of Unity . . . . . . . . | 2 |
1.1 | Installing Unity. . . . . . . . . . . . . . | 3 |
1.2 | 3 | |
1.3 | 4 | |
1.4 | 5 | |
1.5 | 6 | |
1.6 | 6 | |
1.7 | 6 | |
1.8 | 7 | |
1.9 | 7 | |
1.10 | 8 | |
1.11 | 10 | |
1.12 | 11 | |
1.13 | 15 | |
1.14 | 15 | |
1.15 | 16 | |
1.16 | 17 | |
2 | 24 | |
2.1 | 26 | |
2.2 | 29 | |
2.3 | 32 | |
2.3.1 | 34 | |
2.3.2 | 37 | |
2.3.3 | 39 | |
2.4 | 45 | |
2.4.1 | 45 | |
2.4.2 | 48 | |
2.5 | 53 | |
2.6 | 56 | |
3 | 58 | |
3.1 | 58 | |
3.1.1 | 58 | |
3.1.2 | 60 | |
3.2 | 67 | |
4 | 72 | |
4.1 | 72 | |
4.2 | 73 | |
4.3 | 74 | |
4.4 | 77 | |
5 | 80 | |
6 | 87 | |
7 | 99 | |
7.1 | 99 | |
7.2 | 100 | |
8 | 109 | |
8.1 | 109 | |
8.2 | 112 | |
9 | 115 | |
9.1 | 116 | |
9.2 | 120 | |
9.3 | 128 | |
9.3.1 | 128 | |
9.3.2 | 132 | |
9.4 | 141 | |
10 | 142 | |
11 | 151 | |
12 | Video/Cut Scenes . . . . . . . . . . . . . | 160 |
Table 1 table of contents
Introduction
This is a guide for working with Unity. The current version for this document is Unity 6000.0.26. As we progress, we will try to keep it updated. It's intended for the apps extension and scripting course. Unity is available from the the Unity web site. One of the things to be careful of as you work with Unity is that it updates 30 to 40 times a year. Now in most cases, prior projects will still work fine and they might have to be upgraded but there are going to be some cases where upgrading from a prior project to a new project will cause issues. For that reason once we start a project I'm going to suggest for our classes that we maintain one version of Unity for the entire semester.
When we start looking at Unity you'll find there are several versions of it available. The one that's suitable for us is the free personal edition that covers everything that we need to do. Once you get to the point where you're doing professional development or you've got your own independent studio then you can think about upgrading. I believe the agreement now on Unity is, once you get beyond $200,000 a year in income from Unity then you do have to have a paid plan.
Unity Review
The following few sections are a review of Unity. (link to code sample) It may go into more detail on some of the topics than you’ve seen in your other classes. This is the link to the review video.
Installation
To get started with Unity you're going to need to do a download. The download can be either using the download assistant or the hub. If you're going for a current version of Unity, you should be able to just pick it from the plan. However you're going to also want to take a look at going to the download archive if we're looking for a version that is not the most recent. When installing a Unity version, there are many options for additional modules, but for this class we only need to select the option for documentation. If you’re working on a Windows platform, then you may want Windows, Web GL, and Android build support. Mac users may want to add Web GL or IOS build support.
Creating a project
As we start looking at creating a new project, the easiest way is to launch the Unity Hub and select “New Project”. There will be several options to pick from, so you will first want to select the correct editor version. For our class, we will be using the 3D (Built-In Render Pipeline) template. That said, there are other templates like 2D Mobile, 3D Mobile, High Definition Render Pipeline (HDRP, and more depending on what type of project you’re making. For now, we’re using the built-in core option, also shown in the next image.
Give the project a name, select a storage location and then create the project. The asset packages available on initial start up are those that you have already downloaded. You can always add more later from the Asset Store.
Every new project in Unity starts out with a main camera and a directional light. Those will be displayed in the hierarchy. The Assets folder/partition will also have a subcategory labeled Packages. Do not delete the built in packages.
If you need to use the package manager or update packages in the project, the easiest way to find it is go to the top of the Unity editor, click on the window tab, then scroll down and select Package Manager from the dropdown. That’ll open a list of all the packages currently in your project. You may occasionally have to manually update a few of them, so it’s a good idea to check in there every now and then.
Camera and lights
Every new project has two components that are automatically included. One of those is a light and the other is a camera. The typical cameras that you might see are going to be first person, third person, follow with an offset and overhead. For cameras, there's lots of options that you might work with. First person, third person, follow with an offset and overhead just to name a few. There is the possibility of having multiple cameras in a scene. That's either by defining additional viewports for individual cameras or by adjusting the camera depth and the layers that it's going to use. We'll talk about those in greater detail later.
One of the things to be careful of when you're working with a Camera is to make sure they aren’t attached to GameObjects that might be destroyed during gameplay. This will cause the camera to also be destroyed and a “No Camera Rendering” message to appear on the screen. The easier way to take care of that issue to make the player inactive instead of destroying the player.
One of the things that gives your game a better feel is having more than one light. Think about having multiple lights just like on a stage during a play. You can have multiple lights and multiple colors. This allows the player to have a sense of depth and prevents the case where everything's going to be shadowed/blacked out from one direction and fully visible with all the details from another. It's going to give you some flexibility in the view and it's going to make it a more pleasurable game to look at.
Every light is a game object and as a game object it has a transform. You're going to assign it a position and a rotation. The rotation angles will have an effect on the display of the game. Remember if it's a directional light, they are essentially in a relation like the sun is to the earth; for all practical purposes the rays are all coming out parallel and light up a particular surface. Once you have the appropriate angle, you can put that light any place in your game and it'll still be lighted up that way. Another couple options are going to be using either a point light or using a spotlight. The spotlight is going to be more along the lines of what you might see from a flashlight and it's going to have a range and an angle/spread that it's going to illuminate items for us with those values.
Basic objects
In Unity, every object you add whether 2D or 3D will be a GameObject, and every GameObject will have a Transform component. The Transform contains three properties: Position, Rotation, and Scale. They look like they're all set up to be Vector3s. It is important to note that Rotation is actually a set of Euler angles from a quaternion. One of the nice things about our game objects is if we make a child object, that child inherits from the parent. If the parent moves, the child moves. If the parent rotates, the child rotates. One thing to be careful of is the child's position that is shown in the transform. That's actually an offset from the parent’s position. The child world position is only going to be the same as the child local position if the parent is at (0,0,0).
Unity Movement
- In movement, we're going to look at a couple different aspects. We're going to look at the use of rigid bodies. We're going to look at applying forces and applying velocities. We're going to look at some timing issues and we're going to look at key input.
Rigidbodies
In both 2D and 3D, a Rigidbody component allows for the capability for a GameObject to be affected by forces like gravity and by collisions with other Rigidbodies. A basic consideration is going to be when we add a rigid body component to an object, its motion is going to be under the control of the Unity physics engine, so even without adding any code to it, we're going to find that a rigidbody object will be pulled down by gravity and it will also react to collisions from objects or colliding objects if the right collider component is present.
The rigidbody has a API for scripting that lets us do things like apply force or set the velocity for an object. We can use linear velocities or radial velocities. Linear velocity controls the forward movement and Angular velocity controls the rotation movement. Those lets us control that rigidbody in a physically realistic way. When scripting for physics code, we should use the FixedUpdate function rather than the normal Update function. They are carried out in a measured time step that doesn't necessarily coincide with their frame update, so it gives you a consistent movement with physics. The reason for that is the fixed update is called immediately before each physics update so any change that is made there will be processed directly.
These first two code samples that we're going to see are both set up for fixed update. One showing using force and one showing a transform rigid body position or transform the position. Both use Unity’s legacy input system which is also known as input manager. The sample I'm going to do after these two are going to be using Unity's new input process which is known as the input system. We'll take a look at those now and see where we go from there.
Rigidbody API
Applying a force to a rigidbody
void FixedUpdate()
{
float hor = Input.GetAxis("Horizontal");
float ver = Input.GetAxis("Vertical");
Vector3 move = new Vector3(hor, 0.0f, ver);
GetComponent<Rigidbody>().AddForce(move * speed * Time.deltaTime);
We can use velocity instead of force but don’t use both. It will cause problems. Create the velocity vector, multiply it by speed and time and then add it to the rigidbody.position or transform.position
void FixedUpdate ()
{
float hor = 0;
float ver = 0;
if (Input.GetKey ("a"))
hor = -1;
if (Input.GetKey ("d"))
hor = 1;
if (Input.GetKey ("w"))
ver = 1;
if (Input.GetKey ("s"))
ver = -1;
Vector3 vel = new Vector3 (hor, 0, ver);
GetComponent<Rigidbody>().position+= vel * speed* Time.deltaTime;
//or GetComponent<Transform>().position+=vel*speed* Time.deltaTime;
}
Input System Review
The code section that's below is going to be a quick demo on how to use unity’s input system. To be able to use it, you must have the input system package imported into the project. In the player configuration, you need to make sure either the new input system or both are selected.
One consideration with the legacy input system is the input manager lets you map to individual keys and create additional axes. With the input system we have a initial setup that has a lot of movement controls configured for multiple items. While we can use the joystick, we can use the mouse, etc. This is not going to allow quite the level of customization without some additional work. We'll take a look at that aspect a little bit later.
To be able to use the input system, make sure that we have our input action and we need to assign that into our player component. Once we have it assigned to the player component, there's not a whole lot else you have to do with the configuration itself. The behavior will normally be set up to send messages. Those are calls to methods that we'll have to make sure that we have in our code. If you notice below, I have a OnMove method and an OnLook method. Those are the methods that are being called from player input. We start by taking the input values and mapping them to our movement and look variables: movex, movey, lookx, and looky. These values are then applied in the FixedUpdate method to ensure consistent behavior with the physics system. We make a new vector3 with movex and movey, multiplied by Time.deltaTime and apply it to the objects Transform.position. For my rotation while I could rotate in more than one axis, rotating about the Y axis is easy enough to control and doesn't affect us too much otherwise. we create a rotation using Quaternion.Euler using rotateY as the Y-axis and apply that to the Transform.rotation.
void FixedUpdate()
{
Vector3 move = new Vector3(movex, 0, movey);
tf.position += move * speed * Time.deltaTime;
rotateY += lookx;
tf.rotation = Quaternion.Euler(0, rotateY, 0);
}
private void OnMove(InputValue movementValue)
{
Vector2 movementVector =
movementValue.Get<Vector2>();
movex = movementVector.x;
movey = movementVector.y;
}
private void OnLook(InputValue lookValue)
{
Vector2 lookVector = lookValue.Get<Vector2>();
lookx = lookVector.x;
looky = lookVector.y;
}
Input Manager Review
Using input manager, we're going to look at a few of the control setups that you might want to utilize. When we use input get axis horizontal or input get axis vertical, those are defined in the input manager. The horizontal gives us movement with A&D or the left/right arrow while the vertical gives us the WS and the up/down arrow. Now if we want something more specific, we can do input get key and A or W or whatever the case may be. We're going to use input get key and we're going to also going to use Input.GetKeyDown.
- Input.GetKey returns true every frame that the key is held down, so if you have it held down for 60 frames it will return true for all 60 frames
- Input.GetKeyDown, it returns true the first frame and it doesn't return true again until you've released the key and pressed it again.
Input.GetKeyDown is for distinct one time actions and Input.GetKey for something that you want to have multiple occurrences of. Another thing to note on the difference between buttons/keys and thumb sticks/triggers. Buttons are digital meaning that they will generate a simple boolean value while the thumb sticks and triggers are analog meaning they will return a numeric value based on their location. GetAxis is going to return a numeric value of -1 if it's left and one if it's right or -1 if it's back or 1 if it's forward. The buttons/keys generate boolean responses. A trigger or thumb stick that isn’t moved/neutral position generates a value of 0.
The following is a brief summary of some of the key combinations that are possible using the keyboard and joysticks one we're using input manager.
- Input.GetAxis("Horizontal") is set by Unity as the a and d keys as well as the left and right arrows
- Input.GetAxis(“Vertical") is set by Unity as the w and s keys as well as the up and down arrows
- Input.GetKey returns true thru each frame as long as the key is down
- if (Input.GetKey ("a"))
- Input.GetKeyDown( string name) (or up) returns true just once, in the frame that it is pressed in and doesn’t generate a new value until it has been released and pressed
- if (Input.GetKeyDown ("d"))
Key naming
- Normal keys: “a”, “b”, “c” …
- Number keys: “1”, “2”, “3”, …
- Arrow keys: “up”, “down”, “left”, “right”
- Keypad keys: “[1]”, “[2]”, “[3]”, “[+]”, “[equals]”
- Modifier keys: “right shift”, “left shift”, “right ctrl”, “right alt”, “left alt”, “right cmd”, Mouse Buttons: “mouse 0”, “mouse 1”, “mouse 2”, …
- Joystick Buttons (from any joystick): “joystick button 0”, “joystick button 1”,
- Joystick Buttons (from a specific joystick): “joystick 1 button 0”, “joystick 1
- Special keys: “backspace”, “tab”, “return”, “escape”, “space”, “delete”, “enter”, “insert”, “home”, “end”, “page up”, “page down”
- Function keys: “f1”, “f2”, “f3”, …
- The names used to identify the keys are the same in the scripting interface and the Inspector.
Unity colliders and triggers
Colliders
- A collider is a component that can be added to a game object. Colliders can be 2D or 3D, depending on the type of game you're going to play. They define the shape of the object for purposes of collision only. One of the things that we want to be careful of is making sure we're not trying to make things too computationally intensive. In most of our cases we want to use the simpler colliders, the box, the sphere, or the capsule colliders rather than using the mesh collider. Note: that unless you're viewing in the editor the colliders are invisible.
- If you're doing a game in 2D, you can use the Box Collider 2D and Circle Collider 2D. Any number of these can be added to a single object to create compound colliders. With careful positioning and sizing, compound colliders can often approximate the shape of an object quite well while keeping a low processor overhead.
There are some cases, where compound colliders are not accurate enough. In 3D, you can use Mesh Colliders to match the shape of the object’s mesh exactly. These colliders are much more processor-intensive than primitive types, however, so use them sparingly to maintain good performance.
Colliders can be added to an object without a Rigidbody component to create floors, walls and other motionless elements of a scene. These are referred to as static colliders. Colliders on an object that does have a Rigidbody are known as dynamic colliders. Static colliders can interact with dynamic colliders but since they don’t have a Rigidbody, they will not move in response to collisions.
Triggers
- The scripting system can detect when collisions occur and initiate actions using the OnCollisionEnter function. You can also use the physics engine simply to detect when one collider enters the space of another without creating a collision.
- A collider configured as a Trigger (using the Is Trigger property) does not behave as a solid object and will simply allow other colliders to pass through. When a collider enters its space, a trigger will call the OnTriggerEnter function on the trigger object’s scripts
Collisions
- OnCollisionEnter(Collision col) and OnTriggerEnter(Collider col)
- When collisions occur, the physics engine calls functions with specific names on any scripts attached to the objects involved. You can place any code you like in these functions to respond to the collision event. Note: Collisions give a Collision parameter, Triggers give a Collider parameter
- Below is a sample of an OnTriggerEnter method. It tests to see which object it collided with and which object it was attached to in order to determine what actions to perform.
private void OnTriggerEnter(Collider other)
{
if (this.tag=="Finish" && other.tag == "Player")
{
go.SetActive(false);
if(go2!=null)
go2.SetActive(false); true;
}
if (this.tag=="Respawn" && other.tag=="Player")
{
go.transform.position = startPos;
go.GetComponent<Rigidbody>().Sleep();
}
. . .
}
Destroying game objects
The first instinct of many programmers is, when a bullet hits an enemy or an enemy hits a player, to destroy the object or objects. You need to carefully consider that event. Why? Consider a camera that is a child of a player. What happens when the player is destroyed? In some cases it is better to use player.SetActive(false);
You also need to be careful on what you destroy. This line of code in a script could have a different effect than intended.
Destroy(this);
That line will actually cause the script component to be destroyed, not the game object it is attached to. The destroy function removes a game object, component or asset.
Unity Terrain, Trees and NavMesh
Unity’s terrain system (link to code sample) allows you to add vast landscapes to your games. At runtime, terrain is highly optimized for rendering efficiency while in the editor, a selection of tools is available to make terrains easy and quick to create. A Terrain GameObject adds a large flat plane to your scene and you can use the Terrain’s Inspector window to create a detailed landscape.
Creating and Editing Terrains
To add a Terrain GameObject to your Scene, select GameObject > 3D Object > Terrain from the menu. This also adds a corresponding Terrain Asset to the Project view. When you do this, the landscape is initially a large, flat plane. The Terrain’s Inspector window provides a number of tools you can use to create detailed landscape features.
Tools
- The paint brush in the terrain toolbar is used to raise and lower the terrain height, paint the terrain texture and paint trees. Trees are solid 3D GameObjects that grow from the surface. Unity uses optimizations, like billboarding for distant Trees, to maintain good rendering performance. This means that you can have dense forests with thousands of trees and still keep an acceptable frame rate. Unity has several tree types in Standard Assets and there are other free packages available.
Grass and Other Details
A terrain can have grass clumps and other small objects such as rocks covering its surface. Grass is rendered by using 2D images to represent the individual clumps while other details are generated from standard meshes
Navigation System in Unity
- The Navigation System (link to code sample) allows you to create characters which can navigate the game world. It gives your characters the ability to understand that they need to take stairs to reach second floor, or to jump to get over a ditch. The Unity NavMesh system consists of the following pieces:
NavMesh
NavMesh Agent
Off Mesh Link
NavMesh Obstacle
Navigation Mesh and agent
NavMesh surface
NavMesh surface is a structure which describes the walkable surfaces of the game world and allows the agent to find path from one walkable location to another in the game world. The data structure is built, or baked, from your level geometry.
NavMesh Agent component helps you to create characters which avoid each other while moving towards their goal. Agents reason about the game world using the NavMesh and they know how to avoid each other as well as moving obstacles.
Off-Mesh Link component allows you to incorporate navigation shortcuts which cannot be represented using a walkable surface. For example, jumping over a ditch or a fence, or opening a door before walking through it, can be all described as Off-mesh links.
NavMesh Obstacle component allows you to describe moving obstacles the agents should avoid while navigating the world. A barrel or a crate controlled by the physics system is a good example of an obstacle. While the obstacle is moving the agents do their best to avoid it, but once the obstacle becomes stationary it will carve a hole in the NavMesh so that the agents can change their paths to steer around it. If the stationary obstacle is blocking the path way, the agents can try to find a different route.
Building a NavMesh
The process of creating a NavMesh from the level geometry is called NavMesh Baking. The process collects the Render Meshes and Terrains of all Game Objects which are marked as NavigationStatic, and then processes them to create a navigation mesh that approximates the walkable surfaces of the level.
NavMesh generation is handled from the Navigation window (menu: Window > AI > Navigation).
Building a NavMesh for your scene can be done in 4 quick steps:
1.Select scene geometry that should affect the navigation – walkable surfaces and obstacles.
2.Check Navigation Static on to include selected objects in the NavMesh baking process.
3. Back in the Navigation window, adjust the bake settings to match your agent size. Agent Radius defines how close the agent center can get to a wall or a ledge.
Agent Height defines how low the spaces are that the agent can reach.
Max Slope defines how steep the ramps are that the agent walk up.
Step Height defines how high obstructions are that the agent can step on.
4. In the NavMesh surface, click bake to build the NavMesh. The resulting NavMesh will be shown in the scene as a blue overlay on the underlying level geometry whenever the Navigation Window is open and visible.
Creating a NavMesh Agent
Create a cylinder: GameObject > 3D Object > Cylinder.
The default cylinder dimensions (height 2 and radius 0.5) are good for a humanoid shaped agent, so we will leave them as they are.
Add a NavMesh Agent component: Component > Navigation > NavMesh Agent.
Now you have a simple NavMesh Agent set up ready to receive commands. When you start to experiment with a NavMesh Agent, you most likely are going to adjust its dimensions for your character size and speed.
The NavMesh Agent component handles both the pathfinding and the movement control of a character. In your scripts, navigation can be as simple as setting the desired destination point - the NavMesh Agent can handle everything from there on.
Unity Components
- Components are the nuts & bolts of objects and behaviors in a game. They are the functional pieces of every GameObject. GameObject is a container for many different Components. By default, all GameObjects automatically have a transform component. Use the Inspector panel to see which components are attached to the selected GameObject. As components are added and removed, the Inspector will always show you which ones are currently attached.
One of the great aspects of components is flexibility. When you attach a component to a GameObject, there are different values or properties in the component that can be adjusted in the editor while building a game, or by scripts when running the game. components can include references to any other type of component, GameObjects, or assets.
A child object inherits the transform properties of the parent game object. As such, its position is now a local coordinate or an offset from the parent, rather than a world coordinate. When using an empty game object as a “grouping object”, set its transform position to (0,0,0). Then the child objects local and world coordinates will be the same.
Unity treats the components as a bucket of elements. A game object can be treated as any of its individual components. Thus GameObjects that have a Rigidbody component could be cast as rigidbodies and stored in a collection of rigidbodies. This will be handy when we want to use polymorphism. A script that is added to a game object is also a component.
Unity Scenes
Using multiple scenes in a game In your scripts, scenes are controlled thru the UnityEngine.SceneManagement library so you need to include it as a using
statement. Adding scenes to a Unity game is easy. After saving your current scene, just select File > New Scene and start its development. To be able to switch scenes during game play, the scenes must be in the Scene list in build profiles. (File > Build Profiles)
Under build profiles, the first item in the list is scene list. Your scenes must be listed there, as shown.
Notes about scenes in build: Each scene has a name and an index number and can be called either way. While numbers are easy to sequence, I recommend you use names. If a scene
is deleted, the remaining indices adjust for the missing scene but it does not get refactored in your code. Once you have your scene list set, you can then create a build profile for whichever supported system you want. You can have multiple build profiles, they are stored as assets so they can be accessed and shared.
Grey Boxing
Grey boxing is a process used by game developers to quickly get a sense of a level’s layout and playability without wasting too much time. Grey boxing is, as the name suggests, a process of blocking out all the features in your map *before you get too hung up on architecture and other pretty things* with simple geometric shapes, which usually take the form of grey boxes (though they don’t have to be grey! And they don’t have to all be boxes!)
It serves a couple of main purposes:
- Allows the designer to see if the level is actually playable. i.e. to scale? Does the player fit through doors and hallways? Does the player know where they’re going? Then the designer can quickly and easily make changes.
- It provides a blueprint of the map and all its assets which the art team can start working on. For example they have a reference for how big a house needs to be, the general shape, where all the are doors etc., the collision fields, etc.
- If there are any programming elements that need testing, i.e. weaponry, power ups, player jump distances etc., this is the time to do so because the level can easily be adapted to either provide challenge to the player or at the very least be possible to navigate.
Camera As we look at the camera we should see that it has a transform just like any other game object. It has a position so we can position it where we want it or it can follow the player. It has a rotation so we can adjust the XYZ angle. The skybox, under the clear flags, gives us a reasonable background but if you want something different, you can hit the drop down and you can see options. For solid color, where you can actually pick the color of your background projection. We have two options with projection. We can either have perspective or orthographic. Most of your time you're going to want to leave it in perspective. The field of view has an axis that is going to be set up for either horizontal or vertical. In most cases you'll want to leave vertical field of view. The typical field of view for the game is going to be about 60° but you're going to see that you have a slider that allows us to adjust it and ultimately we're going to get to the point where we're going to be adjusting our field of view programmatically. The clipping planes determine what is going to be in the field of view. There are 2 clipping planes, one is at .3 that's the near plane and the other is at the far plane set at 1000. If something is less than.3 meters from the camera it's going to be deemed too close and not rendered and, by the same token, if it's more than 1000 meters away from the camera, it’s going to be deemed too far and it's also not going to be rendered. That's done for us automatically. There were a lot of game engines in the past where you had to do your own culling.
The viewport provides us an option to have multiple camera perspectives or camera shots in one scene. If we have multiple players one of the options is to allow each player to have its own camera. To set that up, we can duplicate our cameras and then we can go through and identify the particular view ports for each camera. When you look at the viewport description, it starts off with a X & Y for position and a W & H for width and height. The X is the lower left hand corner X and the Y is the lower left hand corner Y. (0,0) is the lower left hand corner of the screen. The width and the height are values between zero and one so if I set 00 for X & Y and then .5 for width and .5 for height, then we have a viewport that's going to be the lower left quarter of the screen. That could be the view for one player. I could make a second camera and provide it with its own viewport. To do that I can just duplicate the camera and then for the viewport rectangle I would change the X to .5 the Y to .5 the width to .5 and the height to .5. Then I would have a configuration that's going to look like this.
Another item that we need to consider with respect to the camera is the parameter that's labeled depth. The depth is going to determine the order of rendering with the lowest number rendered first and then the next lowest, etc. For example the main camera is at -1 and the new camera is at 0. The original camera -1 will be rendered first and then the camera with the depth of 0 will be rendered on top of it. The higher the number on the depth the higher it's going to be on the view.
Lighting
Lights are an essential part of every scene. Meshes and textures define the shape and look of a scene, but the lights define the color and mood of your environment. You’ll normally have more than one light in each scene. Making them work together requires a little practice. Lights can be added to your scene from the GameObject->Light menu. You can manipulate it like any other GameObject. Additionally, you can add a Light Component to any GameObject.
There are many different options within the Light Component. You can give an entirely different mood to the scene by changing the light color. Direct light is light that is emitted, hits a surface once, and is then reflected directly into a sensor (your retina or a camera). Indirect light is all other light that is ultimately reflected into a sensor, including light that hits surfaces several times, and sky light. To achieve realistic lighting results, you need to have both direct and indirect light.
You can use the Type property to choose how the Light behaves. The available values are:
- Point Light, a Light that’s located at a point in the Scene and emits light in all directions equally
- Spot Light, a Light that’s located at a point in the Scene and emits light in a cone shape
- Directional Light, a Light that’s located infinitely far away and emits light in one direction only
- Area Light, a Light that’s defined by a rectangle or disc in the Scene, and emits light in all directions uniformly across its surface area but only from one side of the rectangle or disc
Point Lights
Point lights are useful for simulating lamps and other local sources of light in a scene. A Point Light is located at a point in space and sends light out in all directions equally. The direction of light hitting a surface is the line from the point of contact back to the center of the light object. The intensity diminishes with distance from the light, reaching zero at a specified range. Light intensity uses the ‘inverse square law’.
Spot Lights
Spot lights are generally used for artificial light sources such as flashlights, car headlights and searchlights. A Spot Light has a specified location and range over which the light falls off. A Spot Light is constrained to an angle, resulting in a cone-shaped region of illumination. The center of the cone points in the forward (Z) direction of the light object. Light also diminishes at the edges of a Spot Light’s cone. Widening the angle increases the width of the cone and with it increases the size of this fade, known as the ‘penumbra’
Directional Lights
Directional Lights are useful for creating effects such as sunlight in your scenes. Behaving in many ways like the sun, directional lights can be thought of as distant light sources which exist infinitely far away. A Directional Light doesn’t have any identifiable source position and so the light object can be placed anywhere in the scene. All objects in the scene are illuminated as if the light is always from the same direction. The distance of the light from the target object isn’t defined and so the light doesn’t diminish.
Directional lights represent large, distant sources that come from a position outside the range of the game world.
Shadows
Shadows add a degree of depth and realism to a scene because they bring out the scale and position of objects that might otherwise look flat. In Unity, lights can cast shadows from a GameObject onto other parts of itself, or onto nearby game objects. There are 3 shadow settings, hard, soft, and none. Hard Shadows is a shadow property that produces shadows with a clear, well-defined edge. Hard shadows are not particularly realistic compared to Soft Shadows but they involve less processing, and are acceptable for many purposes. Soft Shadows is a shadow property that produces shadows with a soft edge. Soft shadows are more realistic compared to hard shadows and tend to reduce the “blocky” aliasing effect from the shadow map, but they require more processing.
GameObject
Unity’s GameObject class represents any element that can exist in a Scene. GameObjects are the essential elements for scenes in Unity and serve as a container for components that determine appearance and behavior of the GameObject.
The GameObject class provides a collection of methods which allow you to work with GameObjects in your code. These include methods for finding, making connections, and sending messages between GameObjects, adding or removing components attached to the GameObject, and setting values relating to their status within the scene.
Tags provide a way of marking and identifying types of GameObject in your scene and Layers provide a similar but distinct way of including or excluding groups of GameObjects from certain built-in actions, such as rendering or physics collisions.
You can modify tag and layer values via script using the GameObject.tag and GameObject.Layer properties. You can also check a GameObject’s tag by using .Equals method, ==, or by using the CompareTag method.
Accessing components
The simplest case is when a script on a GameObject needs to access another Component attached to the same GameObject. The first step is to get a reference to the Component you want to access. Use the GetComponent method. The following code uses the rigidbody
void Start ()
{
Rigidbody rb = GetComponent<Rigidbody>();
}
or
void Start ()
{
Rigidbody rb = GetComponent<Rigidbody>();
// Change the mass of the object's Rigidbody.
rb.mass = 10f;
}
You can also call methods on the Component reference:
void Start()
{
Rigidbody rb = GetComponent<Rigidbody>();
// Add a force to the Rigidbody.
rb.AddForce(Vector3.up * 10f);
Accessing components on other GameObjects
Link to GameObjects with variables in the Inspector. The most straightforward way to find a related GameObject is to add a public GameObject variable to the script:
public class Player : MonoBehaviour
{
public GameObject buddy;
// . . .
}
This variable will be visible in the Inspector, as a GameObject field. You can now drag an object from the scene or Hierarchy panel onto this variable to assign it. The GetComponent function and Component access variables are available for this object as with any other, so you can use code like the following:
public class Player : MonoBehaviour
{
public GameObject buddy;
void Start() {
// Start the buddy 2 units in behind of the player.
buddy.transform.position=player.transform.position + Vector3.forward * -2f;
}
}
Additionally, if you declare a public variable of a Component type in your script, you can drag any GameObject that has that Component attached onto it. This accesses the Component directly rather than the GameObject itself.
public Transform playerTransform;
Linking objects together with variables is most useful when you are dealing with individual objects that have permanent connections. It’s often convenient to locate objects at runtime and Unity provides two basic ways to do this, as described below.
Find GameObjects by Name or Tag
It’s always possible to locate GameObjects anywhere in the Scene hierarchy as long as you have some information to identify them. Individual objects can be retrieved by name using the GameObject.Find function:
GameObject player;
void Start()
{
player = GameObject.Find("Hero");
}
An object or a collection of objects can also be located by their tag using the GameObject.FindWithTag and GameObject.FindGameObjectsWithTag methods.
For example, in a game with one player, and multiple inventory objects in the world (each tagged “item”):
GameObject player;
GameObject[] items;
void Start()
{
player = GameObject.FindWithTag("Player");
items= GameObject.FindGameObjectsWithTag("item");
}
Create and destroy GameObjects
You can create and destroy GameObjects while your project is running. In Unity, a GameObject can be created using the Instantiate method which makes a new copy of an existing object, usually from the prefab or Resources folders.
The Destroy method destroys an object after the frame update has finished or optionally after a short time delay:
Note that the Destroy function can destroy individual components and not affect the GameObject itself.
void OnCollisionEnter(Collision otherObj)
{
if (otherObj.gameObject.tag == "Enemy")
{
Destroy(otherObj, 0.5f);
}
}
A common mistake is to write this:
Destroy(this);
and assume it destroys the GameObject the script is attached to. This represents the script, and not the GameObject. It will actually just destroy the script component that calls it and leave the GameObject intact but with the script component removed.
This GameObject is called “Sphere” and it's currently active in the scene. Let’s look at its components, which are the parts that define what the object does. Every GameObject has a Transform. This includes a Vector3 for Position, Rotation (which looks like X, Y, Z but is stored as a quaternion using Euler angles), and a Vector3 for Scale (how big it is on each axis). This sphere also has a Mesh Filter and a Mesh Renderer, which give it its visual shape and make it visible. If you want to hide the object without deleting it, you can turn off the Mesh Renderer. It also has a Sphere Collider where since the “Is Trigger” box is unchecked, it will respond to physics collisions. Finally, it has a Rigidbody so it can move with physics, and a script is attached, but it’s currently turned off.
2. Interactive Input
Now we're going to address input into the game. Unity has two different input processes that allows the player to interact with the game. It has its legacy system which is known as the input manager and it has a newer system which is known as the input system. We're going to talk about those for the little while and then we're going to also look at a couple different options on creating the interface to be able to interact with those.
We are going to dive into creating controls for a standard USB game controller. or we could consider it a Xbox controller or a PS5 controller or a switch controller. That could be an Xbox controller, a PlayStation controller, a Switch controller, or something similar. We will look at how to work with those kinds of devices, and we will also cover keyboard controls. Specifically, we will explore how to check which input devices are connected. After that, we will move on to two more key areas. First, we will learn how to customize control layouts so that players can switch between different profiles. Then we will go into how to remap individual buttons or keys using a custom graphical interface.
We will develop three basic configurations. One will be with IMGUI which is the Immediate GUI which uses the OnGUI method and is programmatically developed The other two are going to use the canvas and the UGUI system and those will be either calling functions where the buttons are being controlled with the GUI or assigning event listeners through code.
One of the first things that we need to do is when we're making any game is to figure out what actions we want the player to be able to make. Then we want to figure out how we want to control them and what devices do we want to just have. Do we want to limit it to keyboard controls or do you want to have mouse controls or do you want to have a USB controller? All of those are options, so I've got a small table down below that shows some of the things we might consider.
Mapping the desired actions and controls
Action Keyboard/mouse USB Controller
- Movement W A S D keys Left thumbstick
- Rotate camera Mouse Right thumbstick
- Quick items 1 2 3 4 keys Directional pad
- Inventory The I key The A button
- Pause game The Esc key The Start button
- Attack/use an item left mouse button The left trigger
- Aim right mouse button The right trigger
- Jump space The X button
The following images are going to show three different USB controllers one is an Xbox 360, another is a Xbox One and then finally the third is PS 4 controller. Any of these controllers will work with the game, it's just a matter of making sure we have the proper matching of the axes for the controls. If you look at the 360, you can see that those axes are just a little bit different from those for the Xbox One and they are also different than the PS4. We just need to make sure that you have a correct/current map before we go into figuring out the actual control configurations we're going to use. We're going to start off using Unity's input manager and we'll look at that one in just a second.
Checking the Xbox 360 Controller inputs
Input Manager Processing
To get started, access your input manager (link to code sample) (input manager video Part 1) by navigating to the Edit menu, hovering over Project Settings, and clicking on Input. Open up the Axes dropdown by clicking on the arrow next to it. When you look at the input manager you can see that unity starts with 18 axes, predefined for you. We can take a look and see we have horizontal and vertical and fire1 and fire 2 and another horizontal and another vertical. We'll take a look at those in just a second. For example, we're looking at the horizontal, we can see its name is horizontal with the capital H, it has a negative button being left, a positive button being right. Those represent the left and right arrow. It has alternate negative button being A and alternate positive button being D. Those are the A&D keys on the keyboard, for moving left and right. So that first horizontal maps the left arrow and right arrow, the A and the D keys. Then you can see it's identified as a key or mouse
button and it's for the X axis.
Looking down below that you can see fire 1 is also accessible. It's positive button is the left control, it's alternate positive button is mouse. For those of you have forgotten, mouse0 is the left mouse button and mouse1 is the right mouse button and again, it's a key or mouse button and it's X axis.
Earlier, you saw how we used Input.GetAxis when working with movement, and now we are applying a similar idea to actions like firing. Instead of using Input.GetKey or Input.GetKeyDown, we are now using Input.GetButton or Input.GetButtonDown. This allows the Input Manager to handle the setup of both axes and buttons, giving us a flexible way to define the inputs used in our games.
Customizing Controls
We’re going to discuss how to customize controls in input manager. We’ll look at how to set up controls for the USB controllers.
Adding controller inputs
To change the number of control inputs we're going to go up to axes and size and we're going to change it from 18 to 21. That will add three more options at the bottom of the list and they'll all duplicate the last control, giving me 3 new cancel buttons, for a total of four.
Making a button
Next pick one of the cancel buttons and we're going to change the name. you can actually make the name anything you want but let's try to keep it something related to a controller so I'm going to make the name USB-A. USB-A is going to represent the A button on the controller which is joystick button 0 so our changes would look something like this
- Change the value of the Name parameter to USB-A.
- Change the value of the Positive Button parameter to joystick button 0.
Creating this button doesn’t initially have any significant effect since we can access the control directly by calling joystick button 0. In fact we have 3 different ways to check for the button being pressed.
if (Input.GetButtonDown("USB-A"))
print("usb a");
if (Input.GetKeyDown(KeyCode.JoystickButton0))
print("button 0");
if (Input.GetKeyDown("joystick button 0"))
print("button 0 again");
Adding a new Axis
We’re going to add a new axis that will work similar to the horizontal axis. This will be on the right stick, which in an Xbox One, is the 4th axis. The axis is an analog type response in that the return value is a float in the range of -1 (full left) to +1 (full right). The button commands return a boolean value of true or false.
- Change the value of the Name parameter to USB-AxisX.
- Change the value of the Sensitivity parameter to as necessary.
- Change the dead zone value to .2
- Uncheck the Invert parameter.
- Change the value of the Type parameter to Joystick Axis.
- Change the value of the Axis parameter to 4th Axis (Joysticks).
Note: Something to keep in mind is that some controllers have the axes on the right stick switched. For example, the Xbox One controller diagram shows the horizontal axis as 5 and the vertical axis as 4. I have three different controllers myself. Two of them follow that setup, but one has it the other way around, with horizontal as 4 and vertical as 5. So it is always a good idea to check how your specific controller maps its inputs.
We can now access it in code as: hor = Input.GetAxis("USB-AxisX");
Working with the joysticks and axis can make a game more enjoyable but there are issues that arise with controllers that are out of adjustment. You may find the player starts drifting when there hasn’t been any input. There are 2 easy solutions for this situation. In the input manager, you can adjust the dead zone and or sensitivity settings to allow for a larger null area on the joystick. You can also adjust it programmatically. Consider the following:
hor = Input.GetAxis("USB-AxisX");
if (Mathf.Abs(hor) < .5f)
hor = 0;
Adding trigger inputs
Triggers are also analog devices but return a value of 0 to 1. Just as we did with the adding an axis, we’ll do the same process with the trigger. This is going to configure the right trigger on an Xbox controller.
- Change the value of the Name parameter to USB-RTrigger.
- Change the value of the Sensitivity parameter to 2.
- Change the dead zone to .2
- Change the value of the Type parameter to Joystick Axis.
- Change the value of the Axis parameter to 10th Axis (Joysticks).
We can now access it in code as:
if(Input.GetAxis("USB-RTrigger")> 0.1)
print("right trigger");
You should consider using a timer or cool down if you are using the trigger to fire projectiles, otherwise you may see many more than you expect.
Adding directional pad inputs
For this one we will make the vertical axis of the D-pad.
- Change the Name parameter to USB-DpadVert.
- Change the value of the Sensitivity parameter to 1.
- Change the Type parameter to Joystick Axis.
- Change the Axis parameter to 7th Axis (Joysticks).
We can now access it in code as:
if (Input.GetAxis("USB-DpadVert") >.1 )
print("dpad vertup");
if (Input.GetAxis("USB-DpadVert") < -.1)
print("dpad down");
For the horizontal directional pad input, you can follow the exact same steps as we did for the vertical directional pad input; just change the value of Name to something suitable for the horizontal D-pad and change the value of Axis to 6th Axis (Joysticks).
Adding PC control inputs
- Most of our PC control inputs are already integrated into the input manager; all that is left are the number keys, I key, and Esc key.
- You can actually follow the same steps as at the beginning of this chapter, when we added the USB controller buttons. For the number keys you'll want to change each of their names to num1, num2, num3, and num4. As for their positive button parameters, change their names to 1, 2, 3, and 4, accordingly.
Input System
The Input System allows you to control your game (link to a code sample) (input system video part 1) using a device, touch, or gestures. This Input System package is a more flexible system which allows you to use any kind of Input Device. It's intended to be a replacement for Unity's classic Input Manager. A video overview of the this section, the basic configuration of the input system is available in my code and video library or from this link.
The input system is imported into most current projects but if it isn’t there, we just need to go to the package manager and select it to be installed. As part of the installation, you will be asked if you want the tutorials to be included in the installation. If this is your first time working with the input system, I would encourage you to explore some of the packages, time permitting. You should not include all of the tutorials in your submission copy of your project.
We’ll talk about key rebinding extensively later but you may want to review that tutorial for reference. The others as you see fit.
Now that the input system is installed, we need to check the active input settings in the Project Settings -> Player. You will be given the option to set the configuration to allow only the input manager (the classic system), to allow only the input system (the newer system) or to allow both. You can see in the image that I selected the both option.
Next we want to add the input system to our player. For the component you want to add it to, select add component. From the menu, select Input and then from the next menu select player input. Now the action map is attached to the player and is almost ready for use. Items to notice include the default map is set to player and the behavior is send message. The input system detects when one of the controls has been triggered/activated and sends the message to the parent object for processing. If the method the message refers to is found, then that method executes. If the method isn’t found, rather than throwing an exception, the message is ignored.
The component must be on the same GameObject if you are using Send Messages, or on the same or any child GameObject if you are using Broadcast Messages.
When you double click on the input system actions in the asset folder, the input actions editor open so you can see the various action maps and actions. The default action maps that are provided are the Player and the UI action maps. We will be focusing on the player map. The default actions are shown as move, look, attack, interact, crouch, jump, previous, next and sprint. Looking at the basic design, move and look are Action types have a vector 2 as a return value. The other actions function as buttons.
Clicking on move and then WASD. WASD opens it so we can see the individual key mappings and the binding properties. WASD is a composite binding property that is a vector 2. The 2D vector Composite has four part Bindings. Up represents (0,1) (Y+), down represents (0,-1) (Y-), left represents (-1,0)(X-) and right represents (1,0) (X+).
The mode function determines whether to treat the inputs as digital or as analog controls.
- If this is set to Mode.DigitalNormalized, inputs are treated as buttons (off if below default Button Press Point and on if equal to or greater). Each input is 0 or 1 depending on whether the button is pressed or not. The vector resulting from the up/down/left/right parts is normalized. The result is a diamond-shaped 2D input range.
- If this is set to Mode Digital, the behavior is essentially the same as Mode.DigitalNormalized except that the resulting vector is not normalized.
- Finally, if this is set to Mode Analog, inputs are treated as analog (i.e. full floating-point values) and, other than down and left being inverted, values will be passed through as is.
- The default is Mode.DigitalNormalized.
There are 2 controls for up, W and Up arrow, 2 for down, 2 for left and 2 for right. In this basic configuration, we have the option to rename actions. Later we will discuss adding more discrete actions and control bindings. Shown in this image, I have changed the default action of Attack to Fire. That is done by right clicking on Attack, selecting Rename and providing the new value. Then when the changes are saved, the send message list is also changed for you. OnAttack() Is now OnFire(). Then if you change your mind, you can reverse the process and rename it attack. We do need to ensure that we have an OnFire() in our script.
Here the player input is configured with the default scheme of keyboard and mouse is using send messages which have been described above. The other 2 processes available are invoke unity or C# events. Invoking a unity event requires a separate Unity Event for each individual type of message. When this is selected, the events available on the PlayerInput are accessible from the Events foldout. The argument received by events triggered for Actions is the same as the one received by started, performed, and canceled callbacks. The C# process is similar to Invoke Unity Events, except that the events are plain C# events available on the Player Input API. You cannot configure these from the Inspector. Instead, you have to register callbacks for the events in your scripts.
We’re going to utilize the send message process here. In this block of code, we have fixed update processing the results of the move and look. OnMove, OnLook and OnAttack are called by the messages from input actions.
void FixedUpdate()
{
Vector3 move = new Vector3(movex, 0, movey);
tf.position += move * speed * Time.deltaTime;
rotateY += lookx;
tf.rotation = Quaternion.Euler(0, rotateY, 0);
}
private void OnMove(InputValue movementValue)
{
Vector2 movementVector = movementValue.Get<Vector2>();
movex = movementVector.x;
movey = movementVector.y;
}
private void OnLook(InputValue lookValue)
{
Vector2 lookVector = lookValue.Get<Vector2>();
lookx = lookVector.x;
looky = lookVector.y;
}
private void OnAttack(InputValue fireValue)
{
GameObject bull = Instantiate(Resources.Load("Bullet"),
shotSpawn.position, shotSpawn.rotation) as GameObject;
}
The OnMove generates values for movex and movey while OnLook generates values for lookx and looky. Those are used in fixed update to move the player in the x and z axes, while the lookx is assigned to rotateY, to rotate the player about the Y axis only. OnAttack is called directly from the input actions, so we generate a projectile every time the mouse is pressed. Notice the bullet is loaded from resources so the game doesn’t need an additional reference to the game object. It also used Destroy(bull, 3) to destroy the bullet after 3 seconds.
Modifying the Input during game play
Unity doesn't allow us to edit the input properties while in-game, we will add a function to our script that will allow the player to switch between control schemes. We’ll explore doing it with the input manager and the input system and we will use the IMGUI system and 2 variations of the UGUI system.
In this section we're going to cover creating control configurations using the input manager. We're going to break it down into three separate areas. We've already seen how to configure the axes for the input manager and we're going to make use of those and we're going to set up our keyboard controls. Our first profile will have a Xbox/USB controller and a keyboard profile that will be individually selectable. This is not a complete project because it's not going to have any capability to remap the keys yet.
Control Configuration 1 (link to the code sample)
The first block of code that we have is going to be for detecting controller. As we've seen, different controllers are going to have different mappings and you may need to have the flexibility to have two or three different controllers being able to be used for your game. You may need to be able to distinguish between them so again we can use this block of code identify the particular controller type we have.
void DetectController()
{
try
{
if (Input.GetJoystickNames()[0] != null)
{
isControllerConnected = true;
IdentifyController();
}
}
catch
{
isControllerConnected = false;
}
}
void IdentifyController()
{
Controller = Input.GetJoystickNames()[0];
}
This function uses the GetJoystickNames function of the input, which gets and returns an array of strings, which consists of the names of the connected gamepads. The reason for using a try-catch is because the Input.GetJoystickNames() will generate an IndexOutOfRange-Exception if there is no controller connected to the system.
The next box of code shows an OnGUI method from the IMGUI Class. The simple GUI function is going to do two things for us. It is going to show us the key that's being pressed and 2nd it's going to allow us to display the status of our controller, whether the USB controller is active or inactive. When we look at the GUI buttons, remember that they take a bounding rectangle with the top left X top, left Y, width and height, and a string for the text to display. Then I have the second GUI button, that's encompassed inside the if statement. That's going to execute that code if the button has been pressed. Again the rectangle is set up with top left X, top left Y, width and height and now I've added in the string controller plus another string called button display. The first thing that the GUI does, when it's pressed, is change the status of the USB variable. If it's false it changes to true and if it's true, it changes to false. If USB is true, we then make the button display active while if it's false we make it inactive so we always get to see our status of our controller.
void OnGUI()
{
GUI.Button(new Rect(25, 25, 125, 30), KeyDisplay);
if (GUI.Button(new Rect(25, 60, 125, 30), "Cntlr " + buttonDisplay))
{
USB = !USB;
if (USB)
buttonDisplay = "Active";
else
buttonDisplay = "Inactive";
}
}
This block of code with the for each loop is not required for the proper functioning of the game but it does give us a chance to see which key is being pressed at any one time. It does that by going through the system enumerations and gets the value of the key codes. It checks to see if the key has been pressed. If it has, it assigns the string to the key display. When the key has been released, the key display goes to empty string.
foreach (KeyCode vKey in System.Enum.GetValues(typeof(KeyCode)))
{
if (Input.GetKeyDown(vKey))
{
KeyDisplay = vKey.ToString();
}
else if (Input.GetKeyUp(vKey))
{
KeyDisplay = "";
}
}
The following code block is a partial view of our controller configuration. Now there are a couple options to look at from the start. First, with the if statement, I have it set up to check for a USB device. But if you are looking for a specific type of controller, you can check for the controller's name directly. For example, you could use something like Controller.Equals("Controller (Xbox One For Windows)") to target that specific device. In this setup, I am also using the project settings from the Input Manager, where I have defined USB AxisX and USB AxisY. While the buttons on the controller return true and false, the axes are going to return a value from-1 to 1 and when we get to the triggers they're going to return a value from 0 to 1. When we look at USB AxisX value less than 0 that means it's been pushed to the left. I'm going to treat it as if anything to the left of 0 is of full negative, so I'm using the value -1 and to make this movement smooth I'm going to use Time.deltaTime. Now if the value it is greater than 0 on the USB-AxisX then I'm going to use 1.0 times Time.deltaTime and add it to CubeX. I'll do the same thing for the Y axis, adjusting my CubeY.
//USB axis x and y are right stick
if (USB)
//if (Controller.Equals("Controller (Xbox One For Windows)"))
{
if (Input.GetAxis("USB-AxisX") < 0)
{
CubeX -= 1.0f * Time.deltaTime;
}
else if (Input.GetAxis("USB-AxisX") > 0)
{
CubeX += 1.0f * Time.deltaTime;
}
if (Input.GetAxis("USB-AxisY") < 0)
{
CubeY -= 1.0f * Time.deltaTime;
}
else if (Input.GetAxis("USB-AxisY") > 0)
{
CubeY += 1.0f * Time.deltaTime;
}
In my controller profile I have also set up my D-pad and I've got some joystick buttons that I'm going to be able to use. For the D-pad I again defined that in the input manager by creating another axis and I have it USB-DpadHorz and USB-DpadVert. In this extracted sample, if the horizontal value is less than 0 then I'm going to rotate using Vector3.left. There is also a block of code using two different button commands, the joystick button 3 is accessed the same way that we did our keyboard. By putting Input.GetKeyDown and then the name in this case joystick button 3, or we could use the key code and the key code for it is KeyCode.Joystick1Button3.
if (Input.GetAxis("USB-DpadHorz") < 0)
{
this.transform.Rotate(Vector3.left * 1.2f);
}
if (Input.GetKeyDown("joystick button 3"))
{
this.transform.Rotate(Vector3.forward * 10.0f);
}
if (Input.GetKeyDown(KeyCode.Joystick1Button3))
{
this.transform.Rotate(Vector3.forward * 10.0f);
}
This block of code is addressing the set up of the keyboard components. After we've finished all of our USB controls, we will now come to the keyboard. I'm using key codes because ultimately we're going to take this code and change it so that we're able to change the keys while the game’s in play. You should be familiar with the Input.GetKey and Input.GetKeyDown but, just a reminder, GetKey returns true every frame as long as it's been pressed and GetKeyDown returns true only the first time it's been pressed. If we're looking for smooth movement, we may want to use GetKey while if we're looking for discrete movements or maybe firing we want to get GetKeyDown. After the Input.GetKey(KeyCode.A), I jumped down to the mouse axis. I don't show the complete Mouse X and Mouse Y configuration. The Mouse X < 0 will cause rotation using the vector3 left and Mouse Y is used to rotate vertically. After the other statements have been processed, the next step is to update our position using the new values for CubeX, CubeY and CubeZ. The final call to detect controller will allow us to always detect whether we have controller connected, so that'll be checked to every game frame.
else{
// if (Input.GetKeyDown(KeyCode.A))
{
CubeX -= 1.0f * Time.deltaTime;
}
if (Input.GetAxis("Mouse X") < 0)
{
this.transform.Rotate(Vector3.left * 1.2f);
}
if (Input.GetAxis("Mouse Y") > 0)
{
this.transform.Rotate(Vector3.forward * 1.2f);
}
}
this.transform.position = new Vector3(CubeX,
CubeY, CubeZ);
DetectController();
The finished product, using control configuration one, has our simple cube controlled with either the keyboard or the USB controller. In the next section we're going to look at developing an additional profile.
Control Configuration 2, discrete controller profiles (Input manager video part 2)
Here we will create a selectable keyboard profile so we're going to either use WASD or IJKL from the keyboard as well as the USB. Our key codes, rather than being hard coded to KeyCode.A, etc., will be assigned as variables. The keycode strings will initially store the values WASD but later will either hold WASD or IJKL as determined by the active profile. The KeyCode UpKeyCode and associated lines use the strings W/A/S/D rather than the variable names because a field initializer cannot reference a non-static field, method or property. Once we get to the update keycodes, we will see that they will work with the variables correctly.
string UpKeyCode_string = "W", DownKeyCode_string = "S",
LeftKeyCode_string = "A", RightKeyCode_string = "D";
KeyCode UpKeyCode =
(KeyCode)System.Enum.Parse(typeof(KeyCode), "W");
KeyCode DownKeyCode =
(KeyCode)System.Enum.Parse(typeof(KeyCode), "S");
KeyCode LeftKeyCode =
(KeyCode)System.Enum.Parse(typeof(KeyCode), "A");
KeyCode RightKeyCode =
(KeyCode)System.Enum.Parse(typeof(KeyCode), "D");
Inside of Update, we add the code for changing the profiles. This code that I have created uses a 2 key combination to determine if we will use profile 1 with WASD or profile 2 with IJKL.
if (Input.GetKey(KeyCode.LeftShift) && Input.GetKey(KeyCode.Alpha1))
{
SwitchControlProfile("1");
}
else if (Input.GetKey(KeyCode.LeftShift) && Input.GetKey(KeyCode.Alpha2))
{
SwitchControlProfile("2");
}
The controls for using the controller haven’t changed but we will look at the keyboard commands next. They are using the key codes with variable names rather than being hard coded with a keyboard control. In this segment I used Input.GetKeyDown and values of + 1 to adjust the cube rather than using Time.deltaTime and smoother movements with Input.GetKey.
if (Input.GetKeyDown(LeftKeyCode))
{
CubeX -= 1.0f;
}
if (Input.GetKeyDown(RightKeyCode))
{
CubeX += 1.0f;
}
To change the profiles, the switch control profile method is called which causes the appropriate values to be assigned to the key code strings. If it is case 1, we’ll use WASD and case 2 will use IJKL.
void SwitchControlProfile(string Scheme)
{
switch (Scheme)
{
case "1":
UpKeyCode_string = "W";
DownKeyCode_string = "S";
LeftKeyCode_string = "A";
RightKeyCode_string = "D";
break;
case "2":
UpKeyCode_string = "I";
DownKeyCode_string = "K";
LeftKeyCode_string = "J";
RightKeyCode_string = "L";
break;
}
KeyCodeUpdate();
Once that assignment has been made, we will call the key code update method to assign the key code strings to the respective key codes.
void KeyCodeUpdate()
{
UpKeyCode = (KeyCode)System.Enum.Parse(typeof(KeyCode), UpKeyCode_string);
DownKeyCode = (KeyCode)System.Enum.Parse(typeof(KeyCode), DownKeyCode_string);
LeftKeyCode = (KeyCode)System.Enum.Parse(typeof(KeyCode), LeftKeyCode_string);
RightKeyCode=(KeyCode)System.Enum.Parse(typeof(KeyCode),RightKeyCode_string);
}
We now have the capability to have our USB controller or one of 2 keyboard profile active, giving the user a slight bit of flexibility in their control schemes. Next we’ll expand on this to create the ability to remap/rebind keys and customize the input processes.
Control Configuration 3 (Input manager video part 3 )
As we look at developing the rebinding capability for control configuration 3, we’ll have to add another set of variables for the keys, so we’ll now have the active set of keys and 2 more sets for the other profiles.
string UpKeyCode_string = "W", DownKeyCode_string = "S",
LeftKeyCode_string = "A", RightKeyCode_string = "D";
string UpKeyCode_string1 = "W", DownKeyCode_string1 = "S",
LeftKeyCode_string1 = "A", RightKeyCode_string1 = "D";
string UpKeyCode_string2 = "I", DownKeyCode_string2 = "K",
LeftKeyCode_string2 = "J", RightKeyCode_string2 = "L ";
After creating the variables, we will still assign the default values as we did in configuration 2. Then we will add a couple more variables to allow for controlling the key change process. We will need variables for the key to change, one to determine if we are in the key changing process and one more to confirm that we’ve got the new input key.
string keyToChange = "null";
bool keyChanging = false;
bool getInputKey = false;
Inside the update method, we will add a line above the control input so that we don’t accidentally intermix movements and key selection. The block continues with the keyboard input just as it does in configuration 2.
if (!keyChanging)
{
if (USB)
{
if (Input.GetAxis("USB-AxisX") < 0)
As we get to the profile switching method, we will not be using WASD or IJKL in the profile assignments but the string1 or string 2 variables associated with each profile.
void SwitchControlProfile(string Scheme)
{
switch (Scheme)
{
case "1":
UpKeyCode_string = UpKeyCode_string1;
DownKeyCode_string = DownKeyCode_string1;
LeftKeyCode_string = LeftKeyCode_string1;
RightKeyCode_string = RightKeyCode_string1;
break;
case "2":
UpKeyCode_string = UpKeyCode_string2;
. . .} }
As you can see in this image, we have a cube with the GUI buttons in the top left corner but now we also have a large GUI rectangle on the screen with some more buttons. The GUI display only displays 2 buttons in each profile but their actions are representative of all of the control buttons. We’ll start with describing the UGUI functionality and then ultimately get to the actual change key code method.
void OnGUI()
{
GUI.BeginGroup(new Rect(Screen.width/2-300, Screen.height/2 - 300, 600, 800));
GUI.Box(new Rect(0, 100, 600, 400), menuName + " (" + currentProfile + ")");
if (GUI.Button(new Rect(150, 140, 135, 20), "Profile 1"))
{
SwitchControlProfile("1");
currentProfile = "Profile 1";
}
if (GUI.Button(new Rect(325, 140, 135, 20), "Profile 2"))
. . .
}
The OnGUI begins by making a group. When you begin a group, the coordinate system for GUI controls are set so (0,0) is the top-left corner of the group. All controls are clipped to the group. Groups can be nested and children are clipped to their parents. This is very useful when moving a set of GUI elements around on screen. A common use case is designing your menus to fit on a specific screen size, then centering the GUI on larger displays. You can see the top left corner is defined by using the screen center and then applying an offset.
There are labels for up and down and then the 4 buttons follow the same pattern. If the button is pressed, the booleans keyChanging and getInputKey are changed to true, the keyToChange is identified ( up1 in the sample) and the inputKeyCode_string is set to “ “.
GUI.Label(new Rect(25, 175, 125, 20), "Keyboard Up ");
if (GUI.Button(new Rect(150, 175, 135, 20), UpKeyCode_string1))
{
keyChanging = true;
getInputKey = true;
keyToChange = "up1";
inputKeyCode_string = " ";
}
Notice that the coordinates for the rectangles for the labels and buttons are not world coordinates but are offsets from the top left corner of the group.
The last button on the display is the save button that will reconfirm the active profile, call keyCodeUpdate() and change keyChanging to false, ending this process. You do need to remember to include the GUI.EndGroup().
if (GUI.Button(new Rect(220, 250, 135, 20), "Save"))
{
if (currentProfile.Equals("Profile 1"))
{
SwitchControlProfile("1");
}
else if (currentProfile.Equals("Profile 2"))
{
SwitchControlProfile("2");
}
KeyCodeUpdate();
keyChanging = false;
}
GUI.EndGroup();
We have one final task to complete and we will have a process to allow us to rebind keys in Unity while a build of the game is executing. Now we need to look at the actual key change process.
The first step of the process is to traverse the set of key values to see if we have a match between the system values and the key that has been pressed. Then, as long as getInputKey is true, we will then that value to the inputKeyCode_string and change getInputKey to false. Then it is just a matter of stepping thru the if statements to find a match for which key to change. Once we find a match, we assign the inputKeyCode_string to the appropriate value.
void ChangeKeyCode()
{
foreach (KeyCode vKey in
System.Enum.GetValues(typeof(KeyCode)))
{
if (Input.GetKeyDown(vKey) && getInputKey)
{
inputKeyCode_string = vKey.ToString();
getInputKey = false;
}
}
if (keyToChange.Equals("up1"))
{
UpKeyCode_string1 = inputKeyCode_string;
}
else if (keyToChange.Equals("up2"))
{
UpKeyCode_string2 = inputKeyCode_string;
}
else if (keyToChange.Equals("down1"))
{
DownKeyCode_string1 = inputKeyCode_string;
}
else if (keyToChange.Equals("down2"))
{
DownKeyCode_string2 = inputKeyCode_string;
}
}
The completed code sample for this available on my web page in the Config_IM project.
There are several options to consider when creating the key rebinding process. Rather than having the large lists of string variables, consider the possibility of implementing them as a 2D array. I’ve configured this one so that each column is for a profile/device and each element in the row is consistent with the type of movement.
//2d array for holding active and potential key code strings
//each row holds all of one key set, each column all the keys for a configuration
//forward, backward, left, right rotate l, rotate r
//col 0 is the active set being used
//string[,] keyString = new string[6, 4];
// Q, U, E and O are used for rotation in this demo rather than the mouse.
// move devices or profile would result in more columns while more
//control movements would result in more rows.
static string[,] keyString = { {"W","W","I"},
{"S","S","K"},
{"A","A","J"},
{ "D","D","L"},
{ "Q","Q","U"},
{ "E","E","O"},
};
//Note that the variable values in the array can be used for
// initialization of the key codes b/c the array is static.
KeyCode UpKeyCode =
(KeyCode)System.Enum.Parse(typeof(KeyCode), keyString[0, 0]);
KeyCode DownKeyCode =
(KeyCode)System.Enum.Parse(typeof(KeyCode),keyString[1, 0]);
KeyCode LeftKeyCode =
(KeyCode)System.Enum.Parse(typeof(KeyCode),keyString[2, 0]);
KeyCode RightKeyCode =
(KeyCode)System.Enum.Parse(typeof(KeyCode), keyString[3,0]);
KeyCode RotRightKeyCode =
(KeyCode)System.Enum.Parse(typeof(KeyCode), keyString[4,0]);
KeyCode RotLeftKeyCode =
(KeyCode)System.Enum.Parse(typeof(KeyCode), keyString[5, 0]);
Then in the switch profile method, our code uses the arrays for assignment.
keyString[0, 0] = keyString[0, 1];
keyString[1, 0] = keyString[1, 1];
The GUI button uses them as part of the label.
if (GUI.Button(new Rect(150, 175, 135, 20), keyString[0, 1]))
The completed code sample for this project is available in the code samples, in the input demo OnGUI project.
Input System Discrete Configuration (link to the code sample)
In this section we're going to talk about using the input system and methods to create selectable profiles without doing remapping yet. (Input system video part 1) Looking at the image on the left, you can see I've opened up the action map and I'm looking at the player action. I have now gone through and created a series of discrete control so I'm going to have move1with WASD move 2 with TGFH, move3 using the up down left and right buttons on the controller. Those can be individually designed by you or you can do it the easier way by clicking on them, selecting duplicate and renaming the process and the keys to match your requirements. In the Unity project, I still have the player input designed to send messages so we're going to now take a look at some of the code that I’ve put together to make this work with the different control profiles
I have a simple movement class that has the basic transforms. It has the movex, movey, lookx, looky and I've added one additional feature, the enumerations. The enumerations are used to ensure I know which set of controls is being utilized. Using booleans instead of the enumerations is possible but it will require significantly more coding to turn booleans on and off appropriately. The enumerations give us a easy way to control it and it's easy to remember because we can give them names that are relevant.
private Transform tf;
private float movex, movey, lookx, looky;
public float speed = 5;
private float rotateY = 0;
private float rotateX = 0;
enum Config { WASD, TFGH, CNTLR};
private Config config=Config.WASD;
The next block of code has the fixed update method and the first movement method. FixedUpdate has nothing spectacular but you do want to make sure that you notice that we're using movex and movey as the variables that are the X & Z components for the vector3. Then we're just adding it to our transform’s position. We're modifying our rotate variables with lookx and looky, and then our rotation is changed through the Quaternion.Euler method. The input system is configured with player actions to send messages. When the system detects a movement for WASD it's going to use the on move method called OnMove1 and send it the message to activate and execute that method. Then we're going to check to make sure it is WASD. Then we derive the movement vector from the movement value and assign the individual components of X & Y to our movex and movey. Then the next time fixed update, runs those new control values are used and we get either a change in movement or a change in rotation.
void FixedUpdate()
{
Vector3 move = new Vector3(movex, 0, movey);
tf.position += move * speed * Time.deltaTime;
rotateY += lookx;
rotateX+= looky;
tf.rotation =
Quaternion.Euler(0, rotateY, rotateX);
}
private void OnMove1(InputValue movementValue)
{
if (config == Config.WASD)
{
Vector2 movementVector =
movementValue.Get<Vector2>();
movex = movementVector.x;
movey = movementVector.y;
}
}
One of the reasons we need to be able to identify which set of controls is being activated is because of the way the input system and the action map is going to react. you can see below I've got my OnMove2 method. Part of the issue is because the OnMove1 and OnMove1 methods both interact with the same set of variables movex and movey. Without a way to distinguish which one was applied, we would, in effect, be back to the original action map. There would be multiple sets of controls based on move concurrently, rather than have a discreet set of controls being utilized at any one point in time.
private void OnMove2(InputValue movementValue)
{
if (config == Config.TFGH)
{
Vector2 movementVector =
movementValue.Get<Vector2>();
movex = movementVector.x;
movey = movementVector.y;
}
}
These two are sample methods of an OnLook method and an OnFire method. The OnLook method uses the controller and the OnFire method is using anything except the controller but the other systems will work identically.
private void OnLook3(InputValue lookValue)
{
if (config == Config.CNTLR)
{
Vector2 lookVector =
lookValue.Get<Vector2>();
lookx = lookVector.x*5;
looky = lookVector.y*5;
}
}
private void OnFire(InputValue fireValue)
{
if ((config != Config.CNTLR))
{
GameObject bull =
Instantiate(Resources.Load("Bullet"),
shotSpawn.position, shotSpawn.rotation)
as GameObject;
Destroy(bull, 3);
}
}
This is a screen shot of this portion of the project and you should be able to see the 3 buttons for selecting the active control profile. The code for the GUI buttons wasn’t shown but pressing the button sets the value of the enumeration.
if (GUI.Button(new Rect(20, 20, 50, 20), "WASD"))
config = Config.WASD;
This is a link to the video covering this section of the input system.
2.5 Swapping control schemes in Input System
Changing control schemes using the input system, so each is a discrete control entity, requires a few new features. (Input system video part 2). While we will have enums to use as simple control features, we will also have to consider creating InputActionAssets, InputActions and rebinding events. The Unity Rebinding UI is a good resource. I also had K. Markley work with me to put this together.
enum Config { WASD, TFGH, CNTLR };
private Config config = Config.WASD;
enum Swap { WASD, TFGH, CNTLR, NONE };
private Swap swap = Swap.NONE;
enum ActiveStick { LEFT, RIGHT };
private ActiveStick activeStick = ActiveStick.LEFT;
I carried over the enums for config and added enums for swap and active stick.
The action map has been extended with look3 and look4, adding the ability to select and rebind the sticks on the controllers.
To be able to effectively control the rebinding operations, we need to declare some additional variables for those actions.
public InputActionAsset InputActions;
private InputAction myMoveAction, myRotateAction, myMoveAction2,
myMoveAction3, myLookAction2, cntlrLookAction3,
cntlrLookAction4, cntlrLookAction, fireAction;
private InputActionRebindingExtensions.RebindingOperation
rebindOperation;
private InteractiveRebindEvent rebindStopEvent;
private InteractiveRebindEvent rebindStartEvent;
The input action asset is the Unity action asset that is created when you import the input system. It will usually be the Player Input component that is on game object. We will assign it to our script by dragging it into the field labelled input actions. (Input systems video part 3)
The input action variables are assigned for the individual actions from the action map when we get to the start method. We do that by finding the action map named player and then within that map, finding the action named for the action we desire. In the following sample, you can see that we have assigned values to 2 of the action variables. The others follow the same pattern.
void Start()
{
myMoveAction =
InputActions.FindActionMap("Player").FindAction("Move1");
cntlrLookAction4 =
InputActions.FindActionMap("Player").FindAction("Look4");
Most of the move and look functions haven’t changed from the previous example but we have added a look3 and a look4 to be able to have discrete use of the control sticks for rotation. Notice the use of the enum for config and active stick. It makes it easier to track than a number sequence or a set of boolean variables.
private void OnLook4(InputValue lookValue)
{
if (config == Config.CNTLR &&
activeStick == ActiveStick.RIGHT)
{
Vector2 lookVector = lookValue.Get<Vector2>();
lookx = lookVector.x * 5;
looky = lookVector.y * 5;
}
}
In the screen shot above, you can see, the basic configuration of the player input and the movement script on the right. On the left, you can see the project running. There are 2 basic sets of buttons, upper set for selecting the active profile and the lower set for selecting the profile to remap its controls.
When the swap button is pressed, it generates a listing of the controls being used by that configuration. Those names are available through the move actions and array of bindings names.
print("name " + myMoveAction.name);
print("name 0 "+ myMoveAction.bindings[0].name);
print("name 1 "+myMoveAction.bindings[1].name);
print("name 2 "+myMoveAction.bindings[2].name);
print("name 3 "+myMoveAction.bindings[3].name);
print("name 4 "+myMoveAction.bindings[4].name);
The print statements generated this list so we can see the move action name is move 1, the name of bindings [0] is the name WASD and then we can see that bindings 1-4 are the composite members of that control.
In the swap method, I used the effective path to display the name in the GUI button and then we call the start rebind method, using the appropriate move action and the binding as parameters.
if (swap == Swap.WASD)
{
if (GUI.Button(new Rect(70, 100, 50, 30),
this.myMoveAction.bindings[1].effectivePath.Substring(11)))
{
StartInteractiveRebind(myMoveAction, 1);
}
if (GUI.Button(new Rect(70, 140, 50, 30),
myMoveAction.bindings[2].effectivePath.Substring(11)))
{
StartInteractiveRebind(myMoveAction, 2);
}
public void StartInteractiveRebind(InputAction actionToRebind,
int index)
{
PerformInteractiveRebind(actionToRebind, index, true);
}
private void PerformInteractiveRebind(InputAction action,
int bindingIndex, bool allCompositeParts = false)
{
action.Disable();
rebindOperation =
action.PerformInteractiveRebinding(bindingIndex);
rebindStartEvent?.Invoke(rebindOperation);
rebindOperation.Start();
action.Enable();
}
}
[Serializable]
public class InteractiveRebindEvent: UnityEvent<InputActionRebindingExtensions.RebindingOperation>{
}
The interactive rebind method is used here for rebinding an individual key at a time. In the Unity sample code for the rebinding UI, the rebinding would consist of getting input for all 4 of the composite keys each time.
There may also be instances where we want to rebind an individual button rather than something that is part of the composite controls. Consider a button that is used for firing. If you recall, in my action map I have a fire command that is associated with the right mouse button. I have a fire action asset set as
fireAction = InputActions.FindActionMap("Player").FindAction("Fire");
The GUI display has a change fire button that calls the change fire method when it is pressed. The change fire method disables the fire action and then calls the button remapping method with the action as a parameter.
private void changeFire()
{
fireAction.Disable();
RemapButton(fireAction);
}
void RemapButton(InputAction actionToRebind)
{
//Call a method in the InputSystem to handle this.
// To avoid accidental input from mouse motion
var rebindOperation = actionToRebind.PerformInteractiveRebinding()
.WithControlsExcluding("Mouse")
.OnMatchWaitForAnother(0.1f)
.Start();
//Enable the action after we are done rebinding
//so that it will receive input again.
actionToRebind.Enable();
}
This is a link to the video covering this section of the input system.
2.5. Using UGUI
Unity UGUI is a game object based UI system. In Unity UI, every part of the interface uses components along with the Game view to position, plan, and style user interfaces. All parts of the UI appear as game objects in the scene view hierarchy. Because the elements that make up each UI are game objects, they are compatible with other tools and systems within Unity.
The Components
Canvas: The Canvas area is depicted as a rectangle in the Scene view. All of the UI elements must be inside a canvas. The Canvas is a Game Object with a Canvas component on it. Creating a new UI element, such as an Image using the menu GameObject > UI > Image, automatically creates a Canvas, if there isn't already a Canvas in the scene. Canvas uses the EventSystem object to help the Messaging System
Draw order of elements: UI elements in the Canvas are drawn in the order they appear in the Hierarchy. The child element at the top renders first, the second child next, and so on. You can drag the elements in the Hierarchy to reorder them and their rendering order. If two UI elements overlap, the later one will appear on top of the earlier one.
Render Modes: Canvases can render using three different modes:
Screen Space – Overlay: This rendering mode overlays the UI on top of everything in the 3D scene. No 3D object will be rendered in front of the UI, regardless of its placement. The Canvas has the same size as the Game view resolution and automatically changes to match the screen. Post-processing does not affect the UI.
Screen Space – Camera: This is similar to Screen Space – Overlay, but in this mode, the Canvas appears at a given distance in front of a specified Camera component. The camera’s settings (Perspective, Field of View, etc.) affect the appearance of the UI. The UI appears on a 3D plane in front of the Camera defined by the plane distance. The GameObjects can be behind or in front of the Canvas, depending on their 3D positions. This Canvas also has the same size as the Game view resolution.
World Space: In this render mode, the Canvas behaves like a GameObject in the scene. The size of the Canvas can be set manually using its Rect Transform. UI elements will render behind or in front of other objects based on the 3D placement of the Canvas. This is useful for UIs that are meant to be a part of the game world.
When first creating a Canvas, the Render Mode setting defaults to Screen Space – Overlay, which appears very large in the Scene view compared to other GameObjects. One Unity unit (typically one meter) represents one pixel, so creating a UI at HD resolution, for example, makes the Canvas 1920 x 1080 Unity units in the Scene view.
Text: The Text component, which is also known as a Label, has a Text area for entering the text that will be displayed. It is possible to set the font, font style, font size and whether or not the text has rich text capability. There are options to set the alignment of the text, settings for horizontal and vertical overflow which control what happens if the text is larger than the width or height of the rectangle, and a Best Fit option that makes the text resize to fit the available space.
Button: The button is designed to initiate an action when the user clicks and releases it. If the mouse is moved off the button control before the click is released, the action does not take place. The button has a single event called On Click that responds when the user completes a click. The on click has a option to create a list of methods that will be told to execute when the button is pressed. You can select the game object or objects and their associated functions. This is an image of the select controls button. It has a normal, highlighted and pressed colors assigned. There has been one game object, the Cube ugui assigned to the on click list and from that game object, switch controller profile function has been selected from the attached script.
With just a few exceptions, the code between the OnGUI and this UGUI is identical. The major difference is that we will need to break the sequence of if statements in the ONGUI into a set of methods to be called in the UGUI.
if (GUI.Button(new Rect(150, 175, 135, 20), keyString[0, 1]))
{
keyChanging = true;
getInputKey = true;
keyToChange = "up1";
inputKeyCode_string = " ";
}
if (GUI.Button(new Rect(325, 175, 135, 20), keyString[0, 2]))
{
keyChanging = true;
getInputKey = true;
keyToChange = "up2";
inputKeyCode_string = " ";
}
The figure above shows 2 of the OnGUI if statements while the figure below shows those if statements being changed into methods for the on click method to call.
public void ChangeUp1()
{
keyChanging = true;
getInputKey = true;
keyToChange = "up1";
inputKeyCode_string = " ";
}
public void ChangeUp2()
{
keyChanging = true;
getInputKey = true;
keyToChange = "up2";
inputKeyCode_string = " ";
}
A couple of other elements were changed so we could display the text on the screen and in the buttons. Each of these buttons has a child which is a TextMeshPro text object. The using statement using TMPro; is required.
In my script for the UGUI controls, the buttons and text are not public so we will need to find them to start. You could make them public variables and assign them thru the inspector.
These lines have been added to the variable listing above start to store the buttons for movement and the display of the key press.
TextMeshProUGUI UpButton1, UpButton2;
TextMeshProUGUI DownButton1, DownButton2;
TextMeshProUGUI kpText;
In start, we need to find our game objects/buttons and their text fields so we can assign the starting values.
GameObject g = GameObject.Find("keyPress");
kpText = g.GetComponent<TextMeshProUGUI>();
g = GameObject.Find("UpText_c1");
UpButton1 = g.GetComponent<TextMeshProUGUI>();
g = GameObject.Find("UpText_c2");
UpButton2 = g.GetComponent<TextMeshProUGUI>();
g = GameObject.Find("DownText_c1");
DownButton1 = g.GetComponent<TextMeshProUGUI>();
g = GameObject.Find("DownText_c2");
DownButton2 = g.GetComponent<TextMeshProUGUI>();
UpButton1.text = UpKeyCode_string1;
UpButton2.text = UpKeyCode_string2;
DownButton1.text = DownKeyCode_string1;
DownButton2.text = DownKeyCode_string2;
2.6. Using listeners
This next section is going to implement the UGUI but rather than using the on click method of the button, it is going to use listeners. The image depicts another button that does work correctly but there is no method selected for it to execute when it is pressed.
A listener is a function or procedure that waits for specific events to occur, such as user actions like clicks or key presses. When the event happens, the listener executes a predefined block of code to respond to that event, enhancing interactivity in applications.
Multiple event listeners can be defined in a single application. Each listener is logically independent of the other listeners so that events that are enabled or disabled by one listener affect only that listener.
If you recall, in the code block above, we were getting our buttons and TextMeshProUGUI text attributes. We are going to do something similar for our buttons. Above start, we will declare our Button type variables. And then, in start, we will assign the button component to the new Buttons we have made and then assign the listener. The listener is waiting for the onclick method of the button to fire and when it does it will call the method.
Button saveBtn, up1Btn, up2Btn, down1Btn,down2Btn,sel1Btn, sel2Btn;
up1Btn = GameObject.Find("UpButton_c1_2").GetComponent<Button>();
up1Btn.onClick.AddListener(ChangeUp1);
I could have made the code more concise by consolidating the calls to each button but I wanted this to show as an easy extension of using the UGUI without listeners.
The completed code sample for this project is available in the code samples, in the Input demo On_UGUI project. Here is the video for UGUI and IMGUI .
3. UI
The best UI is the one you don’t notice. The user interface is a critical part of any game. Done well, it’s invisible and carefully woven into your application. If done poorly, however, it can frustrate users and detract from the gameplay experience. (The code sample is here.)
UIs include main menus, setting screens or text instructions, and might have buttons, toggles, or sliders. Pretty much every app out there needs some kind of user interface,
whether it’s displaying a character’s vital statistics or the game world’s economy, the interface is your players’ gateway to key information.
If we were looking to craft immersive experiences, this entire course would be about UI/UX. The UI needs to be functional. The trick is using the right UI for the right situation. Should you show an onscreen icon when a player picks up an item or defeats an enemy, or is that too distracting? Game players recognize traditional UIs, such as health bars or menu screens, as conventions of the medium. They’re plastered on the “fourth wall” to communicate with the user. Future trends may involve more immersive features such as health indicators built into a light display on a suit.
2D GUI
When we looked at control configurations, we had a lot of opportunity to work with both canvas and IMGUI. We're going to take a look at both IMGUI and TextMeshPro on a canvas in greater detail.
IMGUI
As we started looking at IMGUI we have a code sample that we're going to utilize that's going to generate a small group of GUI objects. We're going to generate a couple buttons, a label, and a slider. We're going to use screen width and screen height to position them on the screen.
We're also going to use a GUISkin. The GUISkin gives us of couple of advantages over the GUIStyle. With the style, all of the GUI components had the same sets of enhancements. When we start looking at a GUISkin, we're now going to be able to see that we can modify those enhancements so each button, each box, each label has its own set of colorations for on hover on active selected, etc. As we'll see on the following code sample we'll make a GUISkin object as public and then we'll assign to the GUI. And now that we have the GUISkin panel, we can select any of those GUI objects and assign all the various color coding and aesthetics to them.
In the code sample below you can see there is a attempt to position our buttons and sliders and labels so they will be positioned with relation to the center of the screen using Screen.width and Screen.height. As a1st attempt, it will position on the screen but as we go through and have to make adjustments, we're going to find that having all of those hard coded numbers for our offsets and our widths and our height are going to make things rather cumbersome when we have to make changes. Having the numbers hard coded will also make it difficult if we have to try to rescale our features based on are users screen resolution. So while this example will work it, doesn't give us as much flexibility as we might desire.
List<Rect> guiObj = new List<Rect>();
float sliderValue = .5f;
public int currentLevel = 1;
//public GUIStyle myStyle;
public GUISkin mySkin;
void Start()
{
guiObj.Add(new Rect(Screen.width/2 –
160,Screen.height/2 + 233, 55, 55));
guiObj.Add(new Rect(Screen.width/2 –
105, Screen.height/2 + 233, 55, 55));
guiObj.Add(new Rect(Screen.width/2
-50, Screen.height/2 + 233, 55, 55));
guiObj.Add(new Rect(Screen.width/2 +
50, Screen.height/2 + 233, 55, 55));
}
void OnGUI()
{
GUI.skin = mySkin;
if (GUI.Button(guiObj[0], " A "))
{
Camera.main.SendMessage("Shoot");
}
GUI.Button(guiObj[1], "B");
GUI.Label(guiObj[2], "C");
sliderValue =
GUI.HorizontalSlider(guiObj[3],sliderValue,0,1);
}
As you look at the following code I've created a few new variables for my horizontal and vertical scaling and for our starting offset and for our width and our height and while it's initially a bit more work to code that up and use those variables, it's going to pay dividends in the long run because every time I have to change something, I can now make my changes by just changing the value of one or two variables. I don't have to track through and find all the particular hardcoded or magic numbers. I've used variables for horizontal scale and vertical scale but I haven't included those into the code sample. I'm going to leave that up to you to determine how you're going to do it. In some cases you may want to have your display set up for a particular base case resolution and then you'll get the screen resolution from the user and make your modifications. You may want to may only have to adjust horizontally, you may have to also adjust vertical. There's going to be a little bit more coding that goes along with it but it should be relatively straightforward.
float hScale = 1.0f;
float vScale = 1.0f;
int hOffset = 160;
int vOffset = 233;
int width = 55;
int ht = 55;
void Start()
{
guiObj.Add(new Rect(Screen.width / 2 - hOffset,
Screen.height / 2 + vOffset, width, ht));
guiObj.Add(new Rect(Screen.width / 2 - hOffset+ width,
Screen.height / 2 + vOffset, width, ht));
guiObj.Add(new Rect(Screen.width / 2 - hOffset+2*width,
Screen.height / 2 + vOffset, width, ht));
guiObj.Add(new Rect(Screen.width / 2 - hOffset + 3 * width,
Screen.height / 2 + vOffset, width, ht));
}
TextMeshPro
Before we dive into TextMeshPro, remember canvas render mode, world space: In this render mode, the Canvas behaves like a GameObject in the scene. The size of the Canvas can be set manually using its Rect Transform. UI elements will render behind or in front of other objects based on the 3D placement of the Canvas. We will make use of this when we get to create player/enemy labels or health ratings.
TextMeshPro is now the standard Unity UI text component. TextMeshPro is an easy-to-use text system. TextMeshPro is a set of tools for 2D and 3D text. TextMeshPro provides improved control over text formatting and layout when compared to Unity's UI Text & Text Mesh systems. It allows for character, word, line, and paragraph spacing, justified text, and multiple fonts.
The TextMeshPro package is included in the Unity Editor. You do not need to install it. To use TextMeshPro , you must import the TMP Essential Resources package. You can also import the TMP Examples & Extras package to help you learn TextMeshPro.
To use TextMeshPro in your projects, you need to import the TMP Essential Resources.
From the menu, select Window > TextMeshPro > Import TMP Essential Resources
This adds the essential resources to the TextMeshPro folder in the Project.
TextMeshPro also includes additional resources and examples to help you learn about various features. You can import these into your projects as well. From the menu, select Window > TextMeshPro > Import TMP Examples & Extras This adds the examples and additional resources to the TextMeshPro > Examples & Extras folder in the Project. Installing the TMP Examples & Extras is not mandatory.
A TextMeshPro UI Text GameObject has the following components:
- Rect Transform: Controls the GameObject's position and size on the canvas. For more information, see the Rect Transform documentation in the Unity Manual.
- Canvas Renderer: Renders the GameObject on the canvas. For more information, see the Canvas Renderer documentation in the Unity Manual.
- TextMeshPro UGUI (Script): Contains the text to display, and the properties that control its appearance and behavior.
- Material: A Unity material that uses one of the TextMeshPro shaders to further control the text's appearance. For more information see the Shaders section.
There are 4 TextMeshPro object types available for us to use. The text field, a button, a dropdown and an input field. Those are available thru create UI and then select from the pull down. When you add these to a project, if a canvas doesn’t already exist, a canvas will be created to hold them.
In the above Button example, a child object called Text(TMP) has a TextMeshPro
– Text(UI) component that distinctly determines the look of the text.
UI Text GameObjects
The first element we’ll create is a log in. While we often just wish to display non-interactive text messages to the user, there are times when we want the user to be able to enter text or numbers into the game. In section, we’ll use a TextMeshPro input field for the user to enter their name:
As you can see in the following image, we’ve added the TextMeshPro Input field from the UI menu. It contains a text area component that has a placeholder and a text component.
In the input field you can see that I’ve added some colors to the input field. It’s normal color will be green, if the mouse hovers over the field, it will be yellow. When the field is pressed it will flash purple and while it is active, it will be blue.
Instead of our script having a TextMeshProUGUI input field, it is a TMP_InputField and it has been given a tag inputField. That field will be declared as a class variable and then it will be assigned values in Awake rather than Start.
TMP_InputField inputFld;
private void Awake()
{
GameObject g =
GameObject.FindGameObjectWithTag("inputField");
inputFld = g.GetComponent<TMP_InputField>();
}
Once the input field has been created, we will assign a listener when it is activated and remove the listener when it is deactivated. The OnEnable and OnDisable methods will fulfill that functionality. Check name pressed is the method that will be processing our simple functionality which is to get the string and print it to the console.
void OnEnable()
{
inputFld.onSubmit.AddListener(CheckNamePressed);
}
void OnDisable()
{
inputFld.onSubmit.RemoveListener(CheckNamePressed);
}
If the input field is selected (blue), text can be entered. When the user presses the enter key while the input field is active, the check name pressed method will be called. That takes the string from the input field as its parameter. We then clear the input field, print the name and then reactivate the input field for more input. If this were truly a login field, then instead of reactivating the input field, we would probably do some file processing or call scene management to load a new scene and start the game. One other item to notice is that I’ve set the character limit to 8. If the user tries to enter more, they aren’t entered into the field.
void CheckNamePressed(string newText)
{
inputFld.text = string.Empty;
string myName = newText;
print(myName);
inputFld.ActivateInputField();
}
We’ve already seen a lot of coding with buttons when we were working thru the interactive input section. Buttons can also function as labels and it could be nice to be able to change the text thru your scripts. Here we have added a button to our canvas and I’ve given it a tag of b1. I’ve also opened the text child and given the child a tag of b1txt. That is so we can use the tags to find the text field with 2 different processes.
The first process goes directly to the text component to get the TextMeshProUGUI and then assigns the string to the button’s text field. The second process goes to the button component and then goes through the children to find the TextMeshProUGUI and then we assign the string testing 2 to the button’s text field. In actual practice, only one of those processes should be run in the program.
TextMeshProUGUI button1;
void Start()
{
button1 =
GameObject.FindGameObjectWithTag("b1txt").GetComponent<TextMeshProUGUI>();
button1.text = "testing";
button1 = GameObject.FindGameObjectWithTag("b1").GetComponentInChildren<TextMeshProUGUI>();
button1.text = "testing2";
}
In this next section we're going to make a name tag with a TextMeshPro and it's going to be fixed to the game object as a child. Then as the game object moves around the world, the name tag is going to move around with it. In this screenshot you can see the finished product with the label enemy1 above the game object. Now how we got there is going to take a little explaining.
We started by creating a game object called player with name (PlayerWName)and then we added an UI, a TextMeshPro text. That automatically created a canvas that was also going to be a child of our player with name. You can see by looking at the canvas that I've done some manipulating of it so it is closer to the game object in size and position. I've finessed it a bit and you're going to have to do the same thing with yours. You can manipulate the position X, Y and Z and the width and height to adjust it. The image on the left it was our TextMeshPro text object in the inspector. You can see I've had to manipulate the position X, position Y, position Z, the width, and height to make it fit appropriately on the screen. One other key element to look at is the scale. I've dropped the scale value down to .15 to make things look appropriate. The image on the right is the other half of the text object in the inspector and you can see where we have our selectable fonts.
We can set our font size and vertex color. If you set vertex color then you're going to get the image or the label in a solid color. I went to gradient and I used a vertical gradient, but you can use a vertical gradient, a horizontal gradient, a single gradient and a four corners gradient. Now that I have this set up whatever I move player with name around the world, the label enemy1moves along with it.
This next section is going to involve taking our previous example that had a name and we're going to change it into displaying the health. so we have our player object again. it has a canvas attached to it. The canvas is in world space and I've adjusted the scale of the text down to 0.15 again. The first sample we're going to look at involves changing our health display from green to red and this is just going to be based on time rather than any other sort of interaction.
To change the color of the health text I'm going to have to be manipulating the actual color value. I've created a new color, called healthColor that has a 0 value for red, a 1 value for the green and a 1 value for the blue. With the red, green and blue, I'm using the floating point values rather than the zero to 255 integer values.
public class PlayerHealth : MonoBehaviour
{
TextMeshProUGUI health;
float redCount, greenCount;
float currentHealth = 100.0f;
Color healthColor = new Color(0, 1, 1);
float speed = .1f;
float alpha = 1;
void Start()
{
health = GameObject.FindGameObjectWithTag("healthText").GetComponent<TextMeshProUGUI>();
}
void Update()
{
GreenToRed();
// FadeOut();
}
}
void GreenToRed()
{
if(greenCount< .3f)
redCount +=Time.deltaTime*speed;
greenCount -= Time.deltaTime*speed;
healthColor =
new Color(redCount, greenCount, 0);
health.color = healthColor;
}
As we get to the green to red method, we'll see that I'm also using Time.deltaTime and a speed to adjust the rate of change of the color values. By examining the code in green to red, as the green values decrease, the red values are going to increase and I'm going to go from having a dark green health label to a bright red health label.
You may have noticed that in the update method I had a method commented out, the fade out method. The fade out method is designed to take our text color and as we receive more and more damage, it is going to gradually make it become invisible. we're going to do that by changing the transparency component of the color. That's also known as the alpha so technically colors are going to have either 3 components RGB or 4 components RGBA. As you can see the fade out method just uses time to adjust the transparency.
void FadeOut()
{
alpha -= alpha * Time.deltaTime * speed;
healthColor = new Color(health.color.r, health.color.g, health.color.b, alpha );
health.color = healthColor;
}
In the next method, we'll see how to combine these elements with the effects of taking damage. The methods that we talked about above are a way of showing health in general terms without specific values.
As we look at the shot damage method in the OnTriggerEnter method we're going to see a lot of familiar elements. The OnTriggerEnter is going to detect when we have a collision and currently, it's just detecting any collision. It's going to subtract 20 from the health value and then call the shotDamage method. The shotDamage method is going to compute a value for our alpha based on the ratio of the current health to 100. Then it's going to apply it to our health color. Then we're going to access the health text and have it become the word health plus the numeric value. We continue by accessing the color and set our health color.
void shotDamage()
{
alpha = currentHealth / 100;
healthColor = new
Color(health.color.r, health.color.g,
health.color.b, alpha);
health.text = "Health " + currentHealth;
health.color = healthColor;
}
private void OnTriggerEnter(Collider other)
{
currentHealth -= 20;
shotDamage();
}
Once we get all of that working, we are now going to have a health label plus the numeric value that is displayed over the player. it's going to give us an opportunity to see exact values and it's going to give us an opportunity to see if we had increases or decreases in our health numeric value. If we have some power ups or some health packs then we could see the positive effects of utilizing those on the numeric values also.
In this shot sequence you can see the health value that is generated in the inspector in the first shot. And then you can see how the shotDamage method changes the text field to include the numeric value and how we also change the alpha as we go through the process.
This is a link to the video for 2D GUI.
3D GUI
The first component of 3D GUI that we're going to look at is going to be a TextMesh. The TextMesh is a text field but it's not on a canvas and it's not a 2D GUI. The TextMesh is found in unity under: 3D object-> Legacy-> TextMesh
As we look at the TextMesh we can see that it's like a game object. It has a transform with position, rotation and scale, it has a mesh renderer which allows it to both cast shadows and receive shadows and then it has the TextMesh component. The text mesh component allows us to set a text field, set the font size, the line spacing, color, etc. it also has 9 anchors. The anchors determine the starting position of the text and they also determine how it's going to rotate. So we now have a text field that can be in our game. It can rotate in the X, Y and Z planes.
Now that we have our TextMesh object we're going to take a look and see the capabilities to adjust color transparency, adjust rotation and adjust scale.
The first method we're going to look at is going to cause the color of the text to fade or become more transparent. Every time we press the Fire1 button, we're going to decrease our health by 5. Then we're going to take the transparency component our text color and adjust it by multiplying our current health value by .01. Recall that with transparency or alpha, a value of 1.0 is fully opaque and 0 is completely transparent. Thus, as our health decreases, the visibility of our label decreases.
private void FadeOut()
{
if (Input.GetButtonDown("Fire1"))
{
if (currentHealth > 0)
{
currentHealth -= 5.00f;
txtColor.a =
.01f * currentHealth;
}
}
DamageReport.text = "Health " +
currentHealth;
}
Next we're going to discuss a countdown timer or perhaps a race to end condition where the player is going to have a limited amount of time to achieve their goal. In the code sample that follows, I'm using Time.time. The Time.time value is the time in seconds from the start of the application. We check to see if that has stepped past our previous time plus our kill time. If it has, then we’ll assign the current time (Time.time) to previous time and subtract 7 from the health. Then we're going to display it as part of the damage report. We have one additional test to see if the value of currentHealth is less than 0, since we don't want to have a negative health value.
private void Countdown()
{
if (Time.time > (KillTime + PreviousTime))
{
txtColor.a = 1;
PreviousTime = Time.time;
currentHealth -= 7;
}
if (currentHealth < 0)
currentHealth = 0;
DamageReport.text = "" + currentHealth;
}
If you didn't use Time.time with previous time and kill time, you could make a variation of that and make your own counter using Time.deltaTime.
A third variation with TextMesh is to change the scale rather than changing the transparency or just changing the value. The sample again uses the Fire1 button as our activation key. If our health is greater than 0 we subtract 5 from it. The transform has position, rotation and scale. We want to access the scale and we do that with thru the localscale.
private void ScalingText()
{
if (Input.GetButtonDown("Fire1"))
{
if (currentHealth > 0)
{
currentHealth -= 5.00f;
DamageReport.transform.localScale *=
0.01f * currentHealth;
}
}
DamageReport.text = "Health " + currentHealth;
}
Now that we have the scale we're going to take our current health value and multiply it by .01 and then multiply that by the original scale to generate our new scale. In the sample, we're changing the scale on our X,Y and Z. With a little bit of finesse, you could set it up so you only changed the X or the Y or the Z scale.
One more element of 3D GUI that we need to work through is creating a health bar and then being able to scale it. In the image below you can see that I've made a health bar that is a quadrilateral.
The method scalingBar is going to adjust the size of our quadrilateral. In this case, it's only going to adjust it on the X axis but again we have the choice of doing it on X, Y or Z or all three. So again, after we pressed Fire1, we decreased our health by 5 and now we have to generate our proportion. We want to be able to generate the ratio of our current health to our maximum health and one of the things to be careful of here is to make sure that current health or maximum health is a float or a double. You want to be particularly careful not to do integer division. Once we have our ratio we're going to again use the local scale of the transform. We're making a new vector3 that has the original scale in the X multiplied times the current bar length and then the original Y scale and Z scale. Once again we have it set up so that our quadrilateral or our health bar will rotate to face the camera.
private void scalingBar()
{
if (Input.GetButtonDown("Fire1"))
{
currentHealth -= 5.00f;
currentBarLength = currentHealth / maximumHealth;
if (currentHealth >= 0)
HealthBarT.transform.localScale =
new Vector3(
OrigScale.x * currentBarLength,
OrigScale.y, OrigScale.z);
}
HealthBarT.transform.LookAt(Camera.main.transform);
}
We've addressed many different ways that we can generate UI interfaces or displays. The difficult part will be deciding which one you feel fits best for your particular application. The full code sample for the section is in the chapter 3 UI code sample. Here is a video link for the 3D GUI.
4. Classes and Messaging
There are innumerable times that we have created a game object, whether it is an inventory item, an enemy or some other item by just doing a copy/paste and changing the internal code on the object. If this is a one-off game that you don’t intend to ever update or maintain, it may work fine. On the other hand, if you intend to continue working on/ updating/improving the game, a little thought now may save an immense amount of work later. Even if we have gotten to the later point, it would probably be worth the effort to refactor the code to consolidate classes.
Yes, I’m talking about using inheritance within Unity.
Using Inheritance in Unity
As we’ve seen before, Unity children inherit the transform of the parent
The default when creating a script has the script inheriting from MonoBehaviour. In scripts that inherit from MonoBehaviour, the classical constructors cause problems. Start and Awake methods take the place of the constructor for initializing variables and objects. Unity will generate warnings if objects that inherit from MonoBehaviour are instantiated using new.
Unity doesn’t allow an abstract classes to be attached to game objects. So, how do we use an abstract class? A couple of approaches could be used. Have the base class inherit from MonoBehaviour so the subclasses will inherit it indirectly and still be able to use Unity methods such as update. Or don’t inherit MonoBehaviour at all and create a manager to create, move and otherwise control the subclasses. In this case the constructors will need to be used to initialize variables.
Polymorphism
Polymorphism promotes extensibility. Software using polymorphism can operate without knowing the specific types of the objects it interacts with. Only client code that instantiates new objects must be modified to accommodate new types. Polymorphism enables you to write applications that process objects that share the same base class in a class hierarchy as if they were all objects of the base class. This becomes critical if you are using a manager to control behavior rather than using Unity update, fixed update, etc.
The polymorphism occurs when an application invokes a method through a base-class variable. In a method call on an object, the type of the actual referenced object, not the type of the reference, determines which method is called. When the compiler encounters a method call made through a variable, it determines if the method can be called by checking the variable’s class type. At execution time, the type of the object to which the variable refers determines the actual method to use.
Thus the conditions:
- In the same inheritance hierarchy/implement the same interface
- Override the same methods
- Called using a base class reference
- Note: having the objects in a collection of the base class type makes using polymorphism easy.
Interfaces
We can also get polymorphic behavior with interfaces. An interface acts as a contract, requiring any implementing class to define the listed methods. An interface declaration can contain declarations (signatures without any implementation) of methods, properties, indexers and events.
The following interface defines a structure for an enemy class. Any class that uses IEnemy must have an implementation for the Move and the Attack methods. They also have access to the variable destroy time.
interface IEnemy
{
static float destroyTime = 2.5f;
void Move();
void Attack();
}
Below is a simple class for a centaur that inherits from MonoBehaviour and implements IEnemy. The centaur class provides an implementation for the move and attack methods and
uses destroy time in the attack methods to destroy the bullets.
public class Centaur : MonoBehaviour, IEnemy
{
protected NavMeshAgent agent;
public Transform shotSpawn1;
public GameObject BulletPreFab;
float c_timing, fireDelay;
GameObject player;
Vector3 playerPosition;
float speed = 10;
void Start()
{
c_timing = 0;
fireDelay = 10;
agent = GetComponent<NavMeshAgent>();
player = GameObject.FindGameObjectWithTag("Player");
playerPosition = player.GetComponent<Transform>().
position;
agent.destination = playerPosition;
speed = agent.speed;
}
// . . .
This example uses the interface, but does not involve polymorphism. The enemy still use update for movement and there is no manager involved. (link to the interface code sample).
Using Managers
Typical managers are game managers and enemy managers. Avoid creating too many managers, as this can cause confusion and redundant processes. A consideration for using an enemy manager is so that we have one point of control and it makes it easy to use inheritance and polymorphism.
public void Update()
{
Move();
}
public void Attack()
{
GameObject b = Instantiate(BulletPreFab, shotSpawn1.position, shotSpawn1.rotation) as GameObject;
Destroy(b, IEnemy.destroyTime);
c_timing = 0;
}
public void Move()
{
playerPosition =
player.GetComponent<Transform>().position;
c_timing += Time.deltaTime;
if (Vector3.Distance(playerPosition,
agent.transform.position) > 2)
{
agent.destination = playerPosition;
agent.speed = speed * .8f;
}
if ((Vector3.Distance(transform.position, playerPosition)< 20)
&& (c_timing>fireDelay))
{
Attack();
}
}
}
Consider the case where we have an abstract class Enemy. It has abstract move and attack methods. The class does inherit from MonoBehaviour. If the class didn’t, it would not have access to Unity elements such as transforms, get component, etc. While our enemy class does inherit from MonoBehaviour, something like our inventory class may not need to.
Now we will use the IEnemy interface again, this time with a manager. We’ve changed the signature of the move method to include a vector3 that the manager will use to pass a parameter to the enemies.
interface IEnemy
{
static float destroyTime = 2.5f;
void Move(Vector3 p);
void Attack();
}
The manager class will gather the enemies into an array using game objects with tag and then get the IEnemy component and assign it to our list. Now the manager has the update method and it has been removed from the other classes. The manager traverses the list, telling each enemy in turn to move and attack. Notice that the move method is using the player position as the parameter. That is why the interface was changed.
public class EManager : MonoBehaviour
{
List<IEnemy> enemies;
public Player player;
void Start()
{
enemies = new List<IEnemy>();
GameObject[] go =
GameObject.FindGameObjectsWithTag("Enemy");
foreach (GameObject g in go)
enemies.Add(g.GetComponent<IEnemy>());
}
void Update()
{
if (player == null)
return;
Transform playerPos = player.transform;
foreach (IEnemy e in enemies)
{
if (e != null)
{
e.Move(playerPos.position);
e.Attack();
}
}
}
}
As you look at the revised centaur class, using the manager has cleaned it up. The manager is handling the references to the player game object and update. The conditions for the attack approval have been moved into the attack method.
public class Centaur : MonoBehaviour, IEnemy
{
protected NavMeshAgent agent;
public Transform shotSpawn1;
public GameObject BulletPreFab;
float c_timing, fireDelay;
Vector3 playerPosition;
float speed = 10;
void Start()
{
c_timing = 0;
fireDelay = 10;
agent = GetComponent<NavMeshAgent>();
speed = agent.speed;
}
public void Attack()
{
if ((Vector3.Distance(transform.position, playerPosition) <
20) && (c_timing > fireDelay))
{
GameObject b = Instantiate(BulletPreFab,
shotSpawn1.position, shotSpawn1.rotation)
as GameObject;
Destroy(b, IEnemy.destroyTime);
c_timing = 0;
}
}
public void Move(Vector3 playerPosition)
{
c_timing += Time.deltaTime;
if (Vector3.Distance(playerPosition,
agent.transform.position) > 2)
{
agent.destination = playerPosition;
agent.speed = speed * .8f;
}
}
(link to the interface and manager code sample). This is the video for interfaces and managers.
Item Class
Creating an Item class for your game requires careful planning. A simple class might have a name and a quantity. A more complex class might include a method for using the item, whether it involves playing an animation of a punch or swinging a sword or instantiating a bullet prefab. In more complex games, you might define a base class with name and quantity, then create subclasses for various features such as health, gold, weapons, or ammo.”
Projectiles
In most cases, when we instantiate a bullet prefab, we want the bullet to travel forward in the direction the weapon/player is pointed. You will generally want to make a shot spawn object that is attached to the player/weapon. As a child of the player, it has inherited the transform, so as the player moves, jumps, rotates, etc. the shot spawn moves and maintains the same orientation. Using “as GameObject” at the end of instantiation casts the prefab to a GameObject, or returns null if the cast fails. The benefit of that is that we can use that game object in the destroy statement. If the bullet hasn’t been destroyed in the first 5 seconds by some other process, then Destroy will take care of it for us. This prevents the scene from being cluttered with bullets and reduces rendering overhead.
GameObject instantiatedProjectile =
Instantiate(shot, shotSpawn.position,
shotSpawn.rotation) as GameObject;
Destroy(instantiatedProjectile, 5f);
Sending Messages
Send and broadcast messages
We’ve already seen messaging in the input system but we can also use it for communication between game objects. While editing your project, you can set up references between GameObjects in the Inspector. However, this may not always be possible, such as when finding the nearest item at runtime or referencing objects instantiated after the scene loads. In these cases, you can find references and send messages between GameObjects at runtime.
BroadcastMessage allows you to send out a call to a named method, without being specific about where that method should be implemented. It calls the named method on every MonoBehaviour attached to the GameObject and its children. You can optionally choose to enforce that at least one receiver must exist, otherwise an error will be thrown.
SendMessage is a little more specific, and only sends the call to a named method on the GameObject itself, and not its children.
SendMessageUpwards is similar, but sends out the call to a named method on the GameObject and all its parents.
This code is extracted from a destroy by contact script. It sends a message to the collided game object, calling the changeHealth method and with the parameter 10. Notice the method call doesn’t have ( ) associated with it. There can be either no object or one object passed as the parameter. If you need to pass multiple values, then have the parameter be an object that contains them.
void OnTriggerEnter(Collider col)
{
if (col.gameObject.name == "Eathan")
{
col.gameObject.SendMessage("changeHealth", 10);
Destroy(this.gameObject);
}
}
The screenshot below comes from the messaging code sample. The shooter is the orange oval and the other players match their color coding. For demonstration purposes, the camera has two attached scripts, each with a changeHealth method. That method will be called every time the player shoots.
Sending the message to the main camera causes the ChangeHealth method in both classes to be executed.
Camera.main.SendMessage("ChangeHealth", 15);
You should also note that the GUI labels are color coordinated. Adding public GUIStyle myStyle; to your script exposes a GUIStyle field in the Inspector. This allows for a wide variety of features that can be customized to make the interface more aesthetically pleasing. In this shot you can see that I’ve changed the text color to yellow and set the font size to 24. There are many other options to choose from. Some of the more common effects include changing text color on hover or activation of a button. (link to the sending messages sample). This is the link to the video for messaging.
5. Inventory
In this section, we are going to create an inventory system. We will implement the actual storage system and we will create a GUI interface to allow the user to interact with it. It's important that an inventory system operates logically, that it's visually pleasing and lets us quickly identify what we're carrying, and that is easy to use. As we look at the storage system and the display system, there are several decisions that we will have to make.
The first decision is the limitations on the system. Are those limitations going to be the number of items, the weight of the items, or a combination of both. Almost as critical is what game component is going to be the storage medium. A game like “Escape From Tarkov” goes to the extreme limits, instead of just having a backpack, your inventory is divided between a backpack, pockets, etc. The hyperspace bag of Skyrim is a different issue but according to “PCGamer “ magazine, the SkyUI takes one of the worst inventories in modern RPGs and turns it into the best. You can sort the items in the bag by weight or by value or by value per unit of weight.
A system with a limited number of items means that if the inventory is full, we need to use up or drop an item before we can add another. A system that is weight limited might mean that if I drop 1 item weighing 5, then I could pick up several more items that only weigh 1 or 2. The more complex method would be to combine the methods so that both weight and number of items are factors. Consider a pouch with 10 slots but a weight limit of 12. If the items in slots are 1-4 weigh a total of 12, then nothing else could be picked up because the weight limit had been reached.
The second decision is how we want to design, manage and access the inventory system. It will be a GUI but it could be IMIGUI or it could be UGUI with or without listeners. How do we want it to be displayed, always on or selectable? Do we want the game to pause when the inventory is selected? Do we want a capability to have a quick inventory always on screen if we opt for a selectable display? Should the quick inventory items be assigned by the user or should it automatically be populated as items are picked up? Each of these questions needs to be answered before we starting developing the system.
Another factor to be considered is interactions with other players or NPCs. Are you going to have a trade or bartering system for the items. Can the player go to a store and acquire additional elements or upgrades or powerups?
Do we need the ability to order the inventory. If so, what is the key for ordering it, the physical size, item type, slot size, or alphabetical order. Physical size can be either lightest to heaviest or heaviest to lightest. By item type, healing items should be kept separate from weapons and armor. These separate item types can be split up into multiple displays: weapons, armor, healing items, etc.
To keep things manageable, we’ll be using a simpler approach. We're going to create our inventory with a fixed size and allow our items to stack. Adding a duplicate item increases its quantity rather than using an extra inventory slot. One thing you’ll need to decide when implementing the inventory is what kind of data structure to use. As we're talking about inventory items, we also need to probably create a class for those inventory items. t should be simple, with fields like a name and a quantity. You may want to create a ToString or ToJson for it.
We should choose a data structure that provides efficient access to inventory items. Our data sets are going to be small so we could use almost anything. We could use an array, a list or we could conceivably use a dictionary or a linked list. There may be some situations where we'd want to use a different linear data structures, perhaps something such as a stack so that whatever gets put in first goes down to the bottom of the stack and is the last to come out and whatever we put into the stack last is the first thing to come out. In that case, it would be like our backpack was ordered and we could only pick off what's on the top.
There are several key functions we’ll need to implement for the inventory system. Our inventory system needs to support three basic actions: adding items, removing items, and initializing at the start. We do also want to remember we want to have our items stack so we're going to have multiples of items. Ultimately we’ll also add separate inventory for the quick-access items. Those are the basic functionalities that we need to implement our inventory. Now we need to talk a little bit about how we're going to display our inventory.
For display purposes our inventory should be selectable, we can press the key or press the button that will just cause the inventory to be displayed or if it's already being displayed, to cause it to hide. Conversely, the quick inventory should always be on screen. The position is going to be a little bit subjective. I can envision it being top left corner or it being the centered at the bottom of the screen. That's something that you're going to ultimately resolve for your game. We need to be able to assign items from the main inventory to the quick inventory. In this sample, the quick inventory will auto populate.
The logical choice for representing inventory items is to use buttons. In this sample our buttons will be single purpose but there will be cases where the main inventory buttons will be dual purpose buttons. By dual purpose I mean a button able to perform 2 functions, one with the main inventory selected, we should be able to activate or shoot a bullet or an arrow. The second purpose is to let us assign items to the quick inventory by using a key combination, like Shift plus a hotkey.
In the sample, I'll be using IMGUI with the OnGUI methods but if you'd rather use UGUI with a canvas that's fine.
public Item[] invItems;
public Item[] QuickItems;
public int InventorySize;
void InitializeInventory()
{
invItems = new Item[InventorySize];
QuickItems = new Item[4];
for(int i = 0; i < InventorySize; i++)
{
invItems[i] = new Item(EmptyObject,0);
if(i < QuickItems.Length)
QuickItems[i] = invItems[i];
}
}
Here I've shown a couple of the variables I've declared. Inventory items and quick items are both going to be arrays of type item and I have a variable called inventory size that is going to be initialized in the inspector. Then we have our initialize inventory method. The initialized inventory method is called from awake. In this method we setting quick items to size 4 and inventory items are set to inventory item size. Since we're going to be assigning inventory items into our arrays we need to make sure that they are instantiated with the initial values before we do any other processing.
The OnTriggerEnter method is going to be activated when we collide with one of our game objects. It's going to check to see if the tag is an inventory item and it's also going to check to see if the name equals a group inventory item. If it's an inventory item without being a group item then our quantity is 1 and we call the add to inventory method using our amount and our game object. If it turns out to be a group inventory item, those have multiple copies of the objects, so we're going to then get that value from the group object. We're going to use that new value by passing it to the add to inventory method. At the end of the method, we use SetActive(false) to disable the object.
void OnTriggerEnter(Collider col)
{
int invAmount = 1;
if(col.gameObject.tag.Equals("InventoryItem"))
{
if(col.gameObject.name.Equals("GroupInventoryItem"))
{
invAmount =
col.gameObject.GetComponent<GroupInventoryAmount>().amount;
}
AddToInventory(invAmount, col.gameObject);
col.gameObject.SetActive(false);
}
}
This next block of code is the add to inventory method. Notice it takes an int quantity for how many items we're adding and it takes the game object called new item. Because we don’t reposition empty objects by moving them to the end of the array, we will have to process the array twice. The first part of the process is looking to see if the item we're adding is a duplicate. If it's already in the array, we will just need to change the quantity and set added to true.
void AddToInventory(int HowMany, GameObject NewItem)
{
bool added = false;
for (int i = 0; i < invItems.Length; i++)
{
if (invItems[i].Name != "Empty")
{
if (invItems[i].Name == NewItem.name)
{
int val = invItems[i].Quantity + HowMany;
invItems[i].Quantity = val;
added = true;
break;
}
}
}
if (!added)
{
for (int i = 0; i < invItems.Length; i++)
{
if (invItems[i].Name == "Empty")
{
Item it = new Item(NewItem, HowMany);
invItems[i] = it;
added = true;
break;
}
}
}
for (int j = 0; j < QuickItems.Length; j++)
SetQuickItem(invItems[j], j);
}
If we complete the first loop without a match, it doesn’t exist in the array. The next step is to traverse the array again to find the first open slot, where the name will be “Empty”. If we find an element with the name ”Empty”, we create a new item from our item parameter and our count and assign that item into that spot in the array. If we reach the end of the array without finding an empty slot, the inventory is full and the item is not added.
Now we'll take a look at our quick items. Since we're going to let our quick items auto populate, we're going to traverse our quick items and we're assigning our inventory items to the corresponding spot in our quick items.
The next block of code shows 2 methods. It shows the update method which has our key press of the I key to control inventory variable. if you haven't seen it before the “?” operator is a conditional or ternary operator. a ? b : c evaluates to b when a is true and c when a is false. So this code is turning show inventory either true if it was false or if it was false it's turning it true.
showInventory = (showInventory) ? false : true;
Next is the on GUI method and instead of having everything listed out inside the on GUI method we're going to be calling it with a slightly different process. I have the inventory rectangle that's been declared as
public Rect inventoryRect =
new Rect(Screen.width / 2, Screen.height / 2, 400, 400);
If show inventory is true then the inventory rectangle is going to receive a GUI window. The window has an ID of 0, the inventory rectangle we've just finished declaring, inventory GUI which is a call to a the InventoryGUI method and the parameter in quotes is the label that's going to be on our display.
void Update()
{
if(Input.GetKeyDown(KeyCode.I))
{
showInventory = (showInventory) ? false : true;
}
}
void OnGUI()
{
if(showInventory)
{
inventoryRect = GUI.Window(0, inventoryRect,
InventoryGUI, "Inventory");
}
}
In the next block of code, we're going to be exploring the inventory GUI method. The first thing to notice is that we have a GUILayout and we're going to set an area. That's going to set a region for all of our buttons and other features to be stored in. Then we have a GUILayout.BeginHorizontal. The BeginHorizontal creates a row and sizes the individual items to fill the row. If there's only one item in there then that one item is going to fill the entire row while if there are two items in there they're going to be split 50/50 etc. If we have 3-4 items, they will be scaled proportionally to the size of the bounding rectangle. We do have to remember that at the end of our GUILayout, for each begin that we have, we also have to have an end. In the same, we need to have the GUILayout.EndHorizontal(); and the GUILayout.EndArea(); If those are omitted, Unity will generate warnings or errors related to unbalanced GUI clips.
Notice the buttons are also just a little bit different. it's now a GUILayout.Button that takes a string for the title and the height of the field. Inside the if statement we have two methods being processed. First is using an item. That's going to determine what is instantiated. The second is to remove the item from inventory. I'm not going to show you the implementation code for the use at this point in time.
void InventoryGUI(int ID)
{
GUILayout.BeginArea(new Rect(0, 50, 400, 400));
GUILayout.BeginHorizontal();
if (GUILayout.Button(invItems[0].ToString(),GUILayout.Height(75)))
{
Use(invItems[0]);
RemoveFromInventory(1, invItems[0]);
}
if( GUILayout.Button(invItems[1].ToString(),GUILayout.Height(75)))
{
Use(invItems[1]);
RemoveFromInventory(1, invItems[1]);
}
GUILayout.Button(invItems[2].ToString(),GUILayout.Height(75));
GUILayout.EndHorizontal()
. . .
GUILayout.EndArea();
The remove from inventory method is called when we press the button in our UI. Similar to the way that we did our add to inventory, we're going to traverse our entire collection to try to find a match. If we find a match, then we have two items to process. First we're going to subtract how many from our quantity and if our quantity reaches 0 or less, we'll replace the item that's in the array with an empty object. If we find a match, after it is processed, we'll trigger the break statement to terminate the loop since we have found our match. Then finally we'll go through and we set our inventory items 0 through 3 into our quick items.
What we've implemented here is a rather simple inventory system that allows us to pick up items, add them to our inventory, use items, remove them from our inventory. As we proceed we may need to start looking at different options. We may need to be able to assign individual items from our inventory to our quick items rather than auto populate. We may want a feature so that we don't have an empty item in the midst of our inventory. We may want to have our inventory collapse so there are always active items in the spots. We may find that as we proceed, the an array is not the best choice for holding our data. We may want to look implementing the inventory as a list, as a linked list or even as a dictionary. As you are exploring processes for assignment of the quick items, you may consider a key combination for activating the selection process or use the clicking of distinct button/hot bar to set change a variable state.
We should also consider making the inventory and its processes distinct from the inventory display. We should plan for a case where the inventory implementation is totally separate from the inventory display. It may mean adding a get method to the inventory. That would allow the inventory to be sent to the display process as an array, a list, a dictionary or some other data structure. (This is the link to the inventory code sample) This is the video on inventory.
6. Enemy and Friendly AIs
AI in games
- Game AI needs to complement the quality of a game. it's about making a game challenging. This means the game should not be so difficult that it's impossible for the player to beat the opponent, or too easy to win. Finding the right challenge level is the key to make a game fun to play.
- The role of AI in games is to make it fun by providing challenging opponents to compete against and interesting non-player characters (NPCs) that behave realistically inside the game world.
We’ve seen very basic AI in our introductory games. The NPC/enemy was given the player’s position and told to move to it. Without a pathfinding capability, the NPC was likely to get stuck along the way because of some obstacles. Adding a pathfinding capability such as Unity’s NavMesh, with surface, agent and obstacle components, generally meant that the navigation would be successful but that still left a series of actions unaccounted for.
Although simple reactive systems are very powerful, there are many situations where they are not realistic enough. Sometimes we want to make different decisions based on what the agent is currently doing, and representing that as a condition is unwieldy. Sometimes there are just too many conditions to effectively represent them in a decision tree or a script. Sometimes we need to think ahead and estimate how the situation will change before deciding our next move. For these problems, we need more complex solutions.
There are several very common techniques used to create AI. We’re going to explore finite state machines and hierarchical finite state machines. The AI behavior tree will be described briefly but it will be left for future development. Depending on the game that you are making and the complexity of the desired AI, the technique you use will vary.
- Finite State Machines (FSM) can be considered as one of the simpler AI models, and are commonly used in games because they are easy to implement and more than enough for both simple and games that are a bit more complex. A state machine basically consists of a finite number of states that are connected in a graph by the transitions between them. A game entity starts with an initial state, and then looks out for the events and rules that will trigger a transition to another state. A game entity can only be in exactly one state at any given time. A traffic light is an example of a simple finite state machine.
Here is a simple FSM that has 3 states, patrol, chase and attack. The player starts in the patrol state and stays there until the player is in sight. Then it will transition to the chase state. If it loses sight, it returns to the patrol mode while if it gets in range, it transitions to the attack mode. Then while in attack, if the player gets out of range, it reverts to chase but it the player is killed or we lose sight, we transition to patrol.
An overview of a behavior tree AI system
- A behavior tree is another kind of AI system that works in a very similar way to finite state machines. This hierarchal system gives us control over many finite state systems within the behavior tree, allowing us to have a complex AI system. Behavior trees are a better way to implement AI game characters that are more complex. One of the reasons for this is that in state machines, all the transitions between states have to be precisely defined. So, as the size of state machine becomes bigger, updating the structure of a state machine with all the transitions becomes extremely complex.
A behavior tree (BT) breaks down complex tasks into manageable actions, allowing game characters to make decisions based on specific conditions. Behavior trees consist of nodes, which represent either actions or decisions. These nodes are connected in a tree-like structure, making it easy to follow the AI’s decision-making process. The main benefit of a behavior tree is its modular nature. You can break down complicated tasks into smaller, more manageable parts, which makes it easier to understand and control how your game characters behave.
The basic elements of behavior trees are tasks, where states are the main elements for FSMs. There are a few different tasks such as Sequence, Selector, Decorator and Composite.
- Selector tasks are represented with a circle and a question mark inside. First it'll choose to attack the player. If the Attack task returns success, the Selector task is done and will go back to the parent node, if there is one. If the Attack task fails, it'll try the Chase task. If the Chase task fails, it’ll try the Patrol task.
- Sequence tasks are denoted by a rectangle with an arrow inside it. The root selector may choose the first Sequence action. This Sequence action's first task is to check whether the player character is close enough to attack. If this task succeeds, it'll proceed with the next task, which is to attack the player. If the Attack task also returns success, the whole sequence will return success, and the selector is done with this behavior and will not continue with other Sequence tasks. If the Close enough to attack task fails, then the Sequence action will not proceed to the Attack task, and will return a failed status to the parent selector task. Then the selector will choose the next task in the sequence, Lost or Killed Player and continue as directed.
Behavior trees can be quite complex as there are often many different ways to draw up the tree and finding the right combination of decorator and composite nodes can be challenging. There are also the issues of how often to check the tree. Do we want traverse it every frame, or only when something happens that means one of the conditions has changed? How do we to store state relating to the nodes? How do we know which nodes were executing last time, so we can handle a sequence correctly?
Hierarchical Finite State Machine
There are drawbacks to the state machine pattern. First, the phenomenon called “state explosion” is common as your system grows in complexity. This means that you end up with so many states that the clarity that the FSM pattern provides is overwhelmed, making it hard to navigate and understand. The hierarchical finite state machine (HSM) not only divides the system into separate states, it puts them into a hierarchy of sub-states, which themselves can be state machines.
With HFSMs, we get the ability to build relatively complex behavior sets in a relatively intuitive manner. The transition rules are tightly-bound to the current state. Careful use of a hierarchy of states can reduce the amount of transition duplication. HFSMs provide a powerful way to manage complex state transitions in your game. By allowing states to be nested, HFSMs enable developers to create more organized and manageable state logic. This is particularly useful in scenarios where multiple states share common behaviors or properties.
As we look at hierarchical finite state machines we're going to take an example where our NPC has the following possible sets of actions. It can be idle, it can patrol or it can search. It's going to have a state for a melee attack that's going to have two substates, one for following the player and one for attacking with a melee style. It's also going to have a ranged attack and again it's going to have the find the target substate and the actual ranged attack substate. it's going to have the capability to heal and it's going to have the capability to flee. Now that looks like about 11 different states that we could potentially have our NPC in and I'm going to see if I can organize them.
As I said earlier there's going to be any number of ways that these can be organized but I think I've put these into a structure that I can work with. I'm going to have an idle behavior that's just going to call the idle action. There's a guard behavior that has 2 actions, patrol and search. In combat, we're going to have the option for melee or ranged type attacks and I also put heal in there. Finally we're going to have our flee behavior, that is just going to have handle the that action.
As we start looking at the code we can see that we're going to start off with some enumerated types for our behaviors. We'll have idle, guard, combat, and flee which matches up with our basic behaviors from above and then we're going to initialize our behaviors to idle when we start. We also have some boolean variables that will be used to help control the transitions. As a note, when you have a handful of flags where only one is true at a time, that’s when you really want to use an enum.
public enum Behaviors {Idle, Guard, Combat, Flee};
public class AI_Agent : MonoBehaviour
{
public Behaviors aiBehaviors = Behaviors.Idle;
public bool isSuspicious = false;
public bool isInRange = false;
public bool FightsRanged = false;
As we look at start and update, the NavMesh agent uses a fully qualified name in the get component but we could remove the fully qualified name by adding using UnityEngine.AI to the using statements.
void Start()
{
navAgent = GetComponent<UnityEngine.AI.NavMeshAgent>();
anim = GetComponent<Animation>();
}
void Update ()
{
RunBehaviors();
}
As we work through this example and these methods, it is important to remember that I have it set up to change the states in the inspector. When this is implemented as a complete AI process, the transitions are going to have to be incorporated into the code.
void RunBehaviors()
{
switch(aiBehaviors)
{
case Behaviors.Idle:
RunIdleNode();
break;
case Behaviors.Guard:
RunGuardNode();
break;
case Behaviors.Combat:
RunCombatNode();
break;
case Behaviors.Flee:
RunFleeNode();
break;
}
}
As you can see there run behaviors method is just a switch statement that switches through our enumerated types to determine which set of behaviors we're going to run. There is no need for a default case because we can only be running 1 of these 4 behaviors. In addition to having run behaviors in update, we also have it the change behaviors method.
void ChangeBehavior(Behaviors
newBehavior)
{
aiBehaviors = newBehavior;
RunBehaviors();
}
Because we want to keep this structured and give us a chance to expand, without the code becoming too messy, the run node methods are very simple at this stage. They just call the next method.
void RunIdleNode()
{
Idle();
}
void RunGuardNode()
{
Guard();
}
void RunCombatNode()
{
Combat();
}
void RunFleeNode()
{
Flee();
}
Below, we have the set of Guard actions. If the NPC is suspicious, then it starts the search for target process. In this simple model, if the distance is less than 2, in range becomes true and the NPC transitions to combat mode. In a more complex model, the in range may be determined by which sorts of attacks the NPC can conduct. There should probably be differing ranges for melee and ranged attacks. If the NPC is not suspicious, then it moves to the patrol method.
void Guard()
{
if(isSuspicious)
{
SearchForTarget();
if (Distance < 2.00f)
{
isInRange = true;
ChangeBehavior (Behaviors.Combat);
}
}
else
{
Patrol();
//PatrolCircle();
//PatrolRandom();
}
}
The patrol method has several different approaches that could be taken. We could generate random coordinates and tell the NPC to move there. The drawback to the random points is that without special care, they could be created in locations that the NPC can’t reach. There may not be a navigable path or they may be generated above or below the terrain.
To solve this issue, it is easy to create a set of waypoints. They can be generated on the terrain and then given to the NPC as array or list. Now the challenge becomes determining the approach to be used by the NPC for travelling between the waypoints. The patrol pattern could start at waypoint 0 and go through them one by one. When reaching the end, you could either reverse the process to traverse them in reverse order or you could travel to waypoint 0 and keep going in the same sequence. Another option could be to choose waypoints at random.
The first patrol pattern uses the reverse option. The distance of 2 is used to transition to the next waypoint, rather than relying on the navmesh agent stopping distance.
void Patrol()
{
anim.Play("run");
Distance = Vector3.Distance(
gameObject.transform.position,
Waypoints[curWaypoint].position);
if(Distance > 2.00f)
{
Destination = Waypoints[curWaypoint].position;
navAgent.SetDestination(Destination);
}
else
{
if(ReversePath)
{
if(curWaypoint <= 0)
{
ReversePath = false;
}
else
{
curWaypoint--;
Destination = Waypoints[curWaypoint].position;
}
}
else
{
if(curWaypoint >= Waypoints.Length - 1)
{
ReversePath = true;
}
else
{
curWaypoint++;
Destination = Waypoints[curWaypoint].position;
}
}
}
}
The following sample uses a simple process to move in a continuous loop through the waypoints.
void PatrolCircle()
{
anim.Play("run");
Distance =
Vector3.Distance(gameObject.transform.position,
Waypoints[curWaypoint].position);
if (Distance > 2.00f)
{
Destination = Waypoints[curWaypoint].position;
navAgent.SetDestination(Destination);
}
else
{
curWaypoint++;
if (curWaypoint >= Waypoints.Length)
curWaypoint = 0;
Destination = Waypoints[curWaypoint].position;
}
}
Using the random number generator, some modulus division and little finese will allow us to generate a random path thru the array of waypoints, taking care to avoid assigning the current waypoint to the new destination. This process uses the unity engine random range method. In the random range, the first parameter is inclusive and the second parameter Is exclusive.
void PatrolRandom()
{
anim.Play("run");
Distance = Vector3.Distance(gameObject.transform.position, Waypoints[curWaypoint].position);
if (Distance > 2.00f)
{
Destination =
Waypoints[curWaypoint].position;
navAgent.SetDestination(Destination);
}
else
{
curWaypoint= UnityEngine.Random.Range(1,
Waypoints.Length) % Waypoints.Length;
Destination=Waypoints[curWaypoint].position;
}
}
As we look at the other methods, some of them involve having our NPC move. We’ve seen how to use the animator before but now we’re going to see how to use an animation.
We are not going to make the animation but use ones that already exist on the character. In the past with animators, you had to make the animator with the states and transitions. With animation, all we have to do is tell the animation to play. While there are many methods available to animation, I’ll use either animation Play or animation PlayQueued. Animation Play stops the current animation and starts the new one while PlayQueued waits until the current animation has finished playing before starting the new animation.
The attack methods themselves are relatively straight forward. In the melee attack, we’ll play the attack animation and perhaps effect the player’s health. For the ranged attack, we’ll generate a bullet and destroy it after a given time frame. While these 2 methods are simple, we should probably consider having a cool down period or timer in both attack methods so we aren’t causing damage or generating bullets every frame.
void RangedAttack()
{
anim.Play("attack");
GameObject newProjectile= Instantiate(Projectile, transform.position,
Quaternion.identity) as GameObject;
Destroy(newProjectile, 5);
}
void MeleeAttack()
{
anim.Play("attack");
Camera.main.SendMessage("stat_health", - 2);
}
The search for target method is essentially chasing the player until the NPC is in range to transition to attack. Again, remember that I am controlling the states in the inspector. A complete implemention would have a condition allowing for the player to get out of range which would cause a transition, probably back to the patrol state or perhaps to the idle state.
void SearchForTarget()
{
anim.Play("run");
Destination = GameObject.FindGameObjectWithTag
("Character").transform.position;
navAgent.SetDestination(Destination);
Distance = Vector3.Distance(
gameObject.transform.position, Destination);
if (Distance < 2.00f) {
isInRange = true;
}
}
For the flee method, we have to decide when and how to conduct it. It might be necessary because the NPC health is low or perhaps it is running low on ammo. It could be that it is outnumbered and has a strong sense of survival. Another component of the flee method is how the destination is selected. Are you going to use an existing waypoint? Is it going to be the waypoint that is farthest away from the current position or is it going to be the first one found that is at least some distance away from the player.
void Flee()
{
anim.Play("run");
for(int i = 0; i < Waypoints.Length; i++)
{
Distance = Vector3.Distance(
gameObject.transform.position,
Waypoints[i].position);
if(Distance > 10.00f)
{
Destination = Waypoints[curWaypoint].position;
navAgent.SetDestination(Destination);
break;
}
else if(Distance < 2.00f)
{
ChangeBehavior(Behaviors.Idle);
}
}
}
In this case, I set the destination to be the first waypoint more than 10 units from the current position. Then upon arriving at that destination, I changed the behavior to idle.
So far, we have given our NPC perfect knowledge of the player location. There has been no opportunity for the player to be out of sight. One of the processes that would increase the realism is using raycasting. It will allow for the player to be masked by terrain or other features. It could also be orientation-based, using the direction the NPC is facing, simulating a field of view.
Raycasts provide a way for you to project lines through your scenes and detect the objects they hit as well as return important information about what they hit. This gives you a way to detect if a projectile will impact a surface, test if one player can see another, simulate a laser pointer and more.
There are two main ways of raycasting in Unity. The first behaves like a laser pointer being cast out from an origin point and stopping once it encounters a single object (or its maximum range). The other option uses the Physics.RaycastAll and returns a set of all objects that would be impacted across the entire length of the ray.
This sample of raycasting first creates the vector for the ray direction by subtracting the NPC’s position from the player’s position. It then uses a 60 degree field of view to see if the player is in that cone. Next it casts the ray from the NPC position, in the direction of the player for the view distance. In this case view distance was defined as 10. The out parameter hit, is a value generated and “returned” by the raycast method. A raycast hit object is a structure that holds information about the object that was hit, such as rigidbody, transform, point, normal, collider and distance.
void Raycasting()
{
float ViewDistance = 10;
RaycastHit hit;
Vector3 rayDirection =
GameObject.FindGameObjectWithTag("Character")
.transform.position- transform.position;
//Check the angle between the AI character's forward
//vector and the direction vector between player and AI
if ((Vector3.Angle(rayDirection, transform.forward)) < 60)
{
// Detect if player is within the field of view
if (Physics.Raycast(transform.position, rayDirection, out hit,
ViewDistance))
{
print("player detected");
}
}
}
Finally in this scene you can see the NPC running around the world with the NavMesh segments selected showing the pathfinding as it goes from one waypoint to the next.
(This is the link to the AI code sample) This is the link to the video on AI.
Another aspect of the AI could be to involve it with Dynamic Difficulty Adjustments (DDA). While adjustments to the player should be avoided, it is feasible to adjust the enemy actions/reactions. If the player is performing too well, it may be necessary to make the enemy stronger, faster or more accurate. If the player is struggling, you could adjust the enemy to make it easier for the player. One of the major challenges with DDA is keeping the player from detecting that it is occurring.
7. Keeping Score
People have scores in real life and scores in games. Real life scores include your credit score, your GPA, your health stats, your driving record at the DMV, etc. People keep score in games to track their performance or for comparison with other players. It enhances the viewing experience by allowing them to relive the game later through their stats and achievements.
PlayerPrefs
PlayerPrefs are a Unity function that's designed to allow storage of some game information. By the Unity specifications the minimum guaranteed volume is one MB. That will depend on the particular system and device being used. In Windows, the information stored in the hot keys directory and the one MB restriction is not going to apply there. PlayerPrefs can store three types of data, strings, ints and floats. They're stored in the format of a key value pair or a dictionary where the first parameter is the lookup key and the second parameter is the value that we're storing.
PlayerPrefs.SetFloat(“keyname”, variable name);
So if I want to store a floating point variable called score I might have:
PlayerPrefs.SetFloat(“myScore”, score);
The PlayerPrefs set commands all follow the same format so I could have:
PlayerPrefs.SetInt(“myAge”, 42);
PlayerPrefs.SetString(“myName”, ”the_author”);
To retrieve data that's been previously saved using PlayerPrefs we're going to use the get command. The PlayerPrefs get method has one parameter. It is the key that we're looking for so getting my score would look like this.
float s=PlayerPrefs.GetFloat(“myScore”);
PlayerPrefs has a save function, PlayerPrefs.Save(); It writes all modified information. By default, Unity writes preferences during OnApplicationQuit(). In cases when the game crashes or otherwise prematurely exits, you might want to save the PlayerPrefs at sensible “checkpoints” in your game. This function will potentially causing a small hiccup when you are writing, therefore it is not recommended to call during actual gameplay.
Note: There is no need to call this function manually inside OnApplicationQuit().
If the key doesn’t exist when you are using a get command, PlayerPrefs will return a 0 or null as appropriate to the data type.
PlayerPrefs are not portable. When you make a copy of your game and put it on a new computer, those values will not exist. You should have a process creates some default values rather than having nulls and 0s inserted into your game. There is also a function in the editor that will clear all PlayerPrefs. An easy check for the existence of the PlayerPrefs is to make an unique key and test if it exists, if not, set the defaults. Consider:
PlayerPrefs.SetString(“WizardGame”, ”Merlin”);
is set as a key for the PlayerPrefs. Then we can test for it,
if(PlayerPrefs.GetString(“WizardGame”) != “Merlin)
we would know that we need to set the default values. We will do an extensive session with PlayerPrefs in the stat class in the next section. Video on PlayerPrefs.
Stats
The statistics that are maintained in a game are game dependent. For some games, it may be just a single score for that particular run. For others, the stats might include items such as health, gold, score, playing time for a run or cumulative totals for a series of items. It may include wins, losses, ties, shots taken, hits, misses, etc. If the data is cumulative, you may also consider creating some achievements. Some achievements may keep a player returning to the game just for the purpose of collecting those. Some achievements could be “won 10 games”, or “won 10 games in a row”, or “played 100 hours”, or “20 levels completed”. As the game developer, you are going to have to decide which stats you want to maintain and how you will display them. You also have to decide the persistence of the data. Is it just for this run or should it be saved and be available for future runs. You may also want to allow more than one player to store their data.
We’re going to start with a basic class for the stats. Notice that it doesn’t inherit from MonoBehaviour. Our stats class has name and amount as attributes, a default and overloaded constructor and then properties for accessing the name and amount. I intentionally omitted the ToString method because I didn’t see a case where it was necessary.
public class Stats
{
private string name;
private float amount;
public Stats() : this("none", 0) {}
public Stats(string n, float a)
{
name = n;
amount = a;
}
public string Name
{
get=> name;
set => name = value;
}
public float Amount
{
get { return amount; }
set { amount = value; }
}
}
Notice in the Name property, the =>. It is not a lambda expression but an expression body member. Expression-bodied members are a set of features that simply add some syntactic convenience to C#. This means that they don’t provide functionality that couldn't otherwise be achieved through existing features. These features allow a more expressive and succinct syntax to be used. Expression-bodied members have a handful of shortcuts that make property members more compact. There is no need to use a return statement because the compiler can infer that you want to return the result of the expression. There is no need to create a statement block because the body is only one expression. There is no need to use the get keyword because it is implied by the use of the expression-bodied member syntax. Using expressions is optional and you can see if have the traditional property configuration for Amount.
Next we are going to create a class for displaying the stats. The StatGUI class will have a public array holding the data that we will display. Notice the hide in inspector that is immediately above the stats array. That means that although it is public, it is not visible in the inspector panel. Start instantiates our stat array and initData provides some arbitrary values to the array. We have a set of enums shown here but I’m going to use them as indices rather than for controlling states. Recall the enums have underlying integer values. Unless otherwise assigned, they start at 0 and increment by 1s. In this case Level is 0, Won is 1, etc.
public class StatGUI : MonoBehaviour
{
public enum statsName { Level, Won, Lost , Kills, Deaths, KDR};
public bool showStats = true;
public Rect statsRect =new Rect(Screen.width/2,
Screen.height / 2, 400, 400);
[HideInInspector]
public Stats[] data;
private void Start()
{
data = new Stats[6];
initData();
}
private void initData()
{
data[0] = new Stats("Level", 200);
data[1] = new Stats("Rounds Won", 70);
data[2] = new Stats("Rounds Lost", 50);
data[(int)statsName.Kills] =
new Stats("Kills", 50);
data[4] = new Stats("Deaths", 2);
data[5] = new Stats("KDR", 0);
}
The next block of code contains an update that calls the set stats method for kills or deaths depending on whether the K or L keys are pressed. Remember this is for demo purposes and that would not be sufficient for a game. The OnGUI method is going to activate the display if the show stats variable is true. The StatsGUI method is going to take advantage of the data being kept in an array and use a loop for generating our display. We’re using a GUILayout so we have a being area and an end area. Then within the area we set up a vertical display using begin vertical. Because we are using GUILayout, we don’t have to specify the rectangle, the width or the height unless the default values are not adequate.
private void Update()
{
if (Input.GetKey("k"))
SetStat("Kills", 1);
if (Input.GetKey("l"))
SetStat("Deaths", 1);
}
void OnGUI()
{
if (showStats)
{
statsRect = GUI.Window(0, statsRect, StatsGUI, "Stats");
}
}
void StatsGUI(int ID)
{
GUILayout.BeginArea(new Rect(15, 25, 400, 400));
GUILayout.BeginVertical();
for (int i = 0; i < data.Length; i++)
{
GUILayout.Label(data[i].Name + " - " + data[i].Amount);
}
GUILayout.EndVertical();
GUILayout.EndArea();
}
The SetStat method is called from update if the K or L keys are pressed. The method header has the usual return type, method name and the string parameter that you normally see.
void SetStat(string stat, int value = 0)
The int value =0 is probably new. This is a way of overloading a method. If we called SetStat (“shots”);, value is assigned a 0 while if we called SetStat(“kills”,5);, value is 5. In the switch statements, you can see I have used the enums, cast into ints, as the indices. This provides a mnemonic for the various objects stored in the array so I don’t have to remember if I need index 0 or 1 for a particular item.
Notice that the kill/death ration is calculated at the end of the SetStat method. The KDR is only going to change when kills or deaths occur, so rather than computing it every frame in update or OnGUI, I placed it here. Division by zero is always a concern and usually will cause a runtime exception. A common mistake is to check for kills being > 0 rather than deaths being > 0.
void SetStat(string stat, int value = 0)
{
switch (stat)
{
case "Kills":
data[(int)statsName.Kills].Amount += value;
break;
case "Deaths":
data[(int)statsName.Deaths].Amount += value;
break;
}
// if (kills > 0)
if (data[(int)statsName.Deaths].Amount > 0)
data[(int)statsName.KDR].Amount =
data[(int)statsName.Kills].Amount /
data[(int)statsName.Deaths].Amount;
}
So far, we have created a display that shows the values as texts in our Gui window.
Next, we will be taking our stats and still using OnGUI, use 2 processes for turning them into a more graphical display. After that, we will explore processes to control the menus and then get to using PlayerPrefs for data persistence.
The first process is uses a series of buttons and a scale factor to display the achievements. This is the same data set that the StatGUI class uses, just a more graphical display. The OnGUI method calls the
void OnGUI()
{
if (showAchievements)
{
achRect =GUI.Window(0, achRect, AchGUI, "Achievements x50");
// achRect =GUI.Window(0, achRect, AchGUI2,"Achievements x50");
}
}
void AchGUI(int ID)
{
GUILayout.BeginArea(new Rect(15, 25, 600, 300));
GUILayout.BeginVertical();
for (int i = 0; i < stat.data.Length; i++)
{
GUILayout.Label(stat.data[i].Name, GUILayout.Height(25));
}
GUILayout.EndVertical();
GUILayout.EndArea();
GUILayout.BeginArea(new Rect(100, 25, 600, 300));
for (int i = 0; i < stat.data.Length - 1; i++)
{
GUILayout.BeginHorizontal();
float achieveAmount = stat.data[i].Amount / 50;
for (int j = 0; j < achieveAmount; j++)
{
GUILayout.Button(stat.data[i].Name + " " + (j + 1),
GUILayout.Height(25), GUILayout.Width(75));
}
GUILayout.EndHorizontal();
}
GUILayout.Button( " " + stat.data[5].Amount,
GUILayout.Height(25), GUILayout.Width(75));
GUILayout.EndArea();
}
AchGUI method or later the AchGUI 2 method. The AchGUI method will loop thru the data in the stat.data array, first creating a label and then in a second area, will use a horizontal layout to generate the buttons, with a scale of 50 units per, to accommodate the data. This design uses a float to control the creation of the buttons so it effectively rounds the display up, 11 would show 1 button while 201 would show 5. The last stat is not displayed with the series of buttons since the KDR will normally be less than 50.
This second version of our AchGUI is going to keep the same framework for the labels but rather than having multiple buttons for the sets of values, the horizontal size of the button will grow. Once again, I’ve omitted having the KDR grow.
void AchGUI2(int ID)
{
GUILayout.BeginArea(new Rect(15, 25, 600, 300));
GUILayout.BeginVertical();
for (int i = 0; i < stat.data.Length; i++)
{
GUILayout.Label(stat.data[i].Name, GUILayout.Height(25));
}
GUILayout.EndVertical();
GUILayout.EndArea();
float fixedWidth = 0.4f;
GUILayout.BeginArea(new Rect(100, 25, 600, 300));
for (int i = 0; i < stat.data.Length; i++)
{
if (i < 5)
GUILayout.Button("" + stat.data[i].Amount, GUILayout.Height(25),
GUILayout.Width(stat.data[i].Amount * fixedWidth));
else
GUILayout.Button("" + stat.data[i].Amount,
GUILayout.Height(25), GUILayout.Width(50));
}
GUILayout.EndArea();
}
We now have 3 different processes for displaying the data. As you develop your games, you will need to choose how you want it to be displayed. We still need to discuss the issues of showing/not showing the stats on screen and a save/load process.
The menu class is also attached to the camera. It is just going to use key inputs to control the displays. Selecting alpha1, the 1 on the keyboard, causes the boolean values for show stats and for showing achievements to be negated. Pressing alpha2, turns both boolean values to false.
public class MenuActivate : MonoBehaviour
{
GameObject StatGO;
StatGUI stat;
AchievementGUI ach;
void Start()
{
StatGO = GameObject.Find("Main Camera");
stat = StatGO.GetComponent<StatGUI>();
ach = StatGO.GetComponent<AchievementGUI>();
}
void Update()
{
if ( Input.GetKeyDown(KeyCode.Alpha1))
{
if (stat.showStats)
{
MenuSwitcher("achieve");
}
else
{
MenuSwitcher("stat");
}
}
if ( Input.GetKeyDown(KeyCode.Alpha2))
MenuSwitcher("none");
}
void MenuSwitcher(string activatedMenu)
{
if (activatedMenu.Equals("stat"))
{
stat.showStats = true;
ach.showAchievements = false;
}
else if (activatedMenu.Equals("achieve"))
{
ach.showAchievements = true;
stat.showStats = false;
}
else
{
stat.showStats = false;
ach.showAchievements = false;
}
}
}
We discussed PlayerPrefs earlier in this section but they are also implemented in this code sample with an option to Store (save) the current values in PlayerPrefs, an option to load the values from PlayerPrefs into the game variables and an option to clear the PlayerPrefs.
void Start()
{
StatGUIGO = GameObject.Find("Main Camera");
stat = StatGUIGO.GetComponent<StatGUI>();
if (PlayerPrefs.GetString("WizardGame") != "Merlin")
setDefaults();
}
The start method gets the stat object because that is where the array of data is stored. It then checks to see if my master key for this data set exists. If not, we’ll assign a set of default values. In this case, I’m using the data in the array as the default values. If the array doesn’t exist, it implies that the stats object doesn’t exist so there would be nothing to work with.
private void setDefaults()
{
PlayerPrefs.SetString("WizardGame", "Merlin");
PlayerPrefs.SetFloat("Level", stat.data[0].Amount);
PlayerPrefs.SetFloat("Won", stat.data[1].Amount);
PlayerPrefs.SetFloat("Lost", stat.data[2].Amount);
PlayerPrefs.SetFloat("Kills", stat.data[3].Amount);
PlayerPrefs.SetFloat("deaths", stat.data[4].Amount);
PlayerPrefs.SetFloat("kdr", stat.data[5].Amount);
}
The OnGUI method functioning is readily apparent. The Clear button is a call to the PlayerPrefs delete all function. That is directed to the disk location the PlayerPrefs are stored in. It uses the data from the Unity player information. It finds the folder with the company name and the subfolder with the product name and deletes them. It does not delete all PlayerPrefs in the system. You do need to be aware that if you leave the default company and default name values in player information, there is a chance that PlayerPrefs from one game can overwrite data from another game.
private void OnGUI()
{
if (GUI.Button(new Rect(20, 20, 100, 50), "Store"))
{
storeData();
}
if (GUI.Button(new Rect(20, 70, 100, 50), "Load"))
{
loadData();
}
if (GUI.Button(new Rect(20, 120, 100, 50), "Clear"))
{
PlayerPrefs.DeleteAll();
}
}
Small sections of the load and save methods are shown below.
private void loadData()
{
stat.data[0].Amount = PlayerPrefs.GetFloat("Level");
. . .
stat.data[5].Amount = PlayerPrefs.GetFloat("kdr");
}
private void storeData()
{
PlayerPrefs.SetString("WizardGame", "Merlin");
PlayerPrefs.SetFloat("Level", stat.data[0].Amount);
. . .
PlayerPrefs.SetFloat("kdr", stat.data[5].Amount);
}
This is the link to the complete scoring code sample. This is the video on Score keeping.
8. Scriptable Objects and Object Pools
Scriptable Objects
A ScriptableObject is a data container that you can use to save large amounts of data, independent of class instances. The main uses for ScriptableObjects are saving and storing data during an Editor session and saving data as an Asset in your Project to use at run time.
- ScriptableObjects can reduce your Project’s memory usage by avoiding copies of values. This is useful if your Project has a Prefab that stores unchanging data in attached MonoBehaviour scripts. Every time you instantiate that Prefab, it will get its own copy of that data. Instead of using the method, and storing duplicated data, you can use a ScriptableObject to store the data and then access it by reference from all of the Prefabs. This means that there is one copy of the data in memory. Just like MonoBehaviour, ScriptableObjects derive from the base Unity object but, unlike MonoBehaviour, you can not attach a ScriptableObject to a GameObject. Instead, you need to save them as Assets in your Project.
When you use the Editor, you can save data to ScriptableObjects while editing and at run time because ScriptableObjects use the Editor namespace and Editor scripting. In a deployed build, however, you can’t use ScriptableObjects to save data, but you can use the saved data from the ScriptableObject Assets that you set up during development.
While scriptable object data does persist throughout a session, scriptable objects do not save data on their own. In the editor, changes made inside play mode will not reset, which might trick you into thinking that the finished game will behave in a similar way. But, in the standalone player, scriptable object data is temporary, and will be lost as soon as the application closes.
This means that, while it may be useful to use scriptable objects to organize the data that you want to save, you’ll still need to use a file save system to actually save information from one session to the next.
ScriptableObject class in UnityEngine inherits from: Object
To create a Scriptable object, you need a script in the asset folder that extends scriptable object. For example:
using Unity Engine;
[CreateAssetMenu(fileName = "Data", menuName =
"ScriptableObj/StatsObject", order = 1)]
public class StatsSObj : ScriptableObject
{
public int enemyCount = 20;
public int score = 20;
public int health = 100;
. . .
}
With the script in the assets folder, right click on Assets, create Scriptable object -> stats object. That causes the object to be created in the assets folder. Because we gave it a file name Data, it will use that name as the default.
We now have a stats object and the initial values are assigned from our script.
Using it in code
- There are 2 basic options: Instantiate ScriptableObject objects with Create Instance.
public StatsSObj stats;
void Start()
{
stats= ScriptableObject.CreateInstance<StatsSObj>();
//if you use create instance, the values must
// be initialized
stats.health = 95;
stats.shotsFired = 0;
- The second option is to assign the SO like an object in the inspector, values carry over
- void UpdateScore(int s)
- {
- stats.score += s;
- if (stats.kills > 0)
- stats.shotsPerKill = (float)stats.shotsFired / stats.kills;
- else
- stats.shotsPerKill = 0;
- }
The completed code sample for this project is available in the code samples, in the Scriptable Demo project. This is the link to the video on scriptable objects.
Object Pooling
- Object pooling is a design pattern that can provide performance optimization by reducing the processing power required of the CPU to run repetitive create and destroy calls. With object pooling, existing GameObjects can be reused over and over. The key function of object pooling is to create objects in advance and store them in a pool, rather than have them created and destroyed on demand. When an object is needed, it’s taken from the pool and used, and when no longer needed, it’s returned to the pool rather than being destroyed.
The pooling technique is not just useful for reducing CPU cycles spent on instantiation and destroy operations. It also optimizes memory management by reducing the overhead of object creation and destruction, which requires that memory be allocated and deallocated, and constructors and destructors called.
You could also consider the use of object pooling as a possible process for limiting a rate of fire. If the rate of fire is too high or continuous, they may run out of pooled objects and have to wait for items to be returned to the pool. Effectively, they’ve run out of ammo and have a delay while reloading.
Object Pools are primarily used for performance: in some circumstances, object pools significantly improve performance when a project is creating and destroying the same GameObject repeatedly in rapid succession.
Like any optimization technique, there may be times to avoid pooling. Since the objects have been used, they may be in an inappropriate state. A reset operation may be necessary that requires several CPU ticks. You need to be aware that creating too many of them may require significant amounts of memory. It does add some complexity to the code.
This script makes an object pool of game objects. In the actual project, bullet prefabs are attached as the object to pool.
public class ObjectPool : MonoBehaviour
{
public static ObjectPool SharedInstance;
public List<GameObject> pooledObjects;
public GameObject objectToPool;
public int amountToPool;
void Awake()
{
SharedInstance = this;
}
void Start()
{
pooledObjects = new List<GameObject>();
GameObject tmp;
for (int i = 0; i < amountToPool; i++)
{
tmp = Instantiate(objectToPool);
tmp.SetActive(false);
pooledObjects.Add(tmp);
}
}
public GameObject GetPooledObject()
{
for (int i = 0; i < amountToPool; i++)
{
if (!pooledObjects[i].activeInHierarchy)
{
return pooledObjects[i];
}
}
return null;
}
}
This is a sample shooting method that uses the pooled objects. Notice that instead of instantiating the objects, we are getting either the object or a null from the pool Then we assign a position and rotation and finally set it active.
private void shoot()
{ // Instantiate(shot, shotSpawn.position, shotSpawn.rotation);
GameObject bullet = ObjectPool.SharedInstance.GetPooledObject();
if (bullet != null)
{
bullet.transform.position = shotSpawn.transform.position;
bullet.transform.rotation = shotSpawn.transform.rotation;
bullet.SetActive(true);
}
}
Another consideration about pooled objects. In the past, we have set our bullets to destroy themselves after a period of time if they haven’t hit anything. That would not work here because we don’t want to destroy the items. Instead, you may want to put a timing process in the bullet or mover class, perhaps something like this:
elapsed += Time.deltaTime;
if (elapsed > 3)
{
// Destroy(this.gameObject);
// instead of destroying the object, we want to return it
// to the pool so we'll set its state to inactive. Then because
// not every bullet makes contact, we also need to set the elapsed
// time to 0
gameObject.SetActive(false);
elapsed = 0;
}
It is critical to reset the elapsed time to 0. If not, the next time the object is activated, it will deactivate itself. This sample only made 10 pooled objects and the reset was simple enough to not require a method to execute the process.
The completed code sample for this project is available in the code samples, in the Object pool project. This is the video for object pooling.
9. Save and Load Systems
Saving data and loading that saved data are two functions that make a game more interesting because it gives us a chance to have bragging rights. We can show what we've done. We can restore ourselves from previous positions. There are going to be several processes that we're going to look at for saving and loading our data. We're going to talk about basic text files and formatted files, they're either going to be XML or JSON and then we're also going to talk about not using the binary formatter.
Serialization is the process of converting the state of an object into a form that can be persisted or transported. The complement of serialization is deserialization, which converts a stream into an object. Together, these processes allow data to be stored and transferred. What you commonly refer to as reading and writing is actually deserialization and serialization.
The .NET framework features the several serialization technologies including JSON. JSON serialization maps .NET objects to and from JavaScript Object Notation (JSON). JSON is an open standard that's commonly used to share data across the web. The JSON serializer serializes public properties by default and can be configured to serialize private and internal members as well.
There are going to be some cases where using PlayerPrefs is going to be sufficient saving a player's name or a score going from 1 scene to the next where they're going to be playing on the same computer. It's going to be perfectly fine but if we're trying to save more complex information, if we're trying to save inventories or significant game state we're going to have to evolve to using a file system.
As we start looking at our file systems, one thing to make sure you understand is that none of these systems are secure. They're not encrypted. If we're using a text file then we're looking at something that is displayable in Unicode. If we're looking at JSON or XML files we're looking at something that is again is a string or a text file in Unicode plus it has labels and if we're looking at something that's been done with binary formatter while it's harder to read, we can open it up and look at the hexadecimal code.
When we're talking about where are we going to save the file, saving it in the assets folder with Application.dataPath makes it convenient. Saving it in the assets folder means that when we make a copy of the game and we give it to a friend, that file goes along with it. if we were to save it in Application.persistentDataPath, that is in your one of your hidden folders in app data and while it's easily accessible for you on your machine, it is not portable and doesn’t transfer when you make a copy of the game.
Text Files
The first file processing system we're going to discuss is reading and writing text files using a stream reader and stream writer.
The StreamReader and the StreamWriter are built into C# and are readily accepted and work fine within Unity. Unity gives us two basic locations by default for saving our files, one of them is known as Application.dataPath and the other is Application.persistentDataPath. The application data path gives us a qualified name the leads to the asset folder location. Persistent data path gives us the fully qualified name that leads to one of the hidden system folders called appdata and inside there it's in the local low folder with the company name and the game name that comes from the editor.
sr= new StreamReader(Application.dataPath+”/”+filename);
sw = new StreamWriter(Application.persistentDataPath + "\\"+ "scoreFile.txt");
Notice on those two samples from the StreamReader and the StreamWriter using Application.dataPath and Application.persistentDataPath that we have the slash. We have the slashes to distinguish between the path name, which is the sequence of folders and the file name. Without those slashes, it all gets concatenated into one name and that could cause a problem.
Another thought to consider, when you're doing file processing, is to ensure the file exists before you start your game. The script I have below is a nice simple way to see that the file exists and, if it doesn't exist, it handles that case by creating a set of default values for the file.
I attached my file checker script attached to camera.
void Start ()
{
if(!File.Exists(Application.persistentDataPath + "\\" + "scoreFile.txt"))
{
StreamWriter sw = new StreamWriter(Application.persistentDataPath + "\\"
+ "scoreFile.txt");
sw.WriteLine("A 9");
. . .
sw.WriteLine("E 5");
sw.Close();
}
To read a file using StreamReader, we need to know the structure of the file. We need to know how many elements per line, the type of each and the delimiters between the elements. While we don't need to know the number of lines, we do also need to realize that everything we read is going to be treated as a string and we're going to have to parse it or assign it to different data types. Consider this example.
void ReadFile(string file )
{
StreamReader sr;
try
{
sr = new StreamReader(Application.dataPath +”\\”+ file);
int kills = Convert.ToInt32(sr.ReadLine());
int deaths = int.Parse(sr.ReadLine());
…
float x = float.Parse(sr.ReadLine());
float y = Convert.ToSingle(sr.ReadLine());
float z = Convert.ToSingle(sr.ReadLine());
Player.transform.position = new Vector3(x, y, z);
}
catch (FileNotFoundException)
{
. . .
}
finally
{
if (sr != null)
sr.Close();
}
}
In this code sample, we're going to declare the StreamReader and then inside the try block we instantiate it and assign application data path and the file name. This file is structured with five elements, each on their own line, so we'll read each line individually. The first variable, kills, is an int and we’ll use Convert.ToInt32 to convert it from a string to an int. The next variable, deaths, is another int that is converted by int.Parse. Either of those methods are acceptable. For the float we have 3 float values are going be used for X, Y, Z in the vector3. Those floating point values can be converted using either float.Parse or Convert.ToSingle. Once we have those values, we create the vector3. The catch catches a file not found exception. There are many others we could catch and I didn't bother to go through but we would have some sort of relevant code there. We're going to have other catches and then ultimately we'll have a finally to make sure that our file is closed. Remember the finally always runs whether it a catch thrown or not.
The first sample for using StreamReader is fairly straightforward. It was a simple file to read with five items with one per line. We didn't have to worry about running a loop or doing anything other than parsing the data. Now we need to explore a few more concerns.
The example I'm going to use is a simple case of reading a data file and using it to assign height values into a Unity height map for terrain. The data is a set of normalized floating point values, set for one of the Unity terrain resolutions. I’m using the 513 by 513 mapping resolution. That means that there are 513 floats per row with 513 rows. Just like a StreamReader reads a text file it can also read other files in different formats. This format is a comma separated value file (.CSV). I choose a CSV file because the raw data is easier to handle in an Excel spreadsheet and .CSV is one of the formats Excel can use. And yes, my spreadsheet was 513 rows x 513 columns of floating point values.
StreamReader sr = new StreamReader(Application.dataPath + "/" + file);
// char[] delim = { ',', ':', ';', ' ' };
for (int x = 0; x < 513; x++)
{
string line = sr.ReadLine();
string[] datapt = line.Split(',');
// string[] datapt = line.Split(delim);
for (int y = 0; y < 513; y++)
{
heights[x, y] = float.Parse(datapt[y]);
}
}
sr.Close();
This code is actually extracted from a method and I omitted the exception handling for simplicity. The StreamReader uses Application.data-Path with the file name. We’ll loop thru the 513 lines and each one of them is going to be split into 513 individual elements per row. That's is performed in line.Split(‘,’). The delimiter is a character with the comma in the middle. We could actually make that a array of delimiters and have several different values to use for our split. There is a sample of a delimiter array, commented out, at the top of the sample. The char [] delim creates a character array and it has the characters for a comma, a semi colon, a colon and a space. Instead of splitting with the hard coded comma, we could use
string[] datapt = line.Split(delim);
The choice of a single delimiter or an array of delimiters depends on how your data file is designed. One of the considerations I had when I looked at using a stream reader for this is the size of the data it would be hard to edit in a text editor and with a CSV file I can open that with an excel spreadsheet and it makes it very easy to edit in that mode.
As we look at writing files we're going to use StreamWriter and I'll usually call mine SW just to keep it simple and easy to remember. The next step in the process is going to be to create the StreamWriter. StreamWriter handles a couple of processes for us. This is a basic StreamWriter declaration, identifying the path and the file name. The StreamWriter will determine if the file exists. If the file exists, it's going to overwrite the data.
void WriteToFile(string file )
{
StreamWriter sw = new StreamWriter(directory + “//”+file);
{
sw.WriteLine(PlayerPrefs.GetInt("kills").ToString());
sw.WriteLine(deaths);
sw.WriteLine(gold));
…
sw.WriteLine(Player.transform.position.x);
sw.WriteLine(Player.transform.position.y);
sw.WriteLine(Player.transform.position.z);
}
If the file doesn't exist, it will create a new file for us, so we can’t experience file not found exceptions. There is a variation of StreamWriter, with a second parameter. If the parameter is true,
StreamWriter sw = new StreamWriter(directory + “//”+file, true);
then the StreamWriter will append the data to an existing file and again, if the file doesn’t exist, it creates the file.
The StreamWriter is going to take all of our data from wherever it is stored and write it as strings. StreamWriter isn't going to be concerned on the source of the data. I've got the first one being from PlayerPrefs and I've added a ToString to show that we're forcing a conversion from the int to the string but the WriteLine works in StreamWriter like the WriteLine does for a console and it automatically converts the ints and floats to strings so we don't have to explicitly call the ToString method for those. If we're working with something within Unity that uses a text or a text object then we may explicitly specify ToString.
File issues
There are some issues that you need to be aware of whenever you're working with files. We've talked about exception handling and there are any number of exceptions that we can address and we can write our own customized exceptions for if we need to. One concern is to make sure that you always close your file when you're done with it. Because of the way the file system and/or the operating system is designed, we could have a file being read by multiple people and it'll still maintain its state of reliability because it's not open for changes/writing. But if one goes to open it for writing, then only that one person having write privileges can have it open. All of the writing details aren't actually finalized until such time as the file is closed. Without closing the file, we have potential memory leaks, we have issues with data not being finally written, and we also lock every anyone else out from getting access to the file.
Using StreamReader/StreamWriter
In an attempt to help users remember to close the file, they've developed a command called using and we can have a using of the StreamWriter or using of a StreamReader. It's going to include the declaration and at the end it's going to automatically close the file for us. We do have to be careful how we structure it to make sure that the exceptions are addressed.
StreamReader sr = new StreamReader("TestFile.txt")
try
{
using (sr)
{ . . . }
}
But does not incorporate exception handling. Still need to include try & catch blocks to handle errors
XML save/load system
This section addresses saving and loading data with an XML file system. An XML file allows you to get view details on your saved data. An XML file is made up of tagged lines that allow you to load or save specific data for easier usage. XML files can be created in Visual Studio or applications such as notepad. Next, we start adding our tags. First, we make the PlayerData XML file. In PlayerData we're going to save three sets of parameters. We're going to save the X, Y, Z of the position, the X, Y, Z of the rotation and the X, Y, Z of the scale. Notice the tags look like HTML with the start <xPos> and then the end being </xPos> . XML gives us more flexibility than HTML because we can actually declare all of our tags ourselves whatever we want to use.
<pData>
<xPos></xPos>
<yPos></yPos>
<zPos></zPos>
<xRot></xRot>
<yRot></yRot>
<zRot></zRot>
<xScale></xScale>
<yScale></yScale>
<zScale></zScale>
</pData>
In this sample file, <pData> is the root node. As the root, it's the anchor for the rest of our data. The rest of the nodes will hold our individual pieces of data. The actual order that we list these nodes is not relevant because we're giving them tags and we can access them in whatever order we desire. The code sample will demonstrate that, so there is more flexibility than with a text file but again we have a little more overhead and that we have to create our templated data first.
Next we will create a data file for our enemy and we're going to call it eData. The enemy has a name, a position, a rotation, and a scale. Then we'll add a second enemy, so we repeat that data. Now we have a template that is designed to handle 2 enemies. Later we'll look at how to create a file that will scale dynamically to handle a set of any number of enemy.
<eData>
<enemy>
<name></name>
<xPos></xPos>
<yPos></yPos>
<zPos></zPos>
<xRot></xRot>
<yRot></yRot>
<zRot></zRot>
<xScale></xScale>
<yScale></yScale>
<zScale></zScale>
</enemy>
<enemy>
<name></name>
…
<zScale></zScale>
</enemy>
</eData>
In the file, eData is the root node and the anchor for the other nodes. Those enemy nodes hold the rest of the nodes for each particular enemy. The first enemy processed will be store in the first enemy set. The enemy name, position, rotation and scale parameters will be stored and then the process continues for the next enemy. and then we're going to end now for his enemy then we're going to repeat the process for the next enemy.
You can consider the enemy to be a class and the child nodes within it as the class's properties. The reason we are doing this is to save multiple enemies to our XML file, and they will each have their own data.
XML files
Be sure to add the following using statements to your code.
using System.Xml;
using System.Xml.Serialization;
using System.IO;
using System.Text;
The XML documents are the data structure that will hold our information. The first thing we have to do is we have to make our documents and then we're going to load our information into it. The XML file processing is broken down into several sections. First we'll talk about making the initial documents. Now that we have our basic data files made, we’ll load the player and then save the player. Then we'll return to do our enemy processing. Here in our beginning document. In our save system script, we make a player document and then we have a public string for the file name and a public string for the directory. Those will be assigned in the inspector. Then we also have our game object for the player. As you can see on the inspector, I've designated my player file as a template to playerdata.xml and later on we'll use our enemy file which is the template enemy data so those are our two template files that will use and load into our XML document. Notice the directory is identified as AllXmlData. The one line of code that is in start, is the line that will load the entire template for the player data file into our XML document. Then we can use it.
public class XML_Save_System : MonoBehaviour
{
XmlDocument xPlayer = new XmlDocument();
public string pFileName = "";
public string xDirectory;
public GameObject Player;
void Start()
{
xPlayer.LoadXml(File.ReadAllText(xDirectory + @"\" + pFileName));
. . .
}
The first process will be a walk through of the SavePlayer method. The SavePlayer method is activated by pressing a key in the game that the save handler addresses. If that key is pressed, this method is called. As long as the player is not null, we're going to set the root node to the first child, which is pData. Then we'll process each node. As we start, we're working through our document, we take a look at it and because of the way we set up the file we have xPos as the first child node. As we continue, we get our Player.transform.-position.x which is a float. We cast it into a string using the ToString method, assign it to the node’s inner text and then execute the break. We're done with the xPos. The next node is the yPos. We repeat the same process until we have finished. In this sample, that is with the zScale. Notice that when we work with the scale, it's player transform local scale. Finally, we have processed the data and we are ready to save it. We're going to save it in the directory with data appended in front of the file name. Remember the save directory is the AllXmlData and the file name is now dataTPlayerData.xml.
In this image you can see over on the right hand side on the solution explorer, Visual Studio has created our folder for us, AllXmlData, and we do have some of their other files already in there because of the way I have things set up.
But here we see our data for our player is no longer just an empty template, it actually has values that have been realized.
void SavePlayer()
{
print("received message write player xml");
if (Player != null)
{
XmlNode root = xPlayer.FirstChild;
foreach (XmlNode node in root.ChildNodes)
{
switch (node.Name)
{
case "xPos":
node.InnerText = Player.transform.position.x.ToString();
break;
case "yPos":
node.InnerText = Player.transform.position.y.ToString();
break;
case "zPos":
node.InnerText = Player.transform.position.z.ToString();
break;
case "xRot":
node.InnerText = Player.transform.rotation.x.ToString();
break;
node.InnerText = Player.transform.rotation.y.ToString();
break;
case "zRot":
node.InnerText = Player.transform.rotation.z.ToString();
break;
case "xScale":
node.InnerText = Player.transform.localScale.x.ToString();
break;
case "yScale":
node.InnerText = Player.transform.localScale.y.ToString();
break;
case "zScale":
node.InnerText = Player.transform.localScale.z.ToString();
break;
}
}
xPlayer.Save(xDirectory + @"\" + "data" + pFileName);
}
}
Reading and Loading the player from an XML file
As we get ready to load the player from our previously saved file, the first thing I'm going to do just for demonstration purposes, in this sample code, is I'm going to zeroize all of the positions so that you can tell that the new position or new values have been properly inserted. Then we'll work through the assignment to the nodes.
As we proceed through loading the player from file, it's the reverse process of when we're saving our data. We still start off with the first child of our XML document. That's the first child of the xPlayer and then we just use the switch statement, switching on the node name. Now our case values are items like xPos or yPos. From each node, we get the inner text, which is a string. Then we convert the text into floating point values. As we perform the conversions, you can use the Convert.ToSingle, or you can use float.Parse or you can use float.tryParse to address exception handling. Once we finished processing the data file, all of the variables that we need to recreate our transform have been assigned. Notice that the position and localScale transforms are Vector3s while the transform for rotation is a set of values making a quaternion. The quaternion uses the x, y and z values and adds in the 0 for the w parameter. So that is a quick and easy way to load a known element like a single player and then later on recreate it from a file.
void LoadPlayer()
{
print("received message load player xml");
float xPos = 0.00f; float yPos = 0.00f; float zPos = 0.00f;
float xRot = 0.00f; float yRot = 0.00f; float zRot = 0.00f;
float xScale = 0.00f; float yScale = 0.00f; float zScale = 0.00f;
if (Player != null)
{
XmlNode root = xPlayer.FirstChild;
foreach (XmlNode node in root.ChildNodes)
{
switch (node.Name)
{
case "xPos":
xPos = float.Parse(node.InnerText);
break;
case "yPos":
yPos = Convert.ToSingle(node.InnerText);
break;
case "zPos":
zPos = Convert.ToSingle(node.InnerText);
break;
case "xRot":
xRot = Convert.ToSingle(node.InnerText);
break;
case "yRot":
yRot = Convert.ToSingle(node.InnerText);
break;
zRot = Convert.ToSingle(node.InnerText);
break;
case "xScale":
xScale = Convert.ToSingle(node.InnerText);
break;
yScale = Convert.ToSingle(node.InnerText);
break;
case "zScale":
zScale = Convert.ToSingle(node.InnerText);
break;
}
Player.transform.position = new Vector3(xPos, yPos, zPos);
Player.transform.rotation = new Quaternion(xRot, yRot, zRot, 0.00f);
Player.transform.localScale = new Vector3(xScale, yScale, zScale);
}
}
Saving the enemy
As we look to save the enemy, one of the things that we're not always going to be sure about is how many enemies are going to exist. There might be one or two or there might be 15 or 20. In that set of circumstances, we can't have a preset data structure for our enemy files. We need to add a couple more elements to the program. There is an XML document for our xEnemy.
XmlDocument xEnemy = new XmlDocument();
public string eFileName = "";
public GameObject[] Enemies;
We have a file name and added in a array of game objects called enemies. The first step in saving our enemy is removing any existing nodes in the xEnemy document using xEnemy.RemoveAll(). Next we’ll create a new node for the root node, XmlNode eRoot and then create an array of all of node names, one for each data element that we want to save, xPos, yPos, zPos, etc. The next thing to notice in SaveEnemies () is the array of enemies. I'm not going to go into how I got it right now. It could be from knowing I have exactly two or three enemies or it could be from finding game objects with tag. The 1st loop traverses the enemy array. As long as the particular enemy is not null, we create a new XML node called eBase. Now that we have the eBase, the next loop traverses the array of node names, creating new nodes, with its appropriate name, that is then appended as a child to the eBase.
Next we traverse the child nodes in the eBase that we’ve just created, loading the data from the enemy object into the nodes of the current eBase. At the end of each pass thru the switch statement, we append that node to the eRoot, as a child of the eBase. When we finish the process for 1 enemy object, we append that as a child to the xEnemy. We continue with this process until all the enemy data has been collected and stored. Then we save the xEnemy document in the AllXmlData folder in the file dataTEnemyData.xml.
Loading Enemy from an XML file
As we look at loading the enemy from our XML file, other than the fact that we have a potential array of enemies, it is virtually identical to the process that we use for loading a single player. The only significant change is looping through the data, once for each enemy. To distinctly address the enemy, they are indexed in the array. After we have retrieved an enemy’s compete set of data, we will assign its name and create and assign the values for the transforms.
void LoadEnemies()
{
for (int e = 0; e < Enemies.Length; e++)
{
if (Enemies[e] != null)
{
XmlNode eData = xEnemy.FirstChild;
XmlNode enemy = eData.ChildNodes[e];
if (enemy.Name == "enemy")
{
foreach (XmlNode eNode in enemy.ChildNodes)
{
switch (eNode.Name)
{
case "name":
name = eNode.InnerText;
break;
case "xPos":
xPos = Convert.ToSingle(eNode.InnerText);
break;
case "yPos":
yPos = Convert.ToSingle(eNode.InnerText);
break;
//. . .
case "zRot":
zRot = Convert.ToSingle(eNode.InnerText);
break;
//. . .
case "zScale":
zScale = Convert.ToSingle(eNode.InnerText);
break;
}
Enemies[e].name = name;
Enemies[e].transform.localPosition =
new Vector3(xPos, yPos, zPos);
Enemies[e].transform.localRotation =
new Quaternion(xRot, yRot, zRot, 0.00f);
Enemies[e].transform.localScale =
new Vector3(xScale, yScale, zScale);
}
}
}
}
}
Save handler
There are a couple of different approaches that we can use for save handling. There are advantages and disadvantages that you need to weigh. There is also the perspective, are you the designer and setting requirements or are you attempting to make it more user friendly. You could allow the player to save the game whenever desired or you use a checkpoint system. You may decide that once they crossed the checkpoint, the data is saved and the checkpoint is disables so they can’t use that one again. You could design it so the save is only executed with quitting the game, i.e. Application.Quit. A consideration could also involve how much data is being stored/loaded and the impact on the game if you allow save on demand. That's going to be determined by you as a game designer on how you want to address it.
Checkpoint. If you decide to implement a checkpoint system one of the approaches may be using a Collider on a checkpoint. When they intersect with the Collider, that triggers the save function. After the save function has been completed, consider destroying the checkpoint, so it can't be reused. The little code sample I've got below does exactly that. They trigger the Collider on the save point, and we send a message. I'm sending it to the camera, where my save script is located, to write the file and then I destroy the game object which destroys the Collider.
Note that this function also destroys the trigger object so that the player can't reactivate the checkpoint
void OnTriggerEnter(Collider other)
{
if(other.tag == "SavePoint")
{
Camera.main.SendMessage("WriteToFile");
Destroy(other.gameObject);
}
}
Save handler on demand
- Another option for saving is “save on demand” where you allow the player to save whenever they want by pressing a key or a key combination. You just have to be careful that you set it up so they cannot abuse the system.
The completed code sample for this project is available in the code samples, in the Save and Load project. This is the video for using text and xml files.
JSON JavaScript Object Notation
JSON (JavaScript Object Notation) is a annotated format for representing structured data similar to HTML and XML. Although JSON grew out of the JavaScript programming language, it is widely used for exchanging data between systems. Most current APIs address JSON. JSON data is always a string. These strings can be decoded into a range of 6 basic data types, strings, numbers, booleans, arrays, null and objects. This means object hierarchies and relationships can be preserved during save/load operations. There are some drawbacks to using JSON. JSON data can't include comments, fields can't reference other values in the data structure and JSON can't natively store dates, times, or geolocation points.
In C#, JSON is often used to serialize and deserialize objects. Serialization converts objects to JSON format, while deserialization converts JSON data into objects. C# provides robust libraries like System.Text.Json to handle these tasks efficiently.
The System.Text.Json namespace provides functionality for serializing to and deserializing from JavaScript Object Notation (JSON). Serialization is the process of converting the state of an object, that is, the values of its properties, into a form that can be stored or transmitted. The serialized form doesn't include any information about an object's associated methods. Deserialization reconstructs an object from the serialized form.
This sample is designed to demonstrate the use of JSON in C# without Unity. It involves a main class and 2 additional classes, Employee and Pet. Each Employee has a list of pets. Before we start with the code we need to make sure that we have a using statement for System.Text.Json in the using statements and you may also need to add it to the references.
As we look at both classes, a couple of things need to be emphasized. First, the classes are marked serializable, that allows JSON to serialize and deserialize them. Second, we have public properties with get and set for all of our class variables. Third, we have default constructors or if you prefer parameterless constructors that are going to allow the deserialization to work properly. The overloaded constructors are the standard ones that we've seen before, with the various parameters. I've left the ToString function in there from C# so we can compare the ToString results to the JSON results.
In the first part of main, that appears below, we're executing the basic process of making a list of employees, making a list of pets, and then creating the employees and adding them to the list. That's basic C# and there should be nothing unusual about that. I use Console.WriteLine of e & e2 to the contents using to the ToString method.
List<Employee> elist = new List<Employee>();
List<Pet> p1 = new List<Pet>();
List<Pet> p2 = new List<Pet>();
Pet p = new Pet("Fifi", "dog");
p2.Add(p);
p2.Add(new Pet("Fluffy", "cat"));
Employee e = new Employee("Bob", "12345678", 50000, p1);
Employee e2 = new Employee("Alice", "23951111", 70000, p2);
elist.Add(e);
elist.Add(e2);
Console.WriteLine(e);
Console.WriteLine(e2);
In this next section of code we create a StreamWriter and assign the file people.json to it. By default that's going to be stored in the bin debug folder. If the people.json file doesn’t exist, the StreamWriter will create it. The next line is where we can set our JSON options with the JSON serializer options. There are about 20 different options to choose from but the one I’ve selected is WriteIndented. When we see it in Unity, Unity will call it prettyPrintJson. It provides a formatting option. The data is displayed in a line if WriteIndented is false. If it is true then we're going to see indents in the file with a tree like structure. There are three data lines, using employee e, employee e2 and then the list. Next they are written using the StreamWriter and then we also print them to the console so we can compare them.
StreamWriter sw = new StreamWriter("people.json");
JsonSerializerOptions options =
new JsonSerializerOptions{ WriteIndented =false };
string stringData = JsonSerializer.Serialize(e, options);
string stringData2 = JsonSerializer.Serialize(e2, options);
string stringData3 =
JsonSerializer.Serialize(elist, options);
sw.WriteLine(stringData);
sw.WriteLine(stringData2);
sw.WriteLine(stringData3);
Console.WriteLine("j1 " + stringData);
Console.WriteLine("j2 " + stringData2);
Console.WriteLine();
Console.WriteLine("j3 " + stringData3);
sw.Close();
In the next image you can see what the JSON file looks like when it's in the data file itself. Again that's with indented/formatting turned off. Data3, the contents of the entire list have been truncated for purposes of readability.
j1 {
"Name": "Bob",
"NUM": "12345678",
"Salary": 50000,
"Pets": []
}
j2 {
"Name": "Alice",
"NUM": "23951111",
"Salary": 70000,
"Pets": [
{
"Name": "Fifi",
"Type": "dog"
},
{
"Name": "Fluffy",
"Type": "cat"
},
]
}
j3 [
{
"Name": "Bob",
"NUM": "12345678",
"Salary": 50000,
"Pets": []
}
"Name": "Alice",
"NUM": "23951111",
"Salary": 70000,
"Pets": [
{
//...
The image on the left depicts what the data file looks like when WriteIndented is turned on. You can see a structure that is very easy to read with everything labeled and we have our subarrays divided into components. The details are laid out.
For this next section, WriteIndented will be reset to false and the program rerun to create the unformatted data file. Using the ReadLine of StreamReader it will read only a single line at a time. That won’t work well for the formatted data set. Part of the issue is the method that I used for this sample with the different objects being serialized individually. With a different structure I could save all of the objects and deserialize them.
In the last section of code the StreamWriter was closed. I’ve omitted it here but you will see in the actual code sample that all of the objects are null and the list empty. StreamReader is used to open the file and
StreamReader sr = new StreamReader("people.json");
string data1 = sr.ReadLine();
e = JsonSerializer.Deserialize<Employee>(data1, options);
Console.WriteLine(e);
string data2 = sr.ReadLine();
e2 = JsonSerializer.Deserialize<Employee>(data2, options);
Console.WriteLine(e2);
string data3 = sr.ReadToEnd();
Console.WriteLine(data3);
elist = JsonSerializer.Deserialize<List<Employee>>(data3,
options);
Console.WriteLine("de 0 "+elist[0]);
Console.WriteLine("de 1 "+ elist[1]);
sr.Close();
ReadLine is used to read the individual lines for e and e2 and ReadToEnd is use for the list of data. The deserialization process is generically typed. It requires the data type to be able to convert the data. It also requires the options parameter so it can use the existing data structure. To deserialize data1 and data2 into employees, the Employee type must be used. For data3, since it is a list of employees, that must be specified for that deserializer. As the final step, close the reader.
Name=Bob NUM=12345678 Salary=500000
Name=Alice NUM =23951111 Salary=70000 pet name=Fifi is a dog pet name=Fluffy is a cat
[{"Name":"Bob"," NUM ":"12345678","Salary":50000,"Pets":[]},[{"Name":"Alice"," NUM ":"23951111","Salary":70000,"Pets":[{"Name":"Fifi","Type":"dog"},{"Name":"Fluffy","Type":"cat"}]}]
de 0 Name=Bob NUM =12345678 Salary=500000
de 0 Name=Alice NUM =23951111 Salary=70000 pet name=Fifi is a dog pet name=Fluffy is a cat
C:\Users\regsi\Documents\_KSU\_unity book material\JsonDemo\bin\Debug\JsonDemo.exe (process 16360) exited with code 0 (0x0).
Press any key to close this window . . .
This image depicts the deserialized data and that the objects are recreated. Bob and Alice are restored with their names, salaries and pets and the entire list was deserialized and with one command. Element 0 and element 1 of the list are shown. Next we will be talking
about using Unity’s JSON functionalities and see how those work for us
The completed code sample for this project is available in the code samples, in the JSON demo C# project. Here is the video for JSON in C#.
JSON Unity
Now that we have an understanding of JSON in C#, we’re going to explore how to use JSON in Unity. Unity has a JSON Utility that has 3 functions:
- JsonUtility.ToJson that converts an object into a JSON string by generating a JSON representation of the public fields of an object
- JsonUtility.FromJson<T> that converts a JSON string into an object
- FromJsonOverwrite; that takes an existing object and rewrites its data values
The following descriptions are extracted from the unity documentation. The ToJson function is overloaded. There is a method with a single parameter:
public static string ToJson(object obj);
and one with 2 parameters:
public static string ToJson(object obj, bool prettyPrint);
By default, the output is generated in minimum size (pretty print is false). But if pretty print is set to true, you get the complete indented format that should be familiar from C#’s version.
Internally, this method uses the Unity serializer; therefore the object you pass in must be supported by the serializer: it must be a MonoBehaviour, ScriptableObject, or plain class/struct with the Serializable attribute applied. Unsupported fields will be ignored, as will private fields, static fields, and fields with the NonSerialized attribute applied.
using UnityEngine;
public class PlayerState : MonoBehaviour
{
public string playerName;
public int lives;
public float health;
public string SaveToString()
{
return JsonUtility.ToJson(this);
}
// Given:
// playerName = "Dr Charles"
// lives = 3
// health = 0.8f
// SaveToString returns:
// {"playerName":"Dr Charles","lives":3,"health":0.8}
}
If the object contains fields with references to other Unity objects, those references are serialized by recording the InstanceID for each referenced object. Because the Instance ID acts like a handle to the in-memory object instance, the JSON string can only be deserialized back during the same session of the Unity engine.
Note that while it is possible to pass primitive types to this method, the results may not be what you expect; instead of serializing them directly, the method will attempt to serialize their public instance fields, producing an empty object as a result. Similarly, passing an array to this method will not produce a JSON array containing each element, but an object containing the public fields of the array object itself (of which there are none). To serialize the actual contents of an array or primitive type, it is necessary to wrap it in a class or struct. You should not alter the object that you pass to this function while it is still executing.
public static T FromJson(string json);
Create an object from its JSON representation.
Again, internally, this method uses the Unity serializer; therefore the type you are creating must be supported by the serializer. It must be a plain class/struct marked with the Serializable attribute. Fields of the object must have types supported by the serializer. Fields that have unsupported types, as well as private fields or fields marked with the NonSerialized attribute, will be ignored.
using UnityEngine;
[System.Serializable]
public class PlayerInfo
{
public string name;
public int lives;
public float health;
public static PlayerInfo CreateFromJSON(string jsonString)
{
return JsonUtility.FromJson<PlayerInfo>(jsonString);
}
// Given JSON input:
// {"name":"Dr Charles","lives":3,"health":0.8}
// this example will return a PlayerInfo object with
// name == "Dr Charles", lives == 3, and health == 0.8f.
}
Only plain classes and structures are supported; classes derived from UnityEngine.Object (such as MonoBehaviour or ScriptableObject) are not. Note that classes derived from MonoBehaviour or ScriptableObject can be used with JsonUtility.FromJsonOverwrite as an alternative.
If the JSON representation is missing any fields, they will be given their default values (i.e. a field of type T will have value default(T). It will not be given any value specified as a field initializer, as the constructor for the object is not executed during deserialization).
If the input is null or empty, FromJson returns null.
public static void FromJsonOverwrite(string json, object objectToOverwrite);
Overwrite data in an object by reading from its JSON representation.
This method is very similar to JsonUtility.FromJson, except that instead of creating a new object and loading the JSON data into it, it loads the JSON data into an existing object. This allows you to update the values stored in classes or objects without any allocations.
using UnityEngine;
public class PlayerState : MonoBehaviour
{
public string playerName;
public int lives;
public float health;
public void Load(string savedData)
{
JsonUtility.FromJsonOverwrite(savedData, this);
}
// Given JSON input:
// {"lives":3, "health":0.8}
// the Load function will change the object on which
// it is called such that
// lives == 3 and health == 0.8
// the 'playerName' field will be left unchanged
}
Internally, this method uses the Unity serializer; therefore the object you pass in must be supported by the serializer: it must be a MonoBehaviour, ScriptableObject, or plain class/struct with the Serializable attribute applied. The types of fields that you want to be overwritten must be supported by the serializer; unsupported fields will be ignored, as will private fields, static fields, and fields with the NonSerialized attribute applied.
Any plain class or structure is supported, along with classes derived from MonoBehaviour or ScriptableObject. Other engine types are not supported. If a field of the object is not present in the JSON representation, that field will be left unchanged.
using System;
using UnityEngine;
[Serializable]
public class PlayerScore
{
public string name;
public int score;
public Vector3 pos;
// public Transform trans;
public string ToJson()
{
bool prettyPrintJson = true;
return JsonUtility.ToJson(this, prettyPrintJson);
}
public override string ToString()
{
return name + " " + score + " " + pos.ToString();
}
}
Here's a more complete example of simple JSON IO. Then we'll make a JSON list writer. To start we'll create a simple class called player score. It's serializable and it's has three public variables. It has a string for a name and int for score and a vector3 for a position. There is one method that will generate a JSON string and one that will generate a normal C# string. ToJson creates the JSON string using the boolean to set the formatting. It calls the JsonUtility.ToJson and that's going to return our JSON string containing all of the public members. In a little bit I'll explain why the public transform is commented out. For now we'll have this class that's going to be utilized with those 3 members. The JsonUtility.ToJson method with the prettyPrintJson true is the equivalent of C#’s WriteIndented = true. If you have pretty print set to false then you're going to have the data stored in minified version. The ToString is shown there not because we need it for this method but so that we can compare the two outputs when we're doing our processing.
This segment is the start of our simple JSON IO program. It has a data folder called dataA and the file name is player.json. There is a private instance of the PlayerScore object, the transform, a speed, a file path, and string data. Notice this is in the awake method. The transform is gotten, the player score is instantiated, and then we assign values to the public variables.
using System.IO;
using UnityEngine;
public class SimpleJsonIO : MonoBehaviour
{
public string folderName = "DataA";
public string fileName =
"player.json";
private PlayerScore player1Score;
private Transform t;
private float speed = 5;
string filePath;
string stringData;
private void Awake()
{
t = GetComponent<Transform>();
player1Score = new PlayerScore();
player1Score.name = "matt";
player1Score.score = 800;
player1Score.pos = t.position;
// player1Score.trans = t;
}
In the start method we have a very simple process for saving our file. We use stringData = player1Score.ToJson(); which calls the ToJson method and then we identify our file path as Application.dataPath along with the folder and the file name then call the write text file method.
The write to text method then creates the StreamWriter, writes the data and then closes the writer. You can see the data file on the right side of the page.
void Start()
{
stringData = player1Score.ToJson();
filePath = Application.dataPath + "\\" +
folderName + "\\" + fileName;
WriteTextFile(filePath, stringData);
}
public void WriteTextFile(string pathAndName,
string stringData)
{
StreamWriter sw = new
StreamWriter(pathAndName);
sw.Write(stringData);
sw.Close();
}
{
"name": "matt",
"score": 800,
"pos": {
"x": -1.7400000095367432,
"y": -0.9599999785423279,
"z": 0.0
}
}
If the Transform is uncommented and active, the Instance ID will be printed in the file.
public Transform trans; player1Score.trans = t;
It acts like a handle to the in-memory object instance, the JSON string can only be deserialized back during the same session of the Unity engine. Here you can see the instance ID for the transform on this run is 32582 while on a previous run it was 21347.
{
"name": "matt",
"score": 800,
"pos": {
"x": -1.7400000095367432,
"y": -0.9599999785423279,
"z": 0.0
}
"trans": {
“instanceID”: 32582
}
}
Notice that the Vector3 works without issues. Rather than saving the transforms directly, consider assigning them to Vector3s for storage and then extracting the Vector3s and assigning them to the transform components.
Reading the file uses the StreamReader. Depending on the structure of the data, you may want to use ReadLine or ReadToEnd. In this project, since I only had 1 object and wrote it with pretty print true, I chose to ReadToEnd rather than having to test for the brackets and identifying each line.
The FromJson method needs to know the data type it is working with. In this case, we indicate that it is a PlayerScore object. Then after assigning it to the player1 score, we use the position value to reposition the sphere in the game.
private void Reload()
{
StreamReader sr = new
StreamReader(filePath);
string data = sr.ReadToEnd();
player1Score =
JsonUtility.FromJson<PlayerScore>(data);
t.position = player1Score.pos;
}
To show that the processing works, this script is incorporated into a simple Unity game with a moveable sphere and a couple of GUI controls for selecting file operation options. The reset method sets name to “”, score to 0 and position to (0,0,0). Reload reads the file and assigns the values while the functioning of the save button is shown.
void Update()
{
float dx = Input.GetAxis("Horizontal");
float dy = Input.GetAxis("Vertical");
t.position += new Vector3(dx, 0, dy) *
Time.deltaTime * speed;
}
private void OnGUI()
{
if (GUI.Button(new Rect(20, 20,100,50),
"Reset"))
Reset();
if (GUI.Button(new Rect(20, 80, 100, 50),
"Load"))
Reload();
if (GUI.Button(new Rect(20, 140,100,50),
"Save"))
{
player1Score.pos = t.position;
stringData = player1Score.ToJson();
WriteTextFile(filePath, stringData);
}
}
Handling of lists of objects.
To handle a list of objects, the first step is to create a new serialized class to hold this list. This class is a PlayerScoreList and has a public list of player scores. It has a ToJson method that returns the string representation with pretty print being true so the data will have indentation. Next we will look at the elements that compose the Unity class.
using System.Collections.Generic;
using System;
using UnityEngine;
[Serializable]
public class PlayerScoreList
{
public List<PlayerScore> list = new
List<PlayerScore>();
public string ToJson()
{
return JsonUtility.ToJson(this, true);
}
}
We create a new folder for the data and create the file name as playerList.json. We then instantiate 2 player score objects. In awake, the player score objects are assigned values and then they are added to the list.
public class myJsonListWriter : MonoBehaviour
{
public string folderName = "DataB";
public string fileName = "playerList.json";
private PlayerScore playerScore1 = new
PlayerScore();
private PlayerScore playerScore2 = new
PlayerScore();
private PlayerScoreList playerScoreList = new
PlayerScoreList();
void Awake() {
playerScore1.name = "matt";
playerScore1.score = 800;
playerScore1.pos = new Vector3(2.5f, 2.7f, 4);
playerScore2.name = "joelle";
playerScore2.score = 901;
playerScore2.pos = new Vector3(5.5f,2.1f,-1.9f);
playerScoreList.list.Add(playerScore1);
playerScoreList.list.Add(playerScore2);
}
void Start() {
string stringData = playerScoreList.ToJson();
string filePath = Application.dataPath + "\\"+
folderName + "\\" + fileName;
WriteTextFile(filePath, stringData);
}
In start, for this simple example, we create a StreamWriter, using the pathAndName parameter. We then write the JSON data and close the writer.
The read list method is next. The StreamReader is using Application.dataPath to the asset folder and uses the
public void WriteTextFile(string pathAndName, string stringData)
{
StreamWriter sw = new StreamWriter(pathAndName);
sw.Write(stringData);
sw.Close();
}
public void ReadList()
{
string filePath = Application.dataPath + "\\" +
folderName + "\\" + fileName;
StreamReader sr = new StreamReader(filePath);
string data = sr.ReadToEnd();
PlayerScoreList p2 =
JsonUtility.FromJson<PlayerScoreList>(data);
print(" rl1 " + p2.list[0]);
print(" rl2 " + p2.list[1]);
sr.Close();
}
ReadToEnd method to gather all of the data. Notice in this case that the generic type for the FromJson is the player score list, not just a player score. The 2 print commands are designed to show the results in the console. The list is generated and looks like the image on the left and the console display is at the bottom.
"list": [
{
"name": "matt",
"score": 800,
"pos": {
"x": -2.5,
"y": 2.700000047683716,
"z": 4.0
}
"trans": {
"instanceID": 0
}
},
{
"name": "joelle",
"score": 901,
"pos": {
"x": 5.5,
"y": 2.0999999046325685,
"z": -1.899999976158142
}
"trans": {
"instanceID": 0
}
}
]
The completed code sample for this project is available in the code samples, in the JSON Demo Unity project. Here is the video for JSON in Unity.
Binary Formatter
According to Microsoft, “the BinaryFormatter type is dangerous and is not recommended for data processing. Applications should stop using BinaryFormatter as soon as possible, even if they believe the data they're processing to be trustworthy. BinaryFormatter is insecure and can't be made secure. Starting in .NET 9, the in-box BinaryFormatter implementation throws exceptions on use, even with the settings that previously enabled its use. Those settings are also removed. BinaryFormatter was implemented before deserialization vulnerabilities were a well-understood threat category. As a result, the code does not follow modern best practices. The Deserialize method can be used as a vector for attackers to perform DoS attacks against consuming apps. These attacks might render the app unresponsive or result in unexpected process termination. This category of attack cannot be mitigated with a Serialization Binder or any other BinaryFormatter configuration switch. .NET considers this behavior to be by design and won't issue a code update to modify the behavior.”
10. Audio
In this section, we’ll be working with the Unity audio system, creating several different music and sound effect processes. We’ll create a random play option and a playlist style option for the music player. We will also create a system for playing ambient/natural background sounds. We will explore different processes for the sound effects, including event actuated ones.
A game would be incomplete without some kind of audio, be it background music or sound effects. Unity’s audio system can use most standard audio file formats, play sounds in 3D space, and apply effects including echoes and filtering. Unity can also record audio from any available microphone on your machine for use during gameplay or for storage and transmission.
In real life, objects emit sounds that listeners hear. The way a sound is perceived depends on many factors. A listener can tell roughly which direction a sound is coming from and may also get some sense of its distance from its volume and quality. A fast-moving sound source (such as a falling bomb or a train) changes in pitch as it moves as a result of the Doppler Effect. Surroundings also affect the way sound is reflected. A voice inside a cave has an echo, but the same voice in the open air doesn’t.
To simulate the effects of position, Unity requires sounds to originate from Audio Sources attached to objects. The sounds emitted are then picked up by an Audio Listener attached to another object, most often the main camera. Unity can then simulate the effects of a source’s distance and position from the listener object and play them to you accordingly. You can also use the relative speed of the source and listener objects to simulate the Doppler Effect for added realism. Unity can’t calculate echoes purely from scene geometry, but you can simulate them by adding Audio Filters to objects.
Files: Unity can import audio files in AIFF, WAV, MP3 and Ogg formats in the same way as other assets, simply by dragging the files into the Project panel. Importing an audio file creates an Audio Clip which can then be dragged to an Audio Source or used from a script
Unity can access the computer’s microphones from a script and create Audio Clips by direct recording. The Microphone class provides a straightforward API to find available microphones, query their capabilities and start and end a recording session.
Reverb zones
Unity has a feature called a reverb zone. Reverb Zones take an Audio Clip and distorts it depending where the audio listener is located inside the reverb zone. They are used when you want to gradually change from a point where there is no ambient effect to a place where there is one, for example when you are entering a cavern. The reverb zone is an audio component that can be added just as any other component. From the Unity documentation:
- Min distance represents the radius of the inner circle in the gizmo. This determines the zone where there is a gradually reverb effect and a full reverb zone.
- Max distance represents the radius of the outer circle in the gizmo. This determines the zone where there is no effect and where the reverb starts to get applied gradually.
- Reverb preset determines the reverb effect that will be used by the reverb zone.
The image on top is the reverb component. The image below it is a diagram that illustrates the properties of the reverb zone and how the sound works in a reverb zone
Background music
Background music can set the mood of a scene, keep the player entertained on a subconscious level, or even be gameplay mechanics that the player interacts with. We can also create a dynamic system that will allow us to play songs randomly or in a playlist style. Later we will look at creating some dynamic effects that will be event driven, i.e. a shot being fired, an explosion, a creaking door.
In this section we’re going to explore the process of making a music player that is configured to either play the clips at random, to treat them as a playlist or to repeat them.
For this sample, we’ll have a List of audio clips that will be stored in the list in the inspector. For the initial set up, we create our list of songs, set the volume, and declare a few variables in our background music class.
public List<AudioClip> SongList = new List<AudioClip>();
public float bgVolume = 1.00f;
public int curSong = 0;
public int ranMin, ranMax;
public bool PlayRandomly = false;
public float pitch = 1f;
public float StereoPan;
private AudioSource source;
void Start()
{
//GetComponent<AudioSource>().volume = bgVolume;
//GetComponent<AudioSource>().pitch = pitch;
source = GetComponent<AudioSource>();
source.volume = bgVolume;
source.pitch = pitch;
ranMax = SongList.Count;
}
In start, we will get our audio source component and then set the volume, pitch and the upper limit of our random number. While we could use get component as an aspect of every call to the audio source, it is more efficient to call get component 1 time and then continue by using the reference to it.
The audio source has many methods, properties, and public variables available for use. You can see the volume and pitch in start. Some of the others are isPlaying, clip, loop and Play().
- Volume is a value that is designed to be 0.0f to 1.0f.
- Pitch makes a melody go higher or lower. With an audio clip with pitch set to one, increasing the pitch as the clip plays will make the clip sound higher. Similarly, decreasing the pitch to less than one makes the clip sound lower. When resource is an AudioClip, the pitch property is clamped to the range [-3, 3].
- An AudioSource clip determines the audio clip that should play next. When you assign a new audio clip to clip, the old clip it replaces stops and is replaced by the new one. However, the new clip doesn't automatically play so you need to use AudioSource.Play to play it.
To play a random song we're going to need to have our list of songs or a list of clips. We're going to need a random number generator and then we're going to have to tell our source to play. Our random number generator that will use is from Unity’s random range where we have a min and a max range. Remember that the min is inclusive and the Max is the exclusive bound. So if min is 0 and Max is five, our random number generator is going to return a value from zero through 4.
void PlayRandom()
{
if (!source.isPlaying)
{
curSong = Random.Range(ranMin, ranMax);
source.clip = SongList[curSong)];
source.Play();
}
}
A couple things to note on this method of play random. First it checks to see whether the source is not playing. We don't want to assign a new clip until the last one has finished. Once our source has finished playing, we'll select a new clip from those values and then tell the source to play. Notice that the random min and ran Max (being perhaps zero to 5), I have the potential to have the same clip selected again. By making a more sophisticated process, we can ensure that the selected the value prevents the clip from repeating.
To make a playlist we're going to follow a similar concept but instead of selecting a index at random, we will play them in sequence. If the source is not playing, we’ll increment our index, curSong. If that value >= the length of the list of clips then we'll reset it to 0. Finally, we'll tell our clip to play the song at that index.
void Playlist()
{
if (!source.isPlaying)
{
curSong++;
if(curSong >= SongList.Count)
{
curSong = 0;
}
source.clip = SongList[curSong];
source.Play();
}
}
The third option we have for our song tracks would be to play the clip again or to repeat it. This process is again very simple and we're going to make use of another variable that belongs to the audio source and that's loop. We're going to set loop to true and then tell our song to play so our clip will automatically proceed through when it finishes if loop is true then it'll start over again at the beginning. That is essentially the same as set loop to true in the inspector but again we can control it with the scripts.
void PlayRepeat(AudioClip Song)
{
source.clip = Song;
source.loop = true;
source.Play();
}
To be able to control this as far as which methods go into action we're going to add I enumerated type to our set of variables and I'm going to call it BG music mode with playlist repeating and randomly as their options.
public enum BGMusicMode { PlayList, Repeating, Randomly}
BGMusicMode style = BGMusicMode.Randomly;
Then we're going to go to update and in update I'm going to have a switch statement that uses 3 cases one for repeating one for playlists and one for randomly to select the play mode and it's also going to reset the volume the pitch and the pan stereo. I'm not going to include in this sample is whether I'm using UGUI or IMGUI or if I have a different sort of switch or key mode to change the various styles that's something that you can set up on your own
switch(style)
{
case BGMusicMode.Repeating:
PlayRepeat(SongList[curSong]);
break;
case BGMusicMode.PlayList:
Playlist();
break;
case BGMusicMode.Randomly:
PlayRandom();
break;
}
source.volume = bgVolume;
source.pitch = pitch;
source.panStereo = StereoPan;
Ambient or background sounds
These are sound effects played in a game to give your scene more immersion, perhaps waves crashing on a beach, the leaves blowing in the wind, the sounds of a crowd in a restaurant or even the sounds of a city with sirens and honking horns.
The concept of playing the ambient sounds is identical to the one we used for playing the background music but we will implement the storage of the clips differently for this example. We’ll use a data structure called a Dictionary.
A Dictionary is a generic class provides a mapping from a set of keys to a set of values. Each addition to the dictionary consists of a value and its associated key. Retrieving a value by using its key is very fast, close to O(1), because the Dictionary< TKey, TValue> class is implemented as a hash table. Every key in a Dictionary must be unique according to the dictionary's equality comparer. A key cannot be null, but a value can be. Each individual element of the Dictionary is a KeyValuePair <TKey, TValue>. If you traverse the Dictionary with a foreach loop, you would address the elements similar to this:
foreach( KeyValuePair<string, string> kvp in myDictionary )
In this block we've declared our initial variables we have a list called clip list that's going to hold our audio clips and a public list of keys that are strings. Both of those will be assigned values in the inspector. The dictionary has a string for the key and the audio clip as the value.
public List<AudioClip> clipList = new List<AudioClip>();
public List<string> keys = new List<string>();
private Dictionary<string, AudioClip> ambList = new
Dictionary<string, AudioClip>();
public float ambVolume = 1.00f;
private AudioSource source;
private int level;
void Start()
{
source = GetComponent<AudioSource>();
source.volume = ambVolume;
int i = 0;
foreach(AudioClip ac in clipList)
{
ambList.Add(keys[i], clipList[i]);
i++;
}
level = 1;
Play ("Level "+level);
}
In start we get the audio source, set the source volume to the ambient volume and then traverse the audio clips. In the dictionary, we pair audio clip with a key as the dictionary is built. Upon completion of the dictionary, the Play method is called with the parameter of a level.
In the play method since the audio clips are stored in a dictionary, we have direct access to them by calling the get value method using the key. There is a simple method in update that uses a key press to step thru the dictionary keys. It first stops the current ambient sound and then starts the next.
void Play(string ambKey)
{
source.clip =
ambList.GetValueOrDefault(ambKey);
source.Play();
}
if (source.isPlaying)
{
source.Stop();
Play("Level " + level);
}
Sound Effects
In the last sections we've addressed background music and ambient sounds which, while they can be event driven, are generally about setting a mood or environment. These sound effects are event driven. Unity has a system that lets a clip be played on awake. Play on awake is set in the inspector with the audio clip and it might suffice for elements such as shooting a bullet. That's not going to be suitable for most of the other event driven elements. We'll develop an event based system that is controlled by scripts. This will be an event-based system that makes playing a sound effects very easy.
In the inspector I've assigned 3 sound effects to my sound list and I've assigned three values to my keys. There is a baseball hit, a boom and an explosion for the sounds and there are a run, walk and jump for the keys. The code is next.
The audio clips are again stored in a dictionary. In start as usual we get the audio source component and assign it to the source. The source volume is assigned the sfxVolume and then the dictionary is initialized. There is one assumption, that the list of keys and the list of sounds are the same length. Then traverse through them and create the dictionary entries using the special effects dictionary add method and then the parameters being the key and the clip from the corresponding positions in their collections.
public List<AudioClip> soundList =
new List<AudioClip>();
public List<string> keys = new List<string>();
public float sfxVolume = 1.00f;
public float pitch;
public float StereoPan;
private AudioSource source;
private Dictionary<string, AudioClip> sfxDict =
new Dictionary<string, AudioClip>();
void Start()
{
source = GetComponent<AudioSource>();
source.volume = sfxVolume;
for (int i=0; i<keys.Count; i++)
{
sfxDict.Add(keys[i], soundList[i]);
}
}
Using a dictionary for special effects makes accessing them easy. This is the entire extent of the play method. It uses a dictionary get value method using the key as the parameter which returns the clip and assigns it to the source clip.
void Play(string SFX_Type)
{
source.clip = sfxDict.GetValueOrDefault(SFX_Type);
}
In this next section we'll explore how to make these special effects occur. To keep things simple, I have them set up for key events. You may have some key events in your program that cause something to fire or you may have events occur where you collide with something, you hit a trigger, someone shoots at you, you shoot at someone else, something explodes. So for all of those events, when they happen, we want a sound effect to occur. We want to ensure there isn’t a clip playing, and then we're going to get the clip, set volume, pitch, etc., and then tell it to play.
As you look at the code that I've extracted from the update method, there are three blocks that are going to cause something to play and then I have one block that's going to stop the current source from playing.
if (Input.GetKeyDown(KeyCode.W))
{
if (!source.isPlaying)
{
Play("Run");
source.loop = true;
source.pitch = 1.7f;
source.Play();
}
}
else if (Input.GetKeyDown(KeyCode.Q))
{
if (!source.isPlaying)
{
Play("Walk");
source.loop = true;
source.pitch = 1.7f;
source.Play();
}
}
else if
(Input.GetKeyDown (KeyCode.X))
{
if (source.isPlaying)
{
source.loop = false;
source.pitch = 1f;
source.Stop();
}
}
else if (Input.GetKeyDown(KeyCode.Space))
{
Play("Jump");
source.Play();
}
source.volume = sfxVolume;
}
If the player presses the W key then, as long the audio source is not playing, we'll proceed inside the if statement. We'll set the source.clip to the value the run key is associated with. In this case, the source.loop is set to true so the clip will keep playing until we stop it. The X key will stop any source is that is playing. the loop is set to false and the source to stop. The Q key and the space key both have different sounds they'll play. Notice the space key will play jump whether anything is playing or not. so It's going to override the previous clip.
The completed code sample for this project is available in the code samples, in the audio project. Here is the link to the video for Audio.
11 Game Settings
In this section we’ll be looking at elements that can make the game more user friendly. We’ll be creating some video configurations, creating and modifying some audio configurations, and setting up some more save and load options. We'll look at ways we can modify elements of Unity in runtime rather than be stuck with configurations that were set in the inspector while in the Editor mode.
Some aspects will allow the player to optimize their game for their optimum experience. It will let them tune the performance to match what their equipment can handle. If it’s a complex game that has significant rendering requirements, our player may not have a high end computer. We may need to give him an option to lower some of those requirements by reducing some of the video load, whether that is from shadows or anti aliasing, etc. We will give them a process to control the volumes within the game and the various sound effects discreetly rather than just having one master volume.
We'll do this by creating an options menu. That menu will display the aspects of the game that they can modify and will consist of buttons or sliders that access the various components.
Video Configurations
When it comes to performance, the video settings are perhaps the most important. Changing something as simple as the shadows can greatly change how a player can smoothly play the game.
Shadows. Shadows add depth and an element of realism to a scene because they bring out the scale and position of the objects. Without shadows, the scenes may look flat. While there is a shadows setting on lights for setting the type of shadows, there is also a cast shadow feature in the mesh renderers. We are going to use the feature that is an element of the lights. In lights, the shadow types available in the inspector are no shadows, soft shadows and hard shadows. No shadows is the least demanding of the system while soft shadows is the most demanding. Soft shadows require the calculation of the direct effects plus the effects of reflected lights. Shadow calculations are generally performed on the GPU.
To set our shadows, we’ll gather all of the lights in the scene. In older versions of Unity, we could use GameObject.FindObjectsOfType but that is now deprecated/obsolete and is replaced by GameObject.FindObjectsByType<Light>((FindObjectsSortMode.None)). Depending on the value of the shadow setting parameter, all the lights will be configured with the same shadow setting.
public void ShadowSetting(int setting)
{
Light[] lights = GameObject.FindObjectsByType<Light>((FindObjectsSortMode.None));
foreach(Light light in lights)
{
if(setting == 0)
light.shadows = LightShadows.None;
else if(setting == 1)
light.shadows = LightShadows.Hard;
else
light.shadows = LightShadows.Soft;
}
}
FOV
The field of view is a video setting that doesn't have much affect on performance, but it is an option that many PC gamers like to modify. The field of view literally means what it's called; it determines how big the view port is for the player to see the game. It's measured by angle degrees and can also be measured vertically, horizontally, or diagonally. Typically, the field of view is measured diagonally for video games.
public void SetFOV(float newFOV)
{
Camera.main.fieldOfView = newFOV;
}
Resolution
Resolution is a component of the player resolution and presentation section of project settings. This setting will only take effect in a built game, not in the editor. From the Unity documentation, the full screen modes are:
- Fullscreen window which sets your app window to the full-screen native display resolution, covering the whole screen. This mode is also known as borderless full-screen.
- Exclusive Fullscreen sets your app to maintain sole full-screen use of a display. This mode changes the OS resolution of the display to match the app’s chosen resolution. This option is only supported on Windows.
- Maximized Window sets the app window to the operating system’s definition of maximized. On Windows, it is a full-screen window with a title bar. On macOS, it is a full-screen window with a hidden menu bar and dock.
- Windowed sets your app to a standard, non-full-screen movable window, the size of which is dependent on the app resolution. In this mode, the window is resizable by default.
This code sample has some generic resolutions listed on the settings. To discover the settings that your particular system supports, you could use Screen.Resolutions which returns an array of the supported resolutions.
public void SetResolution(int Res, bool fs)
{
switch(Res)
{
case 0:
Screen.SetResolution(1920, 1080, fs);
break;
case 1:
Screen.SetResolution(1600, 900, fs);
break;
case 2:
Screen.SetResolution(1280, 1024, fs);
break;
}
}
Resolution[] resolutions =
Screen.resolutions;
foreach (var res in resolutions)
{
print( res.width + "x" + res.height);
}
Anti-aliasing
Aliasing in a game is where the models being rendered have jagged edges. Anti-aliasing is what the game’s renderer does to smooth out those jagged edges. This is one of the options that will make your game look great, but will also slow down the performance. The way anti-aliasing works is that it will blur the edges by a number of samples. If the number of samples is zero, no anti-aliasing will happen. The quality settings anti-aliasing sets value of the multi-sample anti-aliasing (MSAA) that the GPU will perform. The value indicates the number of samples per pixel. Valid values are 0 (no MSAA), 2, 4, and 8. If the graphics API does not support the value you provide, it uses the next highest supported value.
public void SetAA(int Samples)
{
QualitySettings.antiAliasing = Samples;
}
V-Sync
V-Sync is vertical synchronization and it is a method of locking your frame rate to the refresh rate of your display. V-Sync affects how the frames are rendered. With V-Sync on, the game will wait until the frame has finished rendering before starting the next frame. With V-Sync off, the game will start to render the next frame while the current frame is still being rendered. The bonus of V-Sync being off is that the game will render faster but could cause an effect called screen tear, which shows an obvious line on the screen caused by the frames overlapping each other.
In Unity, vSyncCount is the number of vertical syncs that should pass between each frame. It’s an integer in the range of 0-4. By default, it is set to 1. vSyncCount specifies the number of screen refreshes your game allows to pass between frames. If vSyncCount > 0, then the field Application.targetFrameRate is ignored, and the effective frame rate is the native refresh rate of the display divided by vSyncCount.
- If vSyncCount == 1, rendering is synchronized to the vertical refresh rate of the display.
- If vSyncCount is set to 0, Unity does not synchronize rendering to vertical sync, and the field Application.targetFrameRate is instead used to pace the rendered frames.
For example, if you're running the Editor on a 60 Hz display and vSyncCount == 2, then the target frame rate is 30 frames per second.
public void SetVsync(int Sync)
{
QualitySettings.vSyncCount = Sync;
}
Audio Configurations
The basic audio configurations will involve creating separate volume controls for each audio source. In the sample, there are 3 game objects that are individually responsible for holding their respective audio sources. There is one for ambient sounds (AmbSound), one for special effects sounds ( SFXSound) and one for background sounds/music player (BGSound).
Setting the initial volumes for background music, sound effects, and the ambient sounds There are a couple of different approaches that we could take to set our initial volumes. One approach would be to have a series of variables just within the class that are automatically assigned. Another approach could be to save the data in PlayerPrefs and call it from PlayerPrefs. A third approach would be to store the data in a file. Then read the file when you want to load the settings and when you're done, save the file. With a file there are a couple of different options. You can use a text file, a JSON file or an XML file. I opted to use a JSON file. We’ve created a separate class that is marked as serializable so we can utilize JSON to serialize and deserialize it. It's called AV_Settings, for audio visual settings. The only variables currently displayed are the volumes for the background, the special effects, and the ambient noises. Notice that this class does not extend or inherit from MonoBehaviour so it can't be attached to a game object. This class is actually instantiated in a separate class, AVSettingManager, using
[Serializable]
public class AV_Settings
{
public float volBG;
public float volSFX;
public float volAmb;
}
avs = new AV_Settings();
to accomplish it. We'll talk more about the settings manager after we get a little further into game settings. We’ll also see how to set up a GUI for the configuration. One thing to be aware of with regards to the initialization of the AV settings is, that while most of our scripts are fine using start, because of a synchronization issue, you want to make sure that you use Awake as the method that you have your initialization in.
If you were using PlayerPrefs for storing your data then your code might look something like this.
public void SetVolumes()
{
AudioSource[] audios = GameObject.FindObjectsByType<AudioSource>(FindObjectsSortMode.None);
foreach(AudioSource source in audios)
{
if(source.name.Contains("BG"))
{
source.GetComponent<BG_Music_Manager>().bgVolume =
PlayerPrefs.GetFloat("bgVolume");
}
else if(source.name.Contains("AMB"))
. . .
}
}
While the following sample uses the data from the AV_Settings.
public void SetAllAudio(AV_Settings avs)
{
AudioSource[] audios = GameObject.FindObjectsByType<AudioSource>(FindObjectsSortMode.None);
foreach(AudioSource source in audios)
{
if(source.name.Contains("BG")){
source.volume = avs.volBG;
}
//. . .
}
}
In both those code segments, there are three different audio sources that are each attached to their own component. One of background sound source, one of special effects sound source and the others the ambient sound source. Since the names are unique and distinctive, rather than trying to match names, I'm getting their names from the sources and determining that if it contains BG or AMB or SFX. Then I know the type. Then I’ll set the volume from my master collection of settings from the script AV settings. So source volume = avs. volume and I would do the same for my ambient sounds and my special effects sounds.
Speaker mode
Another audio configuration item that you may want to consider is speaker mode. The speaker mode settings will range from mono to stereo to quad and surround sound 5.1 and 7.1. In the past we were able to directly assign values to the speaker using AudioSpeakerMode mono, stereo, etc. That's changed to a slightly different process. That's going to shown in this next code segment. To change speaker mode we need to acquire the audio settings configuration and create a reference variable for whether we're changing things. Then we can process thru the switch statement to set our audio speaker mode and finally call audios settings to reset the configuration.
The data file and processing
With my settings manager you can see I'm using application data path and I'm going to save my files a JSON file. Recall that we need this processing in Awake, so that the data is loaded before all the other start methods are executing. In Awake I also test to make sure the file exists. If it exists we’ll load the data from it but if it doesn't exist we’ll load from a set of default values.
public void SetAudioType(string SpeakerMode)
{
bool modified = false;
AudioConfiguration config =
AudioSettings.GetConfiguration();
switch (SpeakerMode)
{
case "Mono":
config.speakerMode =
AudioSpeakerMode.Mono;
modified = true;
break;
case "Stereo":
config.speakerMode =
AudioSpeakerMode.Stereo;
modified = true;
break;
‘ ‘ ‘
}
if (modified)
AudioSettings.Reset(config);
}
public class AVSettingsManager : MonoBehavior
{
public AV_Settings avs;
string path = Application.dataPath + "\\" + "AV_Setting.json";
void Awake()
{
avs = new AV_Settings();
if (File.Exists(path))
LoadData();
else
SetDefaultValues();
}
private void SetDefaultValues(){}
private void LoadData(){}
// Update is called once per frame
void Update(){}
public void SaveAVSettings(AV_Settings avs){}
The GUI
For my GUI I've opted to use IMGUI rather than UGUI but if you'd rather use UGUI that's perfectly fine. Below we'll have some samples of the various buttons and sliders. Normal buttons work well but there are some cases where a slider or a toggle may be more appropriate or intuitive. You may want to consider sliders for FOV and volumes and toggles for AA and full screen.
The first sample here has a label for quality settings and then this shows one of the four or five buttons that would be associated with setting our quality settings. Remember our rectangle has a top left X, top left Y then height and width. That's both for our label and for our buttons. If it's pressed, it returns true which causes it to react so then we get our av settings quality set to 1. Then we go through the quality settings and set the quality level to one and true which is the low setting that's one of the five possible settings for quality.
GUI.Label(new Rect(25, 0, 100, 30), "Quality Settings");
if (GUI.Button(new Rect(25, 30, 75, 30), "Low"))
{
avs.quality = 1;
QualitySettings.SetQualityLevel(avs.quality, true);
}
The next code sample we're going to use shows us creating a slider and this one's going to be for field of view so I have a GUI label to start and then I make a GUI horizontal slider. I again give it a bounding rectangle and now I give it a variable that we're going to be adjusting, avs field of view and I set a min value and a max value. When the slider is moved it, returns the values of avs field of view and that is then used to in the field of view method in the video configuration.
GUI.Label(new Rect(25, 75, 100, 30), "Field of View");
avs.fov = GUI.HorizontalSlider(new Rect(115, 80, 100, 30), avs.fov, 10f, 160.00f);
videoC.SetFOV(avs.fov);
The next sample is going to use a GUI toggle to turn anti aliasing on and off again we have a label for it and then we have our toggle with this value.
GUI.Label(new Rect(25, 100, 100, 30), "Antialiasing");
avs.aa = GUI.Toggle(new Rect(115, 100, 100, 30), avs.aa, " On/Off");
The next section is just one more example of a slider but it's going to be setting the volume. We’re going to adjust the volume of the background music and then we're going to call the set volumes of the audio configuration class to apply the changes.
GUI.Label(new Rect(25, 230, 150, 30), "Music Volume");
avs.volBG = GUI.HorizontalSlider(new Rect(25, 250, 100, 30), avs.volBG, 0.00f, 1.00f);
//...
audioC.SetA llAudio(avs);
Saving the values
Earlier we've discussed saving files and we have text files, XML files, JSON files. You have several different choices and it's entirely up to you. Next is a process that I used because I thought it was going to be straightforward and easy. In my settings manager class, I have the save settings method that takes the AV settings, converts them to JSON and writes them to the file.
public void SaveAll()
{
AVSettingManager avMan = GetComponent<AVSettingManager>();
avMan.SaveAVSettings(avs);
}
Loading the values
Loading the values into my Av settings is just the reverse. I’ll read the file, in this case with the JSON file, convert it from JSON, store it in my Av settings class. Now if you prefer you can use XML or text files if either of those would be fine
public void LoadData()
{
StreamReader sr = new StreamReader(path);
string data = sr.ReadToEnd();
sr.Close();
avs = JsonUtility.FromJson<AV_Settings>(data);
}
public void SaveAVSettings(AV_Settings avs)
{
StreamWriter sw = new StreamWriter(path);
string stringData = avs.ToJson();
sw.Write(stringData);
sw.Close();
}
The completed code sample for this project is available in the code samples, in the game settings project. Here is the link to the video for game settings.
12 Video/Cutscenes
Unity’s video system allows you to integrate video into your game. The video footage can add realism, save on rendering complexity and help you integrate other content.
To use video in your game, we need to import Video Clips and configure them using the Video Player component. The system allows you to feed video footage directly into the Texture parameter of any component that has one. Unity then plays the Video on that Texture at run time.
Video features include hardware-accelerated and software decoding of video files, transparency support, multiple audio tracks and network streaming.
Video Player component Use the Video Player component to attach video files to GameObjects and play them on the GameObject’s Texture at run time. By default, the Material Property of a Video Player component is set to MainTex. When the Video Player component is attached to a GameObject that has a Renderer, it automatically assigns itself to the Texture on that Renderer (because this is the main Texture for the GameObject).
Here the video has been attached to a sphere. It was set to play on awake, not to loop, with playback speed of 1. The render mode is material override and the material property is _MainTex. When the video clip ends, the fire display will terminate.
We can also attach the video player to the main camera. Then we have the option of having it on the far clipping plane or the near clipping plane. With the video player rendering on the far clipping plane, it acts as a background to the scene and everything is rendered between it and the camera.
If the video player is set to render on the near clipping plane, it will be between the camera and other objects in the game. The video player has a slider for the alpha component of the video which is used to adjust the transparency of the clip. A value of 1 is totally opaque while 0 is totally transparent.
This image is with the player rendering on the near clipping plane with an alpha of 1.
The image below shows the video player rendering on the near clipping plane with an alpha of 0.2.
The completed code sample for this project is available in the code samples, in the video project. This is the link to the video about using video in a project
Thanks to:
Nick Murphy for proofing and captioning
Kevin Markley for the assistance with the input system
Antonio Brown and Kalil Masters for the proofing
References
Unity 4.x Game AI Programming, Kyaw, Swe, Peters
Unity Scripting Reference documentation, online
Unity Manual documentation, online
MSDN online documentation, C#
User Interface Design and Implementation in Unity, Unity E-Book, unity.com
Introduction to Game Level Design, Unity E-Book, unity.com
Total Beginner’s Guide to Game AI Ben Sizer Aug 2018
Unity Game Development Scripting, D'Aoust Publisher: packt publishing
Unity Cookbook 2023, Smith, Ferns, Murphy packt publishing