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
- Prefabs
- 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