Saturday 15 April 2017

Creating primitives and textures in Unity

I love Unity. I love that you can write code and compile it to multiple platforms. I love that you can "hit up" the Asset Store and have a game working in a couple of hours. At least, a simple game.

But one of the things I've always fancied doing with Unity was have it load levels (from a web server perhaps) and create rooms and playing areas dynamically. We've played about with doing just that using pre-bought assets (it's not as easy as you think, if you're working on a grid-based system, since most assets have their origin in the dead centre, not on one corner!)

So as a bit of an experiment, we played about with creating a map "plane" from primitives, onto which we'll dynamically load textures. So at the start of the "game" there's nothing on screen - then a few script calls and we'll create some primitive shapes (after all, most floors and walls are not much more than simple rectangles) and apply some textures.

It's worth noting that we're creating a 2D top-down type map, even though we're using 3D shapes (the 3d shapes allow us to work with complex principles such as rotation and line-of-sight later on down the line).


We've set up our camera as orthographic and have it pointing straight down. We also added a directional light and made this a child of the camera - effectively following it as it moves over the map. We also created a "gameWorld" empty gameobject just to hold all our dynamically generated content, in case we need to turn the global world on/off  for some reason in the future.

Now a couple of scripts to actually generate our primitive shapes and to apply textures to them. We're working on a grid-based map and each object we create in our game-world will be placed from the bottom-left corner:



But when you create a gameobject in Unity, the origin of the object is smack-bang in the centre. Which makes getting everything to line up in a grid a bit of a pain (especially if the objects are not perfectly square).


So whenever we create an object that we want to align on our grid, we "wrap it up" inside an empty gameobject and set the local x/y co-ordinates to half the height/width of the object. This way we can place our floors and walls without having to keep applying an offset to get the origin somewhere near the bottom-left corner.


With the gameobject in worldspace, placed at 0,0 half of the floor tile is beyond our 0,0 position (ok, it's only a quarter section, but you get the idea)



By placing the tile inside an empty game object, we can place the parent at 0,0 and offset the child by half the height/width and get our tile to appear where we want it "in world space".

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class object_creator : MonoBehaviour {

    Material mat;
    Shader shdr;   

    // if you're using one-square to one-unity-unit keep track of it here
    // (in earlier versions, a 0.5 scaled plane - 5Uunits - represented a board
    // of 8x8 grid, in which case square size would be 5/8 = 0.625)
    private float square_size = 1f;

    // Use this for initialization
    void Start () {
       
    }

    void Awake(){
        shdr = Shader.Find ("Sprites/Default");
        if (shdr) {
            mat = new Material (shdr);
        } else {
            Debug.Log ("wtf");
        }
    }

    // Update is called once per frame
    void Update () {
       
    }

    public GameObject createObject(string objName, GameObject objParent, float x, float y, float z, float size_x, float size_y, float size_height){

        // creates a primitive (cube) wrapped inside an empty game object
        // which is placed at the gameworld position x,y       

        // the position of the (empty) game object is such that the origin is in the
        // bottom-left corner (not the centre as is usual with gameobjects)
        GameObject piece = new GameObject();
        piece.name = objName;
        piece.transform.parent = objParent.transform;
        piece.transform.localPosition = new Vector3 (x, z, y);
        piece.transform.Translate(new Vector3(-square_size/2, 0, -square_size/2));

        GameObject cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
        cube.transform.parent = piece.transform;
        cube.transform.localPosition = new Vector3 (size_x/2, 0, size_y/2);
        cube.transform.localScale = new Vector3 (size_x, size_height, size_y);

        return(piece);
    }

    public void setTexture(GameObject o, string imageName){
        // get the child object in "o" with the name "Cube"
        // (this is the actual shape, the game object is the container)
        GameObject p = o.transform.FindChild("Cube").gameObject;
        // download the texture for this object
        string url="http://your_url/" + imageName + ".png";        
        StartCoroutine (downloadImage(url, p));
    }

    IEnumerator downloadImage(string url, GameObject o){
        if (url.Length > 0) {
            Debug.Log ("loading from " + url);
            WWW www = new WWW (url);
            yield return www;
            Texture2D tex = new Texture2D (www.texture.width, www.texture.height);
            www.LoadImageIntoTexture(tex);
            o.GetComponent<Renderer> ().material = mat;
            o.GetComponent<Renderer> ().material.mainTexture = tex;
            o.GetComponent<Renderer> ().material.shader = shdr;           
            Debug.Log ("Texture set");
        }
    }
}


Our "object creator" script is referenced by our "game controller" script.
When any primitive is created, it needs to be given a material to apply to it; so we create a global material, based on the "sprites/default" shader. This same material can be applied to all our primitive shapes. With a material applied, we can then change the texture property of each shape, with a newly-downloaded image, if necessary.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class game_controller : MonoBehaviour {

   public GameObject world;
   public object_creator oc;
   
   // Use this for initialization
   void Start () {      
      GameObject o;
      o = oc.createObject ("b1", world, 0f, 0f, 0f, 8f, 8f, 0.05f);
      oc.setTexture(o,"board1");

      GameObject o2 = oc.createObject ("b2", world, 8f, 0f, 0f, 8f, 8f, 0.05f);
      oc.setTexture(o2,"board2");      
   }

   
   // Update is called once per frame
   void Update () {
      
   }
}

This script creates two "map tiles" each 8x8 units in size. It places the first at 0,0 and the second at 8,0 (immediately to the right of the first one). The script downloads the image board1.png and applies it to the first tile, and downloads the png image board2.png and applies it to the second tile.

The end result looks something like this:


When we place an object at 0,0 (in world space) it appears in the first square, from the bottom-left corner of the map. If we change the co-ordinates of the object to 3,4 in world space, it appears four squares in and five squares up from our "board origin" in the bottom-left corner of the map (remember our map starts at zero, so at x3, the object should appear on the fourth square in).


A liberal sprinkling of iTween functions and a simple download-map-data-via-xml and we're on the way to creating a top-down game which can load map layout data (and sprites/images) from a website - online map editing here we come!

No comments:

Post a Comment