- Home /
What is the best way to make modular (selectable squares) terrain?
I'm looking for best way to make Dungeon Keeper style terrain (or better map, I don't want to use Unity terrain editor). For those who don't know this old great game:
- Whole map should be made from squares - ground, earth, rock, water, lava, etc.
- Every square should be selectable by player.
- There should be squares which can't be changed by player - rock, water, etc.
- Player can dig through earth (destroy it and and place ground there instead).
- On the ground player can build something (traps, buildings, etc).
I'm looking for best way to script it and a nice way to build such maps after scripting.
Regards,
Oskar
PS I'm new to Unity. I know some programming in C/C++/Obj-C and I'm not afraid of complicated answers but please provide some links to the documentation which I should read if your answer is very complicated.
This is a monster of a question. I'm slowly working my way through a decent answer, but it's not something where a single link will solve everything. Also, as I never played Dungeon $$anonymous$$eeper, I've spent a good deal of time watching Youtube videos to understand what you need, heh.
A monster of an answer to go with. Now, fingers crossed that you make a return visit. ;)
Anyone willing to show the project for this first part of the answer that actually generates the board. I'm having a hard time getting it to work. I'm new to Unity3d and still learning everything.
Or if not i have some specific issues im running into i can elaborate.
THAN$$anonymous$$S!
Answer by · Sep 27, 2010 at 08:18 AM
I recently whipped up a 2D grid game. I believe that "the best way" is fairly subjective when it comes to project design, but this approach worked well enough for me. I've assumed you know very little about Unity, but linking the manual/documentation/scripting-reference where possible, and you can hopefully fill in the gaps.
General concepts
For my game, I worked under the premise that every tile (square) is a separate GameObject.
Working backwards from that decision, every tile type (ground, earth, rock, water, lava, etc.) was its own prefab. Keeping each tile as a separate prefab allows you to quickly change types (even at runtime), and assign different properties via different components on each tile prefab. You can also add and remove components at runtime, so your buildings and traps could be components that are created/destroyed on gameobjects. I've also assumed that you'll use a top-down (rotated 90 degrees on the X axis) orthographic camera.
Specific implementation
To start, you'll need a grid to play around with. I wrote a quick script to build me a 20x20 board of tiles, but you can modify it as necessary.
using UnityEngine;
using System.Collections;
public class GenerateTiles : MonoBehaviour {
public int board_size_x_;
public int board_size_z_;
public Transform tile_prefab_;
// Use this for initialization
void Start () {
GameObject board = new GameObject();
board.name = "Board";
for ( int x = 0; x < board_size_x_; x++ ) {
for ( int z = 0; z < board_size_z_; z++ ) {
Transform tile = (Transform)Instantiate(tile_prefab_,new Vector3(x,0,z),Quaternion.identity);
tile.name = "Tile" + x + z;
tile.parent = board.transform;
}
}
}
}
Add the script to an empty GameObject, hook up the appropriate base tile prefab, set the x and z dimension variables, and run the game (click Play) - your board will be created. Then, while the game is still running, create a new prefab in your Project window, and drag the 'Board' object down to it. Then, stop running the game (click Play again). Drag the prefab from your Project window back up to the Hierarchy and you have yourself a board! With this approach, any changes you make to your base prefabs will echo through to the instances in your Hierarchy.
You can read up on the documentation for Transform and Instantiate on the Scripting Reference. One other thing to note is that C# scripts must have the same name as the class contained within. i.e. the script should be named GenerateTiles.cs
Manually changing over prefabs in your map will be exhausting, so you'll want a way to change which tiles are in your map; i.e. a level editor of some sort. Create some other tile prefabs. For now, you might like to set their colour so you can easily differentiate between them. The quick way to do this is create a new material. On this, you can manually specify the Main Color with the color picker tool in the Inspector properties for the material. Personally, I group similar assets in folders, so the following script assumes that your tile prefabs are in Assets/Prefabs/Tiles/. It's also in javascript, and not the cleanest of code - I just wanted a quick-and-dirty level editor tool. Please also note that I am by no means an expert Unity user - I pieced this together by reading the 'Extending the Editor' page in the manual.
enum TileType { Blank, Ground, Earth, Rock, Water, Lava }
class TileEditor extends EditorWindow
{
var selectedType : TileType = TileType.Blank;
var selectedTile : GameObject;
var passiveSelect : boolean = true;
var tilePrefab : Object;
@MenuItem ("Window/TileEditor")
static function Init ()
{
var window : TileEditor = EditorWindow.GetWindow (TileEditor);
window.Show();
}
function OnSelectionChange ()
{
Repaint();
if ( passiveSelect == false )
ChangeTile();
}
function OnGUI ()
{
GUILayout.Label ("Selected tile: " + (Selection.activeGameObject != null ? Selection.activeGameObject.name : "None"), EditorStyles.boldLabel);
GUILayout.Label ("Type to apply:", EditorStyles.boldLabel);
selectedType = EditorGUILayout.EnumPopup(selectedType);
passiveSelect = EditorGUILayout.BeginToggleGroup ("Manually Apply", passiveSelect);
if (GUI.Button (Rect(10,85,60,20), "Apply"))
ChangeTile();
EditorGUILayout.EndToggleGroup ();
}
function ChangeTile ()
{
selectedTile = Selection.activeGameObject;
if ( selectedTile == null )
return;
if ( selectedTile.layer != LayerMask.NameToLayer("Tile") )
return;
switch ( selectedType )
{
case selectedType.Blank:
tilePrefab = Resources.LoadAssetAtPath("Assets/Prefabs/Tiles/Blank.prefab", Object);
break;
case selectedType.Ground:
tilePrefab = Resources.LoadAssetAtPath("Assets/Prefabs/Tiles/Ground.prefab", Object);
break;
case selectedType.Earth:
tilePrefab = Resources.LoadAssetAtPath ("Assets/Prefabs/Tiles/Earth.prefab", Object);
break;
case selectedType.Rock:
tilePrefab = Resources.LoadAssetAtPath ("Assets/Prefabs/Tiles/Rock.prefab", Object);
break;
case selectedType.Water:
tilePrefab = Resources.LoadAssetAtPath ("Assets/Prefabs/Tiles/Water.prefab", Object);
break;
case selectedType.Lava:
tilePrefab = Resources.LoadAssetAtPath ("Assets/Prefabs/Tiles/Lava.prefab", Object);
break;
}
if ( !tilePrefab )
return;
var newTile : GameObject = EditorUtility.InstantiatePrefab(tilePrefab) as GameObject;
newTile.transform.position = selectedTile.transform.position;
newTile.transform.parent = selectedTile.transform.parent;
Selection.activeGameObject = newTile;
DestroyImmediate(selectedTile);
tilePrefab = null;
}
}
The editor script assumes you have tile prefabs named the following: Blank, Ground, Earth, Rock, Water, Lava. It will probably throw you an error if you use it without having those tiles, but you can edit as necessary. You'll need to create a new Javascript object in your Project view, inside a folder named "Editor" (or it won't work). Then, in your Window menu (at the top), you should have a new entry: TileEditor. Clicking that will bring up your new tile editor. In the Scene view, you should now be able to select a tile, and apply a new type to it (from the dropdown menu in the Tile Editor tool). You will also need to create a user layer called "Tile", and specify the tiles as belonging to this layer (I used User Layer 8). As I said, this was rushed together as something of a prototype, so I'm sure the code could be neater/nicer, but it works. For any of the functions you're unfamiliar with, I'd suggest searching for them on the script reference.
Now you have a board, and a means to edit it, you can move onto the player perspective. You could use the built-in OnMouseEnter and OnMouseExit MonoBehaviours, which are called every frame while the mouse is over the Collider (on each tile / unit). You could enable and disable a gameObject or GUI element over the tile from these events.
In my implementation (C#), I used a Raycast for tile selection. I also specified a LayerMask so the Raycast only hit tiles.
GameObject SelectTile ()
{
RaycastHit hit = new RaycastHit();
Ray ray = Camera.main.ScreenPointToRay (Input.mousePosition);
LayerMask layerMask = 1 << 8;
if (Physics.Raycast (ray, out hit, 100.0f, layerMask))
{
// print(hit.collider.gameObject.name);
return(hit.collider.gameObject);
}
return null;
}
However, you could also use a combination of OnMouseOver and GetMouseButtonDown.
Further reading
While working on this prototype, I asked a question on 'finding adjacent tiles in a grid', which I imagine will come in handy when you get to creating some sort of pathfinding logic (as you'll need to know which tiles are adjacent).
You'll also find plenty of other answers about adding components, which you can use to create traps and buildings on a specific tile.
Summary
This is mostly a broad overview, giving specific details where possible. Hopefully it'll give you a good start on your project. In future, I'd encourage you to ask smaller, more specific questions, as the answers will generally be a lot easier to digest. Welcome to UnityAnswers!
P.S. Please forgive any typos - I have the flu and a splitting headache to go with!
I've made a return visit and this is a great answer! Before reading your post $$anonymous$$arowi I didn't even know where to start reading documentation (first page? :P) and what questions to ask. This is definitively very good knowledge base to start and tomorrow at free time I will.
Thanks again, Oskar
No troubles. I'll check back here, so be sure to comment if you have any specific questions about things I've mentioned. Also (if you haven't already done so), remember to vote up answers that help you, and (once you know that it has), mark questions as 'correct' if they solve your problem. Cheers :)
Wow this is indeed a very detailed and useful answer. Thank you.
Okay, so I gave your editor code a shot. Unfortunately, it didn't work. I put the tiles in the right folder, but nothing happened when I hit "Apply." Can you think of any reasons? Thanks - Elliot
Answer by downward · Sep 07, 2011 at 09:57 PM
Will someone be willing to show me a screenshot of their project layout and script variable assignments who were able to get this to work?
When i create a prefab tile object from a generic cube should i name the object i drop into the prefab container the same as the prefab container name?
using UnityEngine;
using System.Collections;
public class GenerateTiles : MonoBehaviour {
public int board_size_x_; //For a 10 by 10 grid should i add "= 10" ?
public int board_size_z_; //should i add "= somenumber" here?
public Transform tile_prefab_; // <-- do i need to assign this?
Use this for initialization
void Start () {
GameObject board = new GameObject();
board.name = "Board";
for ( int x = 0; x < board_size_x_; x++ ) {
for ( int z = 0; z < board_size_z_; z++ ) {
Transform tile = (Transform)Instantiate(tile_prefab_,new Vector3(x,0,z),Quaternion.identity);
tile.name = "Tile" + x + z;
tile.parent = board.transform;
}
}
}
}
Ask a new question ins$$anonymous$$d of using this as an answer
Your answer
Follow this Question
Related Questions
The name 'Joystick' does not denote a valid type ('not found') 2 Answers
Modifying Terrain Splat Texture at Runtime 2 Answers
Infinite 2D placement grid 1 Answer
Adding map through script 0 Answers
2D Game Visual Grid to Place Objects 2 Answers