Now that we have semi realistic terrain we need to be able to manipulate the terrain in real time because that's kind of a staple of these kinds of games or even the point of these kinds of games. The difficulty of this mostly comes down to converting floating point positions from unity to coordinates in our grid array. Our grid array is offset by a little so we need to figure out these offsets. We could also adjust our block generation to align it better to the real positions but this solution is easier.
What we'll start off with is a raycasting script to destroy blocks. Make a new script and call it "RaycastExample". Give it the following variables:
public GameObject terrain;
private PolygonGenerator tScript;
public GameObject target;
private LayerMask layerMask = (1 << 0);
The terrain GameObject will refer to the object with the "PolygonGenerator" script and in the start function we'll get the polygon generator script and save it as the tScript variable:
void Start () {
tScript=terrain.GetComponent("PolygonGenerator") as PolygonGenerator;
}
Now in the update function we'll raycast every frame from this object to the target object destroying the first block hit.
void Update () {
RaycastHit hit;
float distance=Vector3.Distance(transform.position,target.transform.position);
if( Physics.Raycast(transform.position, (target.transform.position -
transform.position).normalized, out hit, distance , layerMask)){
Debug.DrawLine(transform.position,hit.point,Color.red);
} else {
Debug.DrawLine(transform.position,target.transform.position,Color.blue);
}
}
If you haven't used raycasts before I won't go into the basics here but essentially it's the origin of the ray, the direction that we calculate using the origin and the target location, the variable we'll output the hit data to, the max distance of the ray which is calculated earlier as the distance between origin and target, and lastly the layer mask which is which layers this ray collides with. The layermask we've already defined in the start, it's set to 0 which is the default layer. This way you could have characters or entities on another layer and not have them stop the raycasts.
Add the script to a gameobject and create another gameobject to set as the target, make sure they are both at 10 z. Also set the raycaster's terrain variable to the gameobject containing the polygon generator.
If you run it now you should see a line drawn in red to the hit point if it collides and a blue line to the target if not.
The raycast hitting the terrain. |
if( Physics.Raycast(transform.position, (target.transform.position -What these lines do is that they take the position of the collision (hit.point) and create a new vector2 with the position, then we add to that Vector2 half of the reverse of the normal of the surface hit. The normal is the direction that would point straight away from the surface, so adding the reverse takes us 1 unit further into the block but half that takes us half a unit further in where we can round to the position of the block.
transform.position).normalized, out hit, distance , layerMask)){
Debug.DrawLine(transform.position,hit.point,Color.red);
Vector2 point= new Vector2(hit.point.x, hit.point.y); //Add this line
point+=(new Vector2(hit.normal.x,hit.normal.y))*-0.5f; //And this line
} else {
Debug.DrawLine(transform.position,target.transform.position,Color.blue);
}
}
You won't see this but this is a visualization of the raycast to the hitpoint and then a line to the new point with the inverse normals added, you can see that it now ends up inside the block. |
tScript.blocks[Mathf.RoundToInt(point.x-.5f),Mathf.RoundToInt(point.y+.5f)]=0;
This goes after the vector2 point is defined and adjusted. It rounds the point's x and y to ints to that we can use them to choose points in the array. First though you have to subtract .5f from x and add .5f from y because of the terrain's offset from the world coordinates. You wouldn't need this if block 0,0's center was at 0,0 but it isn't.
Now to update the blocks but instead of just rebuilding and updating the mesh remotely we'll use the polygon generator's update to let it do it. Go back to the polygon generator and add a public bool called update.
public bool update=false;
Then create an Update function for the polygon generator. In the Update loop add this:
void Update(){
if(update){
BuildMesh();
UpdateMesh();
update=false;
}
}
This way we can set update to true remotely and the mesh will update but the best part is that even if several scripts change blocks they will and just change update to true but the mesh will only update once for all of them when its Update loop runs.
So set update to true for our polygon generator from the raycast script:
tScript.update=true;
The raycast should destroy one block per frame until it reaches it's target. |
Now that's not all though, sometimes you don't want to destroy a block at a specific point, you want to destroy all the blocks in a general area. For this we'll make a new script "ColliderExample". Give it the following variables:
public GameObject terrain;
private PolygonGenerator tScript;
public int size=4;
public bool circular=false;
And we'll use the same start code to get the Polygon Generator script
void Start () {Because this is going to be removing a lot of blocks at once we'll make a RemoveBlock function:
tScript=terrain.GetComponent("PolygonGenerator") as PolygonGenerator;
}
bool RemoveBlock(float offsetX, float offsetY){
int x =Mathf.RoundToInt(transform.position.x+offsetX);
int y=Mathf.RoundToInt(transform.position.y+1f+offsetY);
if(x<tScript.blocks.GetLength(0) && y<tScript.blocks.GetLength(1) && x>=0 && y>=0){
if(tScript.blocks[x,y]!=0){
tScript.blocks[x,y]=0;
return true;
}
}
return false;
}
What we do here is very similar to the last remove block code we wrote only this time we also check first to see if the block is within the bounds of the array (in case our object is placed close to the edge). Then if the block isn't already air we just set the block to zero and return true to let the script that calls it know that a change was made.
Now in the update loop we'll run the remove block script for all the blocks in an area. I'll just paste in the whole chunk:
void Update () {
bool collision=false;
for(int x=0;x<size;x++){
for(int y=0;y<size;y++){
if(circular){
if(Vector2.Distance(new Vector2(x-(size/2),
y-(size/2)),Vector2.zero)<=(size/3)){
if(RemoveBlock(x-(size/2),y-(size/2))){
collision=true;
}
}
} else {
if(RemoveBlock(x-(size/2),y-(size/2))){
collision=true;
}
}
}
}
if( collision){
tScript.update=true;
}
}
So we run this for each block in a square of size by size, if the circular bool is true then first we check to see if the distance from the origin is smaller than one third the size to create a circular effect. Originally I used half the size consistent with everything else but I found that at low values some sides would be cut off so when set to circular the blast radius is smaller that otherwise. Then for both the circular and noncircular parts we remove the block at that point subtracting half the size (Because otherwise the object would be in the top left of the blast, this offsets it to the center) if the remove block function returns true we set the collision we defined earlier to true. After the loops if anything set collision to true we update the polygon generator, this way we don't update the mesh if nothing was removed.
Apply the script to a gameobject and you can do this. |
For explosive effects |
Now all you have to change is instead of multiplying the normals that you add to the hit point by -0.5f you just multiply by 0.5f to get the block next to the block hit. And instead of setting it to air you set it to whatever you want. For example:
point+=(new Vector2(hit.normal.x,hit.normal.y))*0.5f;
tScript.blocks[Mathf.RoundToInt(point.x-.5f),Mathf.RoundToInt(point.y+.5f)]=1;
And instead of destroying a block every frame you will build one.
It looks kind of freaky. |
And that's part 4, message me with any problems you find or feedback you think of. Follow me on twitter (@STV_Alex) or G+ to get updated when I post part five!
Completed code for the tutorial so far: http://netbook-game.blogspot.no/p/part-4-complete-code.html
Part 5