Tuesday, 23 August 2016

Alex's Student Game Dev

 Summary

In this post I will try to explain, how Alex's package works and how to create your own project with it.

 Thanks

First of all, thanks to the blog creator for making an awesome package.All of the following code belongs to Alex.

Why should you choose this package?

 The answer is fairly simple. This is a great project for starters, who want to create something good without much trouble. There are tutorials which cover all of the package's files, so you don't need to try to find out what every element does. However, for those, who don't want to waste time reading tutorials, this post provides a quick explanation of everything.

Folders and files:

  •  Art
    • Materials
      • tilesheet.mat
    • tilesheet.png 
  • Prefabs
    • Chunk.prefab
  • Scripts
    • Level
      • Chunk.cs
      • ModifyTerrain.cs
      • Noise.cs
      • PolygonGenerator.cs
      • World.cs
    • Utility 
      • ColliderExample.cs
      • RaycastExample.cs
  • Standard Assets
    • Character Controllers
      • Sources
        • Scripts
          • MouseLook.cs
          • CharacterMotor.js
          • FPSInputController.js
          • PlatformInputController.js
          • ThirdPersonCamera.js
          • ThirdPersonController.js 
  • Tutorial (Unity Scene)
  • Tutorial 2 (Unity Scene) 

 Art

Art is the folder, where all textures and materials are placed. In the package, there is only one texture, which is tilesheet.png. The file contains 4x4=16 pieces of 32x32 textures, which are used in the game. You can find references of tilesheet.png in Chunk.cs.

Prefabs

This folder contains the Chunk prefab, which has the following components: Mesh Filter, Mesh Renderer, Mesh Collider, Chunk.cs, tilesheet material.

Scripts / Level

This is the most important folder, where we'll go through all of the scripts, that we need.

Noise.cs

Before looking at the code, look at it's source.

World.cs

This script is used for handling the block and chunks data.
 public GameObject chunk; //Reference to the Chunk.prefab  
    public Chunk[,,] chunks; //Array of chunks which contain Mesh Data  
    public int chunkSize = 16; //Length, width, height of the chunk  
    public byte[,,] data; //Data, containing all of the block ID  
    public int worldX = 16; //Length of the world  
    public int worldY = 16; //Height of the world  
    public int worldZ = 16; //Width of the world  
 int PerlinNoise (int x, int y, int z, float scale, float height, float power) //Generates modified Simplex Noise  
    {  
       float rValue;  
       rValue = Noise.GetNoise (((double)x) / scale, ((double)y) / scale, ((double)z) / scale);  
       rValue *= height;  
       if (power != 0) {  
          rValue = Mathf.Pow (rValue, power);  
       }  
       return (int)rValue;  
    }  
 void Start ()  
    {  
       data = new byte[worldX, worldY, worldZ]; //Set the Array.Length  
       for (int x=0; x<worldX; x++) { //For every block  
          for (int z=0; z<worldZ; z++) {  
             int stone = PerlinNoise (x, 0, z, 10, 3, 1.2f);  
             stone += PerlinNoise (x, 300, z, 20, 4, 0) + 10; //Max stone height  
             int dirt = PerlinNoise (x, 100, z, 50, 3, 0) + 1; //Max dirt height  
             for (int y=0; y<worldY; y++) {  
                if (y <= stone) {  
                   data [x, y, z] = 1; //Stone  
                } else if (y <= dirt + stone) {  
                   data [x, y, z] = 2; //Dirt  
                }  
             }  
          }  
       }  
       chunks = new Chunk[Mathf.FloorToInt (worldX / chunkSize), Mathf.FloorToInt (worldY / chunkSize), Mathf.FloorToInt (worldZ / chunkSize)]; //Create chunks for every chunkSize*chunkSize*chunkSize blocks  
    }  
 public byte Block (int x, int y, int z) //Test if block is in world range  
    {  
       if (x >= worldX || x < 0 || y >= worldY || y < 0 || z >= worldZ || z < 0) {  
          return (byte)1; //Stone  
       }  
       return data [x, y, z];  
    }  
 public void GenColumn(int x, int z){ //Create a chunk  
       for (int y=0; y<chunks.GetLength(1); y++) { //For each chunk from bottom to top  
                //Create a temporary Gameobject for the new chunk instead of using chunks[x,y,z]  
                GameObject newChunk = Instantiate (chunk, new Vector3 (x * chunkSize - 0.5f,  
  y * chunkSize + 0.5f, z * chunkSize - 0.5f), new Quaternion (0, 0, 0, 0)) as GameObject;  
                chunks [x, y, z] = newChunk.GetComponent ("Chunk") as Chunk;  
                chunks [x, y, z].worldGO = gameObject;  
                chunks [x, y, z].chunkSize = chunkSize;  
                chunks [x, y, z].chunkX = x * chunkSize; //Set actual world coordinates  
                chunks [x, y, z].chunkY = y * chunkSize;  
                chunks [x, y, z].chunkZ = z * chunkSize;  
          }  
    }  
    public void UnloadColumn(int x, int z){ //Destroy a chunk  
       for (int y=0; y<chunks.GetLength(1); y++) {  
          Object.Destroy(chunks [x, y, z].gameObject);  
       }  
    }  

Chunk.cs

This script is used for creating a chunk model, which we can use and modify.
 public GameObject worldGO; //world gameObject  
  private World world; //world script  
 //Following is used for mesh generation  
  private List<Vector3> newVertices = new List<Vector3>(); //List of points of mesh  
   private List<int> newTriangles = new List<int>(); //List of triangles  
  private List<Vector2> newUV = new List<Vector2>(); //List of UV mapping coordinates  
 private float tUnit = 0.25f; //1 divided by number of textures in tilesheet  
 //Following are texture coordinates in tilesheet, where (0,0) is bottom left  
    private Vector2 tStone = new Vector2 (0, 0);  
    private Vector2 tGrass = new Vector2 (3, 0);  
    private Vector2 tDirt = new Vector2 (1, 0);  
    private Vector2 tGrassTop = new Vector2 (2, 0);  
    private Vector2 tWater = new Vector2 (0, 1);  
  private Mesh mesh; //Temporary mesh  
  private MeshCollider col; //Collider for the mesh  
  private int faceCount; //For triangle generation  
  public int chunkSize=16; //Number of blocks per line in a chunk  
  public int chunkX; //Actual world coordinates  
  public int chunkY;  
  public int chunkZ;  
  public bool update; //for mesh updates  
  void LateUpdate () { //Generate mesh if needed  
  if(update){  
   GenerateMesh();  
   update=false;  
  }  
  }  
 void Start () {   
  world=worldGO.GetComponent("World") as World; //Find world script  
  mesh = GetComponent<MeshFilter> ().mesh; //Assign mesh to the instantiated prefab  
  col = GetComponent<MeshCollider> (); //Assign collider  
  GenerateMesh(); //Create the mesh  
  }  
 byte Block(int x, int y, int z){ //Convert local block coodrinates to world coordinates  
  return world.Block(x+chunkX,y+chunkY,z+chunkZ);  
  }  
 //Same for other functions  
 void CubeTop (int x, int y, int z, byte block) { //Create the top plane of a cube  
   //Add four points to the mesh for the plane  
  newVertices.Add(new Vector3 (x, y, z + 1));  
  newVertices.Add(new Vector3 (x + 1, y, z + 1));  
  newVertices.Add(new Vector3 (x + 1, y, z ));  
  newVertices.Add(new Vector3 (x, y, z ));  
 //Following code is used for texturing the plane  
  Vector2 texturePos=new Vector2(0,0);  
  if(Block(x,y,z)==1){  
   texturePos=tStone;  
  } else if(Block(x,y,z)==2){  
   texturePos=tGrassTop;  
  }  
  Cube (texturePos);  
  }  
 void Cube (Vector2 texturePos) {  
   //Set the triangles  
  newTriangles.Add(faceCount * 4 ); //1  
  newTriangles.Add(faceCount * 4 + 1 ); //2  
  newTriangles.Add(faceCount * 4 + 2 ); //3  
  newTriangles.Add(faceCount * 4 ); //1  
  newTriangles.Add(faceCount * 4 + 2 ); //3  
  newTriangles.Add(faceCount * 4 + 3 ); //4  
   //Set the texture, put it on the plane  
  newUV.Add(new Vector2 (tUnit * texturePos.x + tUnit, tUnit * texturePos.y));  
  newUV.Add(new Vector2 (tUnit * texturePos.x + tUnit, tUnit * texturePos.y + tUnit));  
  newUV.Add(new Vector2 (tUnit * texturePos.x, tUnit * texturePos.y + tUnit));  
  newUV.Add(new Vector2 (tUnit * texturePos.x, tUnit * texturePos.y));  
  faceCount++; // Add this line  
  }  
 public void GenerateMesh(){  
   //Cycle through all of the blocks  
  for (int x=0; x<chunkSize; x++){  
   for (int y=0; y<chunkSize; y++){  
   for (int z=0; z<chunkSize; z++){  
    //This code will run for every block in the chunk  
    //If required, generate a part of a cube  
    if(Block(x,y,z)!=0){  
    if(Block(x,y+1,z)==0){  
     //Block above is air  
     CubeTop(x,y,z,Block(x,y,z));  
    }  
    if(Block(x,y-1,z)==0){  
     //Block below is air  
     CubeBot(x,y,z,Block(x,y,z));  
    }  
    if(Block(x+1,y,z)==0){  
     //Block east is air  
     CubeEast(x,y,z,Block(x,y,z));  
    }  
    if(Block(x-1,y,z)==0){  
     //Block west is air  
     CubeWest(x,y,z,Block(x,y,z));  
    }  
    if(Block(x,y,z+1)==0){  
     //Block north is air  
     CubeNorth(x,y,z,Block(x,y,z));  
    }  
    if(Block(x,y,z-1)==0){  
     //Block south is air  
     CubeSouth(x,y,z,Block(x,y,z));  
    }  
    }  
   }  
   }  
  }  
 void UpdateMesh ()  
  {  
   //Clear the existing nesh  
  mesh.Clear ();  
  mesh.vertices = newVertices.ToArray(); //Set the required elements for the mesh  
  mesh.uv = newUV.ToArray();  
  mesh.triangles = newTriangles.ToArray();  
  mesh.Optimize (); //Let the mesh do the things itself  
  mesh.RecalculateNormals ();  
  col.sharedMesh=null; //Clear the Collider's mesh  
  col.sharedMesh=mesh;  
  newVertices.Clear(); //Clear our lists and reset variables  
  newUV.Clear();  
  newTriangles.Clear();  
  faceCount=0;  
  }  

ModifyTerrain

This script is used to modify the terrain, use the World's column functions to manage chunk loading relative to the player.

 World world; //reference to World script  
    GameObject cameraGO; //reference to Camera of Player  
    void Start () {  
       world=gameObject.GetComponent("World") as World; //assign the World script  
       cameraGO=GameObject.FindGameObjectWithTag("MainCamera"); //assign the Camera     
    }  
 void Update () {  
       if(Input.GetMouseButtonDown(0)){ //Destroy Block  
          ReplaceBlockCenter(5,0);  
       }  
       if(Input.GetMouseButtonDown(1)){ //Place Block  
          AddBlockCenter(5,255);  
       }  
 LoadChunks(GameObject.FindGameObjectWithTag("Player").transform.position,32,48); //Manage chunk loading  
    }  
 public void LoadChunks(Vector3 playerPos, float distToLoad, float distToUnload){  
       for(int x=0;x<world.chunks.GetLength(0);x++){ //for each chunk  
          for(int z=0;z<world.chunks.GetLength(2);z++){  
             float dist=Vector2.Distance(new Vector2(x*world.chunkSize,z*world.chunkSize),new Vector2(playerPos.x,playerPos.z)); //Calculate the distance between chunk and player  
             if(dist<distToLoad){  
                if(world.chunks[x,0,z]==null){  
                   world.GenColumn(x,z); //Generate chunk  
                }  
             } else if(dist>distToUnload){  
                if(world.chunks[x,0,z]!=null){  
                   world.UnloadColumn(x,z); //Delete chunk  
                }  
             }}}  
    }  
 public void ReplaceBlockCenter(float range, byte block);   
 public void AddBlockCenter(float range, byte block); //Do the same: create a Ray from camera to the place it is looking at  
 public void ReplaceBlockAt(RaycastHit hit, byte block);  
 public void AddBlockAt(RaycastHit hit, byte block); //Do almost the same: calculate the position of the block, that the camera is looking at; Replace gets the block, on which you clicked and Add gets the block in front, where the new block will appear  
 public void SetBlockAt(Vector3 position, byte block); //Converts Vector3 to x,y,z  
 public void SetBlockAt(int x, int y, int z, byte block); //Sets the block, modifies the World's data and calls UpdateChunkAt  
 public void UpdateChunkAt(int x, int y, int z, byte block); //Updates the chunk, which represents modified data, if needed, updates the chunks around the modified one  

Conclusion

In conclusion, the package provides a very simple, but powerful voxel engine, which can be used by many to create wonderful games. Personally, I do not recommend to use it as it is, because the whole thing is very limited, there are no save/load features and, if you want a bigger world, you will have to deal with huge arrays of data, which will slow you down.

Next review / tutorial

Now, I will focus on world generation using noise functions, chunks and more! Warning! This will be a probably short post. Date: next Tuesday.

No comments:

Post a Comment