Editor de hărți de înălțime a terenului în joc pentru Unity
Unity vine echipat cu un editor de teren încorporat, dar ce se întâmplă dacă doriți să adăugați o funcție de editare a terenului în game?
În acest tutorial, voi arăta cum să adăugați un editor simplu de rulare Terrain. De asemenea, nu include texturarea, este încă un exemplu grozav pe care îl puteți construi.
Unity versiune folosită în acest tutorial: Unity 2021.1.0f1 (64 de biți)
Pasul 1: Creați toate scripturile necesare
Acest tutorial conține 3 scripturi:
SC_TerrainEditor.cs
using UnityEngine;
public class SC_TerrainEditor : MonoBehaviour
{
public enum DeformMode { RaiseLower, Flatten, Smooth }
DeformMode deformMode = DeformMode.RaiseLower;
string[] deformModeNames = new string[] { "Raise Lower", "Flatten", "Smooth" };
public Terrain terrain;
public Texture2D deformTexture;
public float strength = 1;
public float area = 1;
public bool showHelp;
Transform buildTarget;
Vector3 buildTargPos;
Light spotLight;
//GUI
Rect windowRect = new Rect(10, 10, 400, 185);
bool onWindow = false;
bool onTerrain;
Texture2D newTex;
float strengthSave;
//Raycast
private RaycastHit hit;
//Deformation variables
private int xRes;
private int yRes;
private float[,] saved;
float flattenTarget = 0;
Color[] craterData;
TerrainData tData;
float strengthNormalized
{
get
{
return (strength) / 9.0f;
}
}
// Start is called before the first frame update
void Start()
{
//Create build target object
GameObject tmpObj = new GameObject("BuildTarget");
buildTarget = tmpObj.transform;
//Add Spot Light to build target
GameObject spotLightObj = new GameObject("SpotLight");
spotLightObj.transform.SetParent(buildTarget);
spotLightObj.transform.localPosition = new Vector3(0, 2, 0);
spotLightObj.transform.localEulerAngles = new Vector3(90, 0, 0);
spotLight = spotLightObj.AddComponent<Light>();
spotLight.type = LightType.Spot;
spotLight.range = 20;
tData = terrain.terrainData;
if (tData)
{
//Save original height data
xRes = tData.heightmapResolution;
yRes = tData.heightmapResolution;
saved = tData.GetHeights(0, 0, xRes, yRes);
}
//Change terrain layer to UI
terrain.gameObject.layer = 5;
strength = 2;
area = 2;
brushScaling();
}
void FixedUpdate()
{
raycastHit();
wheelValuesControl();
if (onTerrain && !onWindow)
{
terrainDeform();
}
//Update Spot Light Angle according to the Area value
spotLight.spotAngle = area * 25f;
}
//Raycast
//______________________________________________________________________________________________________________________________
void raycastHit()
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
hit = new RaycastHit();
//Do Raycast hit only against UI layer
if (Physics.Raycast(ray, out hit, 300, 1 << 5))
{
onTerrain = true;
if (buildTarget)
{
buildTarget.position = Vector3.Lerp(buildTarget.position, hit.point + new Vector3(0, 1, 0), Time.time);
}
}
else
{
if (buildTarget)
{
Vector3 curScreenPoint = new Vector3(Input.mousePosition.x, Input.mousePosition.y, 200);
Vector3 curPosition = Camera.main.ScreenToWorldPoint(curScreenPoint);
buildTarget.position = curPosition;
onTerrain = false;
}
}
}
//TerrainDeformation
//___________________________________________________________________________________________________________________
void terrainDeform()
{
if (Input.GetMouseButtonDown(0))
{
buildTargPos = buildTarget.position - terrain.GetPosition();
float x = Mathf.Clamp01(buildTargPos.x / tData.size.x);
float y = Mathf.Clamp01(buildTargPos.z / tData.size.z);
flattenTarget = tData.GetInterpolatedHeight(x, y) / tData.heightmapScale.y;
}
//Terrain deform
if (Input.GetMouseButton(0))
{
buildTargPos = buildTarget.position - terrain.GetPosition();
if (Input.GetKey(KeyCode.LeftShift))
{
strengthSave = strength;
}
else
{
strengthSave = -strength;
}
if (newTex && tData && craterData != null)
{
int x = (int)Mathf.Lerp(0, xRes, Mathf.InverseLerp(0, tData.size.x, buildTargPos.x));
int z = (int)Mathf.Lerp(0, yRes, Mathf.InverseLerp(0, tData.size.z, buildTargPos.z));
x = Mathf.Clamp(x, newTex.width / 2, xRes - newTex.width / 2);
z = Mathf.Clamp(z, newTex.height / 2, yRes - newTex.height / 2);
int startX = x - newTex.width / 2;
int startY = z - newTex.height / 2;
float[,] areaT = tData.GetHeights(startX, startY, newTex.width, newTex.height);
for (int i = 0; i < newTex.height; i++)
{
for (int j = 0; j < newTex.width; j++)
{
if (deformMode == DeformMode.RaiseLower)
{
areaT[i, j] = areaT[i, j] - craterData[i * newTex.width + j].a * strengthSave / 15000;
}
else if (deformMode == DeformMode.Flatten)
{
areaT[i, j] = Mathf.Lerp(areaT[i, j], flattenTarget, craterData[i * newTex.width + j].a * strengthNormalized);
}
else if (deformMode == DeformMode.Smooth)
{
if (i == 0 || i == newTex.height - 1 || j == 0 || j == newTex.width - 1)
continue;
float heightSum = 0;
for (int ySub = -1; ySub <= 1; ySub++)
{
for (int xSub = -1; xSub <= 1; xSub++)
{
heightSum += areaT[i + ySub, j + xSub];
}
}
areaT[i, j] = Mathf.Lerp(areaT[i, j], (heightSum / 9), craterData[i * newTex.width + j].a * strengthNormalized);
}
}
}
tData.SetHeights(x - newTex.width / 2, z - newTex.height / 2, areaT);
}
}
}
void brushScaling()
{
//Apply current deform texture resolution
newTex = Instantiate(deformTexture) as Texture2D;
TextureScale.Point(newTex, deformTexture.width * (int)area / 10, deformTexture.height * (int)area / 10);
newTex.Apply();
craterData = newTex.GetPixels();
}
void wheelValuesControl()
{
float mouseWheel = Input.GetAxis("Mouse ScrollWheel");
if (Mathf.Abs(mouseWheel) > 0.0)
{
if (mouseWheel > 0.0)
{
//More
if (!Input.GetKey(KeyCode.LeftShift))
{
if (area < 13)
{
area += 0.5f;
}
else
{
area = 13;
}
}
else
{
if (strength < 13)
{
strength += 0.5f;
}
else
{
strength = 13;
}
}
}
else if (mouseWheel < 0.0)
{
//Less
if (!Input.GetKey(KeyCode.LeftShift))
{
if (area > 1)
{
area -= 0.5f;
}
else
{
area = 1;
}
}
else
{
if (strength > 1)
{
strength -= 0.5f;
}
else
{
strength = 1;
}
}
}
if (area > 1)
brushScaling();
}
}
//GUI
//______________________________________________________________________________________________________________________________
void OnGUI()
{
windowRect = GUI.Window(0, windowRect, TerrainEditorWindow, "Terrain Sculptor");
GUILayout.BeginArea(new Rect(Screen.width - 70, 10, 60, 30));
showHelp = GUILayout.Toggle(showHelp, "(Help)", new GUILayoutOption[] { GUILayout.Width(60.0f), GUILayout.Height(30.0f) });
GUILayout.EndArea();
if (showHelp)
{
//Help window properties
GUI.Window(1, new Rect(Screen.width - 410, 50, 400, 120), HelpWindow, "Help Window");
}
}
//Help window display tips and tricks
void HelpWindow(int windowId)
{
GUILayout.BeginVertical("box");
{
GUILayout.Label("- Mouse wheel - area change");
GUILayout.Label("- Mouse wheel + Shift - strength change");
GUILayout.Label("- Hold Shift in RaiseLower mode to lower terrain");
}
GUILayout.EndVertical();
}
void TerrainEditorWindow(int windowId)
{
//Detect when mouse cursor inside region (TerrainEditorWindow)
GUILayout.BeginArea(new Rect(0, 0, 400, 240));
if (GUILayoutUtility.GetRect(10, 50, 400, 240).Contains(Event.current.mousePosition))
{
onWindow = true;
}
else
{
onWindow = false;
}
GUILayout.EndArea();
GUILayout.BeginVertical();
//Shared GUI
GUILayout.Space(10f);
GUILayout.BeginHorizontal();
GUILayout.Label("Area:", new GUILayoutOption[] { GUILayout.Width(75f) });
area = GUILayout.HorizontalSlider(area, 1f, 13f, new GUILayoutOption[] { GUILayout.Width(250f), GUILayout.Height(15f) });
GUILayout.Label((Mathf.Round(area * 100f) / 100f).ToString(), new GUILayoutOption[] { GUILayout.Width(250f), GUILayout.Height(20f) });
//Change brush texture size if area value was changed
if (GUI.changed)
{
brushScaling();
}
GUILayout.EndHorizontal();
GUILayout.Space(10f);
GUILayout.BeginHorizontal();
GUILayout.Label("Strength:", new GUILayoutOption[] { GUILayout.Width(75f) });
strength = GUILayout.HorizontalSlider(strength, 1f, 13f, new GUILayoutOption[] { GUILayout.Width(250f), GUILayout.Height(15f) });
GUILayout.Label((Mathf.Round(strength * 100f) / 100f).ToString(), new GUILayoutOption[] { GUILayout.Width(250f), GUILayout.Height(20f) });
GUILayout.EndHorizontal();
//Deform GUI
GUILayout.Space(10);
deformMode = (DeformMode)GUILayout.Toolbar((int)deformMode, deformModeNames, GUILayout.Height(25));
GUILayout.Space(10);
GUILayout.BeginHorizontal();
if (GUILayout.Button("Reset Terrain Height", new GUILayoutOption[] { GUILayout.Height(30f) }))
{
tData.SetHeights(0, 0, saved);
}
GUILayout.EndHorizontal();
GUILayout.EndVertical();
}
void OnApplicationQuit()
{
//Reset terrain height when exiting play mode
tData.SetHeights(0, 0, saved);
}
}
SC_EditorFlyCamera.cs
using UnityEngine;
public class SC_EditorFlyCamera : MonoBehaviour
{
public float moveSpeed = 15;
public float turnSpeed = 3;
bool freeLook = false;
bool moveFast = false;
float rotationY;
// Use this for initialization
void Start()
{
rotationY = -transform.localEulerAngles.x;
}
// Update is called once per frame
void Update()
{
Movement();
}
void Movement()
{
moveFast = Input.GetKey(KeyCode.LeftShift);
float speed = moveSpeed * Time.deltaTime * (moveFast ? 3 : 1);
if (Input.GetKey(KeyCode.W))
{
transform.root.Translate(transform.forward * speed, Space.World);
}
if (Input.GetKey(KeyCode.S))
{
transform.root.Translate(-transform.forward * speed, Space.World);
}
if (Input.GetKey(KeyCode.A))
{
transform.root.Translate(-transform.right * speed, Space.World);
}
if (!Input.GetKey(KeyCode.LeftControl) && Input.GetKey(KeyCode.D))
{
transform.root.Translate(transform.right * speed, Space.World);
}
if (Input.GetKey(KeyCode.Q))
{
transform.root.Translate(transform.up * speed, Space.World);
}
if (Input.GetKey(KeyCode.E))
{
transform.root.Translate(-transform.up * speed, Space.World);
}
if (Input.GetMouseButtonDown(1))
{
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
}
freeLook = Input.GetMouseButton(1);
if (Input.GetMouseButtonUp(1))
{
Cursor.lockState = CursorLockMode.None;
Cursor.visible = true;
}
if (freeLook)
{
float rotationX = transform.localEulerAngles.y + Input.GetAxis("Mouse X") * turnSpeed;
rotationY += Input.GetAxis("Mouse Y") * turnSpeed;
rotationY = Mathf.Clamp(rotationY, -90, 90);
transform.localEulerAngles = new Vector3(-rotationY, rotationX, 0);
}
}
}
TextureScale.cs
// Only works on ARGB32, RGB24 and Alpha8 textures that are marked readable
using System.Threading;
using UnityEngine;
public class TextureScale
{
public class ThreadData
{
public int start;
public int end;
public ThreadData(int s, int e)
{
start = s;
end = e;
}
}
private static Color[] texColors;
private static Color[] newColors;
private static int w;
private static float ratioX;
private static float ratioY;
private static int w2;
private static int finishCount;
private static Mutex mutex;
public static void Point(Texture2D tex, int newWidth, int newHeight)
{
ThreadedScale(tex, newWidth, newHeight, false);
}
public static void Bilinear(Texture2D tex, int newWidth, int newHeight)
{
ThreadedScale(tex, newWidth, newHeight, true);
}
private static void ThreadedScale(Texture2D tex, int newWidth, int newHeight, bool useBilinear)
{
texColors = tex.GetPixels();
newColors = new Color[newWidth * newHeight];
if (useBilinear)
{
ratioX = 1.0f / ((float)newWidth / (tex.width - 1));
ratioY = 1.0f / ((float)newHeight / (tex.height - 1));
}
else
{
ratioX = ((float)tex.width) / newWidth;
ratioY = ((float)tex.height) / newHeight;
}
w = tex.width;
w2 = newWidth;
var cores = Mathf.Min(SystemInfo.processorCount, newHeight);
var slice = newHeight / cores;
finishCount = 0;
if (mutex == null)
{
mutex = new Mutex(false);
}
if (cores > 1)
{
int i = 0;
ThreadData threadData;
for (i = 0; i < cores - 1; i++)
{
threadData = new ThreadData(slice * i, slice * (i + 1));
ParameterizedThreadStart ts = useBilinear ? new ParameterizedThreadStart(BilinearScale) : new ParameterizedThreadStart(PointScale);
Thread thread = new Thread(ts);
thread.Start(threadData);
}
threadData = new ThreadData(slice * i, newHeight);
if (useBilinear)
{
BilinearScale(threadData);
}
else
{
PointScale(threadData);
}
while (finishCount < cores)
{
Thread.Sleep(1);
}
}
else
{
ThreadData threadData = new ThreadData(0, newHeight);
if (useBilinear)
{
BilinearScale(threadData);
}
else
{
PointScale(threadData);
}
}
tex.Resize(newWidth, newHeight);
tex.SetPixels(newColors);
tex.Apply();
texColors = null;
newColors = null;
}
public static void BilinearScale(System.Object obj)
{
ThreadData threadData = (ThreadData)obj;
for (var y = threadData.start; y < threadData.end; y++)
{
int yFloor = (int)Mathf.Floor(y * ratioY);
var y1 = yFloor * w;
var y2 = (yFloor + 1) * w;
var yw = y * w2;
for (var x = 0; x < w2; x++)
{
int xFloor = (int)Mathf.Floor(x * ratioX);
var xLerp = x * ratioX - xFloor;
newColors[yw + x] = ColorLerpUnclamped(ColorLerpUnclamped(texColors[y1 + xFloor], texColors[y1 + xFloor + 1], xLerp),
ColorLerpUnclamped(texColors[y2 + xFloor], texColors[y2 + xFloor + 1], xLerp),
y * ratioY - yFloor);
}
}
mutex.WaitOne();
finishCount++;
mutex.ReleaseMutex();
}
public static void PointScale(System.Object obj)
{
ThreadData threadData = (ThreadData)obj;
for (var y = threadData.start; y < threadData.end; y++)
{
var thisY = (int)(ratioY * y) * w;
var yw = y * w2;
for (var x = 0; x < w2; x++)
{
newColors[yw + x] = texColors[(int)(thisY + ratioX * x)];
}
}
mutex.WaitOne();
finishCount++;
mutex.ReleaseMutex();
}
private static Color ColorLerpUnclamped(Color c1, Color c2, float value)
{
return new Color(c1.r + (c2.r - c1.r) * value,
c1.g + (c2.g - c1.g) * value,
c1.b + (c2.b - c1.b) * value,
c1.a + (c2.a - c1.a) * value);
}
}
Pasul 2
- Creați o scenă nouă
- Creați un teren nou mergând la GameObject->3D Object->Terrain
- Atașați scripturile SC_EditorFlyCamera și SC_TerrainEditor la camera principală
- Atribuiți variabile Terrain și DeformTexture în SC_TerrainEditor (Terenul ar trebui să fie cel pe care doriți să îl editați în scenă)
Pentru DeformTexture poți folosi imaginea de mai jos sau poți folosi aceste Terrain Brushes de înaltă calitate*:
NOTĂ: Textura deformată trebuie să aibă sursa Alpha de la Tonuri de gri, Citire/Scriere activată și, de asemenea, formatul trebuie să fie setat la unul dintre acestea: ARGB32, RGB24 sau Alpha8.
După ce totul este atribuit, este timpul să îl testați în modul Play:
Selectați dintre trei opțiuni: Ridicați Jos (clic stânga pentru a crește, Shift + clic stânga pentru a coborî), Aplatizare, și Netezire.
Folosiți tastele W, A, S și D pentru a zbura în jur și țineți apăsat butonul din dreapta al mouse-ului pentru a privi în jur.
Totul funcționează conform așteptărilor!