Unity3D is a particularly programmer-friendly game engine. A typical engine likes to be good at one thing -- puzzles, or top-down shooters -- and provides a simple, limited, custom coding language. Unity decided to be general-purpose. It doesn't have direct support for anything, but has an API for all the basic 3D and 2D game stuff. Unity's hook to entice game designers is being a jack-of-all-trades.
I first saw Unity coming from a programming background. For various reasons, I tried to adapt my style to a game engine / Unity way. My advice now is not to bother. You'll see Unity examples with funny coding and assume there's some hidden reason for not doing it the normal way. There rarely is. Code normal things in your mormal way, unless you're dealing with some funy Unity API stuff - then double-check.
Some overall notes and comments:
3.2f
's. If uses floats instead of doubles. Who does that? You can't trust
people who play tricks with types like that. But it turns out game engines are one of the few places that floats are a good choice.Unity uses the standard parts from each game domain, which can seem strange. A few notes:
Compared to UnReal: Unity has nothing useful. No players, doors or elevators, or dialogue trees. The Unity store might have a 3rd party part you need, maybe. Most likely you're coding door-opening yourself.
There's no collider generator. You hand-make colliders by either combining box, sphere and capsule colliders; or create and add your own Mesh collider. There are no explicit trigger zones.
A trigger zone in Unity is a collider with the isTrigger
box checked.
Compared to XNA or OpenGL: Unity does much more for you. It uses a scene camera and persistent objects. It auto-handles matrixes, draw calls, batching and frustum culling. All you need to do is put things where you want them and aim the camera. You're allowed to set camera matrixes and so on, but I've never needed to.
There's no main entry point to your code.
Unity expects you to attach "scripts" to various objects. The script to move the car
goes on the car, the elevator script goes in the elevator, and so on. void Update()
is run every video frame, for each object, in no particular order. Scripts communicate with each other on an ad hoc basis.
You can force a single entry point by having 1 main script with the only void Update()
.
That script would manually control all other objects. Or it can manually reach out and call their hand-made
myUpdate()
function (more, later).
There's no top-level class for a Camera, or Light, or Ball, or Wall, or script. Everything is made using a generic GameObject (the name of the class), with various Components attached to it. For example, a "camera" is a gameObject with an attached Camera component.
This gives a mix-and-match feeling. If you want a camera which is also a spotlight, add both components. If you want physics to affect it, also add a Rigidbody and a Collider. The standard way to check a gameObject "is" something is to check whether it has that component:
Camera c1Cam = c1.GetComponent<Camera>(); if( c1Cam == null ) { // c1 isn't a camera
Oddly, the Transform (position/size/scale) isn't part of the GameObject. It's actually a Component. It's always first in the list, and required (you will never see a gameObject without one, or a Transform which is not in a gameObject's Component list). A picture, then some code:
GameObject (a camera) Components: | Transform: name, position, rotation, scale | Camera: type, angle, max distance GameObject (a bouncing ball) Components: | Transform | MeshFilter: [mesh: "Cube"] | MeshRenderer: [Material: "shaded white"] | Rigidbody: (signals Physics system to move us) | SphereCollider: Radius
If you have a link to any part, you can find the rest. GetComponent
searches
the list (it finds the first one, but there's never a reason to have duplicates). As a shortcut transform
replaces GetComponent<Transform>()
. For any component the associated gameObject is just gameObject
.
On things this means is that a Transform variable is a perfectly fine way to link to an object in the game:
// these create "drag into" slots in the GUI: public GameObject b1; // obvious way to reference a gameObject public Transform b2; // equally good way b1.transform.rotation = Quaternion.identity; // extra step to find the transform b2.rotation = Quaternion.identity; // b2 is b1.transform b1.layer=0; // layer field is on the gameObject b2.gameObject.layer=0; // transform b2 looks-up its gameObject
If the other object has one interesting component, it's common to reference it using that, since we can find any other parts we need from there:
public Camera cam; // only something with a Camera can go here public Transform camT; // anything can go here (but we expect a camera) cam.fieldOfView=20; // easy, since we cleverly linked the camera camT.GetComponent<Camera>().fieldOfView=20; // the long way // but a link to the transform still has an advantage with transform changes: camT.position = new Vector3(0,10,0); // cam is a camera, needs to find its transform: cam.transform.position = new Vector3(0,10,0); // both find their gameObject the same way: cam.gameObject.setActive(false); camT.gameObject.setActive(false);
The split between GameObject and Transform is funny. gameObject has the
active
flag and the layer (the setting for how collisions work).
Oddly, Transform has the name and holds the tree structure (later).
More notes:
new Camera();
isn't allowed. It would be c2.AddComponent<Camera>()
. Not even GameObjects can be free-standing. new GameObject()
is legal, but it add a Transform component
and adds the whole thing to the 3D scene.new GameObject()
. You generally pre-make prototype
objects (prefabs). While running you'll clone the prefab, components and all. For a component which comes and goes it's often easier to pre-make it and use the active
flag to toggle.Unity exposes most fields of a component in the Inspector, using the same name as in code, slightly altered
(camel-case is turned into spaces). The Inspector displays "Field Of View" which in code is
c1.fieldOfView
. A few fields are in the API but not the GUI, or vice-versa, or it's in both but has very different
names. But the same name in both places is the norm.
Adding child gameObjects is the preferred way of having multiples of something. Say you want 2 box colliders to give yourself an L-shape. In theory, you could give a gameObject 2 box colliders. But in practice you'd create two child gameObjects, each with 1 BoxCollider (and nothing else).
There's no overall parent -- the layout in a scene is a forest: lots of parentless gameObjects. To force organization it's common to create an "empty" gameObject (it has the required Transform, but nothing else) used as a folder/directory.
Strangely, parent/child relationships are stored in the Transform. Your transform has a link to your parent's transform (or null). The transform has a list of your child transforms. For example, this would store gameObject g1's gameObject parent in myParentG:
GameObject myParentG; Transform tp = g1.transform.parent; if(tp==null) myParentG=null; else myParentG=tp.gameObject;
You'd rarely do this, since storing everything Transforms is simpler.
Children are handled in an obvious way. Transforms have childCount
and
getChild(i):
public Transform dog; // link to another gameObject // Using Transform instead of gameObject, since that seems easier if(dog.childCount>0) { Transform firstPuppy=dog.GetChild(0); firstPuppy.parent=null; // set our 1st child free // NOTE: it will automatically be removed from our child list firstPuppy.parent=dog; // changed our mind -- bring it back }
Fun fact: when you set a parent, the system checks for and refuses to make cycles.
Besides finding children by index, you can search by name:
Transform bowser = dog.Find("bowser"); // searches the puppies of Dog if(bowser!=null) {} // we found a child named Bowser
There's another Find which can be confusing. GameObject.Find("bowser") is a static function which searches the entire world for bowser, including children. It's for tiny games or as a back-up in oddball situations.
There's a very odd shortcut to run through all children: use a foreach on a transform. Unity wrote a special iterator for this:
// go through all childen in Transform dog: foreach(Transform puppy in dog) { print( puppy.name ); } // one of these is bowser
You'll sometimes read how setting your parent is Unity is "parenting" yourself, and other odd phrasing. That doens't mean anything. Some people writing about Unity simply confuse the terms.
Each "script" is actually an instance of a class. The sample script below is merely a class named ballMover:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class ballMover: MonoBehavior { // <-inherit from this public Vector3 movePerSec; // the Inspector will make a nice field for you here void Update() { transform.position += movePerSec * Time.deltaTime; } }
Dragging that file onto various gameObjects signals Unity to spawn ballmover instances, each hooked up to their gameObjects.
Scripts are Components and go in the gameObject's normal Component list. Like all components, they have the look-ups transform
, gameObject
and GetComponent()
. Scripts always inherit from Monobehavior, which inherits from Component. The Monobehavior class has almost no functionality. It simply signals Unity that we're a script, so it should look and run for Awake(), Start(), and Update().
Like any other component, we can use attached scripts as stand-ins for an object. This code manipulates a gameObject through its ballMover:
// a link to a gameObject through it's attached ballMover: public ballMover otherBall; // drag in any gameObject containing a ballMover script otherBall.movePerSec.x=0; // easy -- changing a class field otherBall.transform.position = Vector3.zero; // like any other component, reach into our transform // we can even use GetComponent to find sibling components: otherBall.GetComponent<Renderer>().material.color = Color.green;
We can use GetComponent to search for a script, since scripts are components:
void stopThisBallIfItIsOne(Transform b) { // bm1 will be the ballMover component of b: ballMover bm1 = b.GetComponent<ballMover>(); if(bm1==null) return; // no script, not a ballMover bm1.movePerSec=Vector3.zero; }
You may have noticed how Update is private and isn't using override. So how is Unity finding these to run them? It turns out it initially searches using C# reflection.
Game designer style is to have each script control the object it's attached to, but there's no special advantage to that. You can trick Unity into giving you a main program. Write one script with a Start() and Update(). It has to be attached to some gameObject, but it can be an empty, created for that purpose.
This single script would move a list of balls:
... public class allBallMover: MonoBehavior { public Transform[] Balls; // fill through the GUI void Update() { for(int i=0;i<Balls.Length;i++) Balls[i].position += Vector3.right * Time.deltaTime; } }
It's often useful to have a simple struct-style class attached to game objects. That's no problem. Here ballData allows us to associate a speed and score with each ball. It needs to inherit from Monobehavior, so it can be attached:
public class ballData : MonoBehavior { // <-can go on a gameObject public Vector3 movePerSec; public int pointValue; // we may as well add helpful public functions, // but these are completely optional: public void doMove() { transform.position += movePerSec*Time.deltaTime; } public void turnRed() { GetComponent<Renderer>().material.color = Color.red; } }
A typical-seeming use is a wall which destroys everything hitting it, and if it happens to be a ball, it gives us the points:
void wallOfDoom(GameObject g) { // is it also a ball?: ballData bd = g.GetComponent<ballData>(); if(bd!=null) playerScore += bd.pointValue; // either way, it's gone: Destroy(g); }
Or we might use it in our master script:
// links to each script, which are really links to every ball: public ballData[] Balls; // drag in each ball gameObject foreach(ballData b in Balls) { if(b.pointValue<0) b.transform.name="sleepy"; }
Of course, we could rewrite ballData as a normal (non-monobehavior) class. We'd add one more field which links it to the actual ball:
// [S.S] allows Unity to display this class in the Inspector: [System.Serialized] class ballDataTraditional { public Transform myBall; // NEW. drag in a link public Vector3 movePerSec; public int pointValue; } public ballDataTraditional[] AllBalls;
Old-style coding would set AllBalls[i].myBall.position= ...
and so on. Very normal-looking to a coder. But this method has a sneaky drawback -- we can't very well find these values when given a gameObject. Our object-destroying-wall can no longer "ask" something whether it's a ball.
Scripts can also be used as flags. This script would go on another wall to zap-away balls hitting it. It doesn't need to know anything about the ball, merely whether it is one:
public class ballBlaster : MonoBehavior { // OnCollisionEnter is a callback from the physics system: void OnCollisionEnter(Collision c) { Transform intruder = c.transform; // the thing that hit me // is the intruder a ball?: ballData bd = intruder.GetComponent<ballData>(); if(bd!=null) { intruder.position = Vector3.zero; // snap it away } } }
One gameObject will gladly hold several scripts. This can be a poor man's multiple inheritance. You might have 4 data scripts named: Monster, Collectable, Animal and Robot. A gameObject could mix-and-match to be a collectable robot monster, or whatever.
Or you might have a speech script, optionally attached to some object to mark it as talking. Other scripts would check for it:
health-=injuryPoints; // if we talk, say ouch: speechScript ss = GetComponent<speechScript>(); if(ss) ss.complain(injuryPoints);
In another type of system these might be fields in the main class, set to null if we don't have one. That method also works in Unity, but Unity's list-of-scripts method also works.
Inheritance trees from Monobehavior work the normal way. Here weasel inherits from pet inherits from MonoBehavior. We can drag weasel onto a gameObject and it works fine:
public pet : MonoBehavior { public int cost; public string getCost() { return "$"+cost; } } public weasel : pet { int stinkiness; void Update() { ... } }
As you'd expect, GetComponent<weasel>() and GetComponent<pet>() can each find a weasel. A pet or a weasel variable can point to a weasel. Unity will display cost (from pet) and stinkiness (from weasel) next to each other in the GUI.
Awake() and Start() are meant for a simple synch on initialization. Everyone's Awake() is run, followed by everyone's Start(). This is so regular initializing can be Awake, then Start handles everything depending on someone else. That usually works well enough. Or use the "one master script on an empty" method: no one else has Awake or Start -- the master script will call their init functions in the correct order.
The system runs all Update() functions in arbitrary (but fixed) order, which is usually fine. If you need to force an order, and hate the master-script method, there's a special Script-Execution-Order window.
Unity's cycles are counted in video-frames, which are assumed to be at a variable rate. A cellphone game probably has 30 evenly-spaces frame per second, but in general we assume each frame is an arbitrary amount of time from the previous. Time.deltaTime
gives us this value, in seconds. It's used for most "wait 1/4th of a second" math to accumulate elasped time. Time.time
gives the total seconds since the game was started. It works equally well for timing by saving the time some event was started and comparing with the currect time in subsequent frames.
Frames are the only method of timing, which can be confusing. For example a quarter-second wait counts off frames until enough time has passed. Waiting for tiny amounts, like 0.01 seconds, is the same as waiting for the next frame.
FixedUpdate() is worse for timing. It runs exactly 50 times a second, regardless of frame-rate, which seems great. But it doesn't space them evenly. After each Update the system decides how many FixedUpdate's it needs to run. It's main use is for code needing to be exactly in step with the automatic Physics system (which is rare).
Inputs are also only updated between frames, making them only safe to check in Update().
Unity has a system for fake threads, called coroutines. They're run in an orderly fashion -- after all Updates() are done, run all corroutines. This eliminates most race conditions, which is nice. The main reason to use
them is for their sleep command, yield
.
This coroutine pauses for 2 seconds, then grows us a little each frame:
IEnumerator explodeBall() { // <- IEnumerator is required yield return new WaitForSeconds(2.0f); // like sleep(2000) // get a little larger each frame, in a loop that can pause: while(transform.localScale.x<3) { transform.localScale*=1.01f; // gradually grow yield return null; // a one frame pause // this loop will run 1 step each frame for 200 frames } Destroy(gameObject); // kill ourself } // start a copy of it running: StartCoroutine(explodeBall());
They now even have handles. This example runs two copies of some coroutine, then later decides to stop the second:
IEnumerator c1, c2; // optional handles which can be used to stop them c1=someCoroutine("abc"); // nothing happens yet c2=someCoroutine("def"); // ditto StartCoroutine(c1); // c1 runs, over time StartCoroutine(c2); // ditto ... StopCoroutine(c2); // c2 aborts, c1 continues
The most horrible thing about Unity is how an infinite loop in a script freezes the entire editor. You can't press the Stop button. The only thing you can do is force close the whole program.
Instead of building objects through code, it's simpler to premake a sample object, tuck it away, and clone it later as needed. These tucked-away objects are officially named pre-fabs. and the clone command is Instantiate.
As you'd guess, Instantiate makes a deep copy -- it copies all components. It even recursively
copies child gameObjects. It hooks
everyone's transform
pointers and so on, and adds all new gameObjects to
the scene list. As a bonus, it can use a link to any part of the object, returning the corresponding
link in the copy. This copy copies a cat and a dog from prefabs:
public GameObject catPrefab; // We're choosing to reference dog using it's dogData script: public dogData dogPrefab; ... // create a cat, placed at 000: GameObject c1 = Instantiate(catPrefab); c1.transform.position = Vector3.zero; // nothing special dogData dd = Instantiate(dogPrefab); // creates the entire Dog // can now use dd as a standard link to a script: dd.barks=4; dd.transform.position=new Vector3(4,0,0);
This also works if cat or dogPrefab has a tree of gameObject children (it wouldn't be very useful if it didn't).
Objects instantiated from a prefab are complete unlinked copies. This can be confusing since the Unity manual explains prefabs in a second way. If you drag in prefabs before running the game, they mirror the prefab, changing as it does. But that's not the case when calling Instatiate in code.
You don't see many examples using regular classes in Unity, but they work fine. You're allowed to have Unity classes as members. The one trick is you'll need to add [System.Serializable] in front if you want them displayed in the GUI. Here was have our own sub-list of the dogs currently in the dog show:
public class dogShowRunner: MonoBehavior { [System.Serializable] public class showDog_t { // hold data about an entry in the dog show public int showID; public dogData d; // find the dog (dog gameObjects have this script) public int score; } // displays an array with a fold-out for the class: public List<showDog_t> SD; // we're even allowed to drag dog gameObjects into "d" fields }
You can also write standard classes, in regular files. Use the normal create->Script to get it in the project. Then hollow it out and write it as normal. You're allowed to use all Unity API commands. The system handles makefiles and dependancies invisibly.
Unity is fine with a static class full of static functions. They can even use the Unity API:
class miscStuff { // Random.Range is in the unity API public static int roll2To12() { return Random.Range(1,7)+Random.Range(1,7); } // can take and return Unity transforms: public static Transform getMaximumAncestor(Transform T) { while(T.parent!=null) T=T.parent; return T; } }
Regular classes can't start a coroutine. They can have coroutines, waiting to be called. But only scripts attached to gameObjects can call StartCoroutine. This is because running coroutines are attached to the gameObjects ultimately calling them.
Unity added the fun trick where you can Destroy(c1.gameObject)
and
have every pointer to it instantly become null. Obviously, this is a smart-pointer hack. All links to gameObjects are actually links to their special Unity handles, which have a "was I destroyed" flag. Unity then overloads if(c1==null)
to
if(c1==null || !c1.isAlive)
.
But it normally works pretty well:
GameObject g; // pretend this points somewhere GameObject g2=g; // g2 and g point to same thing Destroy(g); if(g2==null) // true (since it's secretly checking isAlive)
Generally you might have something like Transform currentTarget;.
When the target is destroyed by anyone means currentTarget==null
magically becomes true.
The callbacks from the physics system don't need to be registered. In fact, they can't be.
Instead they key off the function name. Any function named exactly OnCollisionEnter()
is called when your collider hits another.
To shove "physics objects", change GetComponent<Rigidbody>().velocity
directly. There's an AddForce command, but it's confusing and a relic from older physics systems (it can divide the force by your mass, if you ask it to, but we can do that more easily ourselves).
Raycasts (and Spherecasts and OverlapSpheres) are the primary way of finding things ``in the world." They rely on colliders, so feel like part of the physics system, but aren't: they're run instantly and have no side effects.
A gotcha: raycasts and friends purposely ignore everything the ray starts inside of. If you shoot a ray starting from the center of your head, it can't hit your head. The problem is that objects can slightly interpenetrate. A ball sitting on a table but have a tiny overlap. A ray shot downwards from the bottom of the ball may be slightly inside of the table, incapable of registering a hit on it.
The docs don't always do a great job telling you the units.
World coordinates are unit-less. Technically in meters, but not really.
Various screen coordinates are in 0-1 from lower-left (viewport coords); or in pixels (from lower-left or upper-right.) Canvases are in virtual pixels, scaled to the actual screen size (depending on settings,) with Origin and Direction user-settable. But at the same time, canvases can also exist in the 3D world, where the units are in meters (this mode is useful for mixing 3D objects with menus).
Physics velocity is meters/second. Physics rotation speed is in degrees/second. Many other physics settings aren't specified, for example, spring springiness.
You usually won't need to hand-load and save files, hand-load assets, or hand-build complex items.
Read-only data can often be saved through a script. For example, preset clown data:
class ClownDataHolder : Monobehavior { [System.Serializable] public class ClownData { public string clownName; public string[] tricks; public float hourlyRate; } public ItemData[] Items; }
This would need to be on a gameObject in some scene, and wouldn't exist when the scene wasn't loaded. A work-around is putting in on a prefab, where it's a global Asset. ScriptableObjects are prefabs specially made to only hold data. This awkward code allows us to create new clown-dtat-holders in our Assets area:
// this first line creates an entry in Unity's menu system, // we need it to be able to create these: [CreateAssetMenu (menuName="scriptableObject/ClownData", fileName="sampleItemData")] // then a normal class for data: class ClownDataHolder : ScriptableObject { [System.Serializable] public class ClownData { ...
For saving data created while running, there's a simple read/write file named PlayerPrefs. Sample use: PlayerPrefs.SetFloat("quitTime",1.5f);
and name2=PlayerPrefs.GetString("name2");
.
If you need to, you can read and write actual files. To make it work cross-platform, Unity provides two device-specific strings: Application.dataPath to read-only files (things which came from your project's Resources folder) and Application.persistentDataPath to a directory for read/write files (I'm leaving out an example -- it has a few tricky-yet-boring parts).
Locating various resources can be done the long way: cowPic=Resources.Load("cows/cow1.jpg"). But it's almost always easier to have Unity keep a link:
public Texture2D cowPic; // we've dragged in cow1.jpg
In a build, Unity attempts to remove anything you're not using. It knows how to track Inspector links, but not anything you Resources.Load using a path. To handle that, any directory, anywhere, named Resources is always included in a build.
Unity changes editors as it needs. As of 2021 it uses Visual Studio Code (which is actually the open source monodevelope with a microsoft layer on top). It also runs on a Mac. It's finicky, but it will do code completion with Unity's API.
You shouldn't need to ever use the Compile button or make a project. Unity recompiles when you go back to any Unity window.
You should never need to use "tags". They're a traditional way of adding one piece of information to an object -- tag it as "monster" or "pickup" or "firezone". But we already know how to add a small firezone script and check for that, which is better.
But layers are real, and affect colliders: raycasts can be set to ignore certain layers; the physics system can have any two layers not interact. Cameras skip rendering certain layers... .
There's a LayerMask class which you can skip if you know bitfields. 1<<3 | 1<<7 is the layermask for layers 3 and 7.
transform
. The Material on a Renderer is material
, and
so on.transform.position.x=6
won't
work. position
is a GET, returning a copy of the position, and assigning
x is useless.public int turns=7;
and the Inspector says 9, the value of turns
is 9. The actual sequence is: create instance, apply Inspector values, run Awake/Start.
Comments. or email adminATtaxesforcatses.com