Unity3D for programmers

home

 

Overview, Reassurances

Unity3D is a particularly programmer-friendly game engine. Others are designed to be good at one thing -- puzzles or top-down shooters -- and they provide limited, custom coding languages. Unity decided to be general purpose: need a 2D grid? You know how to use arrays, and Unity's API provides everything to display the game pieces and place the camera.

But there's lots of lots of game-engine stuff to learn -- everyone in the 3D game-making world knows what raycasting against a collider is, and why making something just "red" is non-trivial. But after figuring out that stuff, coding Unity scripts is 98% normal programming.

Some reassurances that Unity is fine:

If you've used another game engine

Compared to UnReal: Unity seems like it has nothing useful. No pre-made players, or doors or elevators, or dialogue trees. The Unity store might have a 3rd party part you need, maybe. Most likely you're coding elevators 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.

If you've haven't used a game engine before

Unity uses the standard parts of game engines, which can seem strange. I'll try to explain a few:

Main entry, Hello World

To make Hello World in Unity, follow the instructions to make a new script and add void Start() { print("Hello"); }. Unity won't run that yet. You have to attach the script onto an item in the Scene panel -- the Camera or the Light or a New Game Object you've made. Then pressing the Run button will print "Hello".

Unity has no main entry point. It goes through every item in the game-world (the Scene), looks for scripts on them (an item could have several), searches for functions matching void Update(), and runs them.

The philosophy is that each item has a script independantly controlling it: a carMove script goes on each car to drive it, and so on. Then, as needed objects can check how they bumped into (for example) and tell something to that script

Object organization

Items in the game world (the Scene panel) are instances of the class GameObject. Each has just a name and a single flat list of subparts, of type Component. Those determine what each game object "is": the Camera is a game object with a Camera component added, while a shrub has two components: a Mesh (with a shrub mesh dragged in) and a MeshRenderer (with a dragged-in link to a shrub-like Material):

GameObject (a camera)
  Components:
    | Transform: with position, rotation, scale
    | Camera: with type, angle, max distance
    
GameObject (a shrub)
  Components:
    | Transform: (ditto)
    | MeshFilter: [mesh: "Shrub1"]
    | MeshRenderer: [Material: "shrubPic1"]
    | CapsuleCollider (tells physics we take up space)

I got fancy and decided the shrub also needed a collider to prevent thrown frisbees from passing through it.

A Transform is always the first component (which you might recall, holds location, rotation and scale). It might seem more natural to have that information right up there in the game object, but this way is traditional and works well enough.

Scripts are components

SCripts inherit from Monobehavior which inherits from Behavior which inherits from Component. So scripts are Components. They go in the component list next to Meshes and Cameras.

Of course, scripts are just classes. Dragging them onto a game object signals you want an instance created and put into the list for that gameObject. And of course all of a scripts "global" variables are instance variables.

Classes Behavior and Monobehavior have no functionality. All they do is signal the engine to search for and call the magic functions -- Update(), Start() and so on.

Trees

A gameObject just has one flat component list -- no trees of components. But you can make trees of game objects (simply drag them in or out of one another, in the Scene panel). This is the simplest way to make, say, a barbell -- create two balls and a rod and drag them as children of some empty game object.

There's no overall Scene parent, and generally no need. But it's common to use empty game objects as folders -- create a a game object named "car_lot" and drag in every car.

Creating objects

Prefabs over manual construction

Unity's API has commands to build game objects from scratch, entirely through code, but you'll never need them. The prefab system does everything you need. Build the object by dragging and set values through the Inspector, install it as a prefab. And then clone it through the Instantiate command. Unity will gladly clone complex multi-part items.

You'll never need to use new GameObject() or go1.AddComponenet<Rigidbody>.

The manual is a bit confusing about prefab copies because there are two ways of making them. A level designer can drag copies into the scene and they remain linked to the original. This is great -- if you update the prefab the copies also update. But you can ignore all that stuff with Instantiate, It's just a standard clone, with no wierd extra linkage.

Finding Resources

You'll need to find these prefabs, and will also probably want to swap around meshes and textures and sounds. You could fetch them directly using Resources.Load, but it's almost always simpler to pre-populate links. This displays in the Inspector with spots for two prefabs, two images and as a resizable list of Meshes for us to populate:

public class forestManager: MonoBehavior {
  public Mesh[] Trees;

  public GameObject catPF1, catPF2; // links to cat prefabs
  public Texture2D ccatFacePic1, catFacePic2;

Note that prefabs are just type GameObject. We should drag in a prefab from the Resource panel, but nothing stops us from dragging in one from the scene.

Likewise, Instantiate should use a true prefab -- Instantiate(catPF1) -- but there's nothing preventing it from being used on an existing in-scene game object. That works, but there's no reason to do it.

Linking to other objects

Any link to any part of a game object can be used to find any other. So generally, instead linking to a gameObject, we'll link to the component we're most interested in using:

// link to an existing gameObject, but through it's transform:
public Transform mySuitcase;

// ditto:
public Rigidbody myOrbOfPower;

This allows us to write simply mySuitcase.position (instead of the longer mySuitcase.transform.position), and allows us to easily use myOrbOfPower.velocity.

Since "scripts" are components, the same trick works linking to them:

public carScript myCar; // link to car gameObject, through script component
  ...
  myCar.beginDriving();

  // of course we can still access the gameObect:
  myCar.transform.name="Herbie";

This also works for prefabs. Instantiate gladly takes a link to any component of a prefab, returning a link to the parallel component in the new one:

// link to a prefab cow, but through the cowScript component:
public cowScript cowPF1;
  ..
  cowScript c1 = Instantiate(cowPF1);
  // c1 is a link to the cowScript component on the new cow
  c1.eatGrass(); // let's use it right away!

  // of course we can use this to access any other part:
  c1.transform.position = Vector3(12,0,-5);

Navigating objects

Within an object

Every component has a backlink named gameObject (scripts also have this, since they're components). We can find any component through GetComponent<className>. And again, this works with scripts since they're components:

  // get my rigidbody ("physics") component and reset speed:
  Rigidbody myRB = GetComponent<Rigidbody>();
  myRB.velocity = Vector3.zero; // stop moving

  // change my main color to red:
  GetComponent<Renderer>().material,color=Color.green;

  // I have a link to a cow, get it's cowScript:

c1.GetComponent<cowScript>().eatGrass();

Code for Trees

Strangely, parent/child relationships are stored in the Transform. The link to the parent is parent:

if(transform.parent==null) ... // do no parent stuff

// turn my cow's parent red:
myCow.parent.GetComponent<Renderer<().material,color=Color.red;

We can change parents by re-assigning: transform.parent = pasture2; or transform.parent=null; to un-child ourselves.

Children can be found using getChild(i) and childCount:

if(transform.childCount>0) {
  Transform firstChild = transform.getChild(0);
  ...

for(int i=0; i<transform.childCount; i++) {
  Transform c1 = transform.getChild(i);

Or, in a really weird move, Unity decided foreach on a transform runs through it's children (they wrote an iterator for it):

foreach(Transform child in transform) {
  // child goes through all of our children

You can also search for children by name, through transform.Find:

// looks for a child of dog named leftLeg:
Transform leg1 = dog.Find("leftLeg");

Misc

Misc scripts

Data-only scripts

There may be objects which don't need a script with Update() but have some data associated with them. It might feel natural to store that data in some master list, including a link to the object. But it's useful to store that data in a tiny script on the object. Here each ball is worth a certain number of points and knows when it was created:

// hold what we need to know about each ball:
class ballData : Monobehavior {
  public int points; // how many points this ball is worth
  public float creationTime;

  public int myIndex; // in some ball array we have
}

What happens is we often just find a gameObject -- the player collided with it, or mouse-clicked on it. In that case we don't know which list it's stored in, at what index. It's nice to be able to determine it's a ball (from the mere existence of that script on the object) and then any data we need is right there:

  ballScript b = thingIHit.GetComponent<ballScript>();
  if(b!=null) {
    // we hit a ball! do ball stuff...
    // ... possibly also use b.points and b.myIndex

Main entry, part II

If you want to have a single entry, controlling everything else, you can do it, and it works fine. Assume our game has lots of balls. We have a ball prefab with our tiny ballScript attached. Our master class creates twenty, in a list, and moves them later:

...
public class allBallMover: MonoBehavior {
  // ball prefab (it has a ballScript, which we use as our link):
  public ballScript ballPF;

  // links to created balls:
  public Transform[] Balls;

  void Start() {
    Ball=newballScript[20];
    for(int i=0;i<20;i++)
      Ball[i] = Instantiate(ballPF);
    // position the balls and so-forth
  }

  void Update() {
    for(int i=0;i<Balls.Length;i++) {
      Balls[i].performMove(); // call member functions
      Balls[i].transform.position+= ... // or move by hand
  }
}

Faking multiple inheritance

One gameObject will gladly hold several scripts. A clever Unity style make modular objects that way. Have data scripts Monster, Collectable, Animal and Robot. A gameObject could mix-and-match to be a collectable robot monster, or whatever, checking "am I a robot" with GetComponent<Robot>(), and so on.

Or simpler, some objects have an optional speech script, used like this:

  // apply injuryPoints of damage
    ...
  // and if we talk, say ouch:
  speechScript ss = GetComponent<speechScript>();
  if(ss) ss.makeHurtSound(injuryPoints);

Inheritance and scripts

Inheritance from Monobehavior works the normal way. Here weasel inherits from pet which inherits from MonoBehavior:

public pet : MonoBehavior {
  public int cost;
  public string getCost() { return "$"+cost; }
}

public weasel : pet { // this is a "script" (a Monobehavior)
  int stinkiness;
  
  void Update() { ... }
}

weasel acts like any other script -- you can drag it into a gameObject and the Inspector will correctly create slots for cost and stinkiness. As you'd expect, GetComponent<pet>() can find a weasel, and a variable of type pet can point to a weasel.

Synching, timing

Together, Awake() and Start() are meant as a crude way to control initialization order of multiple objects. All Awake()'s are run first. In theory normal initialization should be done there. Then Start()'s are run, which gives a place to init object A when it depends on B or C having been initialized.

In other timing, the system runs everyone's Update() in an arbitrary (but fixed) order. That's usually fine. If you need to force an order, there's a special Script Execution Order Window. Or rename all Updates() and have a master script run them however you want.

Unity might run at 30 Updates a second, or 60, or might vary. The standard fix is using the time passed since the previous update, which Unity provides in Time.deltaTime. To increase by 3 units a second, add Time.deltaTime*3.0f each update. And then Time.time gives the total seconds since the game was started.

All timing in Unity is done in terms of Updates. For example, waiting coroutines are only checked after each Update. Waiting for 0.2 seconds in reality means waiting for 11 or 12 or 13 Updates to pass. Waiting for tiny amounts, like 0.01 seconds, is the same as waiting for the next update.

FixedUpdate() always runs 50 times a second, but not evenly spaced. It's possibly run after each Update, skipped as needed, or run twice (it's use is to run code exactly in step with the automatic Physics system, which you rarely need to do).

Inputs (moouse clicks, moves, tuches) are also only checked between Updates. They can only safely be checked there (otherwise you'll likely miss some or see some twice).

Threads, sleep (coroutines)

Normal Unity functions can't sleep, but you're allowed to spawn thread-like things which can, called coroutines. This coroutine pauses for 2 seconds, then grows us a little each frame until we pop into nothingness:

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
  }
  Destroy(gameObject); // kill ourself
}

// start a copy of it running:
StartCoroutine(explodeBall());

Coroutines are run in the same thread as everything else -- so no race condition issues -- and, again, only runs immediately after Update. Waiting 0.25 seconds means waiting until the first Update after which 0.25 seconds have passed.

Coroutines even have handles. This example runs two copies of a coroutine, then later decides to stop the second:

IEnumerator c1, c2; // handles, 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

Back to that first example, it runs faster or slower based on the frame rate. It's a nice example of what Time.deltaTime is for. A better version would use it to always explode after 2 seconds, something like: while(scaleAdd<2) { scaleAdd+=Time.deltaTime*2.0f; transform.localScale=(1+scaleAdd); ... .

Infinite loops

The most horrible thing about Unity is how an infinite loop in a script freezes the entire editor. You can't even press the Stop button. The only thing you can do is force close the whole program.

Using regular classes

Normal classes work fine in Unity. But you'll need to add [System.Serializable] if you want them displayed in the Inspector. Here we have a list of dogs currently in the dog show:

public class dogShowRunner: MonoBehavior {

  // an ordinary class:
  [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;
  }

  public List<showDog_t> SD;

We can also write standard classes, in regular files. Use the normal create->Script, delete the guts and write a class as normal. A file with a utility class and something for longer xyz's:

class miscStuff {
  // Random.Range is in the unity API
  public static int roll2To12() {
    return Random.Range(1,7)+Random.Range(1,7);
  }

  public static Transform getMaximumAncestor(Transform T) {
    while(T.parent!=null) T=T.parent;
    return T;
  }
}

struct DVec { public double x,y,z };

These can't start coroutines, since coroutines must be attached to the gameObject which started them. Our files can have coroutines, waiting to be called by "real" scripts (sadly, this means we can't give our off-site coroutines any drivers).

Destroy and smart nulls

Unity has a hack where destroying a game object magically turns every reference to it, anywhere, into null. Of course what they really do is set the object's dead flag and overload gameObject==. In other words, you would normally need to check if(g==null || !g.isAlive), but Unity does that for you:

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)

This can bite you with new non-overloaded stuff like g is null.

General Notes

Physics

The physics system can generate callbacks whenever two objects collide. Oddly, you don't "register" your callback functions. Instead they search for the preset function name. Any function named exactly OnCollisionEnter() is called when your collider hits another.

The easy way to start "physics objects" movingis to set (or change) GetComponent<Rigidbody>().velocity directly. Unity advertises an AddForce command for this, but it's confusing and a relic from older physics systems.

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 colliders the ray starts inside of. If you shoot a ray starting from the center of your gun, it can't hit your gun. That's great, but if you shoot a ray from just past the tip of your gun, at something your gun is touching, it can't hit that either.

Units, coordinate systems

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 can run 0-1 from the lower-left -- (1,1) is the upper right. These are viewport coords. But others are in pixels (sometimes from lower-left, some from upper-right.) Canvases are in "virtual" pixels, and have user-setable Origin and Direction. But some canvases exist in the 3D world, where the units are in meters (this mode is useful for mixing 3D objects with menus). So good luck.

Physics velocity is units/second (which in theory is meters/second). For example, (user-adjustable) gravity starts at 9.8. Physics rotation speed is in degrees/second. Many other physics settings aren't specified, for example, spring springiness.

Files, data, loading

Traditionally Unity data is entered through the Inspector, letting Unity auto-serialize it. We might create this script, on an empty gameObject, enter data through the Inspector, and we've got all of our clown data saved:

class ClownDataHolder : Monobehavior {

  [System.Serializable]
  public class ClownData {
    public string clownName;
    public string[] tricks;
    public float hourlyRate;
  }

  public ItemData[] Items;
}

Even better, we can put that in on a prefab and now it's a global Asset accessable from any scene. ScriptableObjects are prefabs specially made for this. This awkward code turns clown data into a ScriptableObject:

// this first line creates an entry in Unity's menu system,
// ... the only way to create these:
[CreateAssetMenu (menuName="scriptableObject/ClownData", fileName="sampleItemData")]

// then a normal class for data:
class ClownDataHolder : ScriptableObject {
  [System.Serializable]
  public class ClownData {
    ...

But we can also have real files. Read your data from them using your cutom format, if you like. To make it work cross-platform, Unity provides two device-specific strings: Application.dataPath is to read-only files (files you bundled with the game, in some Resources folder) and Application.persistentDataPath to a directory for read/write files.

We can also save data in PlayerPrefs. It's a list of name/data, which understands string, int and float: PlayerPrefs.SetFloat("quitTime",1.5f); and myQuitTime=PlayerPrefs.GetFloat("quitTime");.

Locating various resources can be done the long way: cowPic=Resources.Load("cows/cow1.jpg"). But again, 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 any Assets you're not using, which is nice. It knows how to track Inspector links, but not things you got with Resources.Load. To handle that, any directory, anywhere, named Resources is always included in a build.

Code Editor

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.

Layers/Tags

You should never need to use "tags". They're a traditional way of adding one piece of information to an object -- tag something as "monster" or "pickup" or "firezone". Tagging things by attaching a small script is better.

But layers are real. Raycasts can be set to ignore certain layers. The physics system can have particular layers not interact. Cameras can be set to skip displaying ("rendering") certain layers.

There's a LayerMask class, but you don't need it if you know bitfields. 1<<3 | 1<<7 is the layermask for layers 3 and 7.

More misc

 

 

Comments. or email adminATtaxesforcatses.com