Unity’de 2D “procedural” izometrik harita oluşturmak

Kağan Ayten
6 min readAug 11, 2021

--

Merhaba herkese, bu yazımda sizlere Unity oyun motorunda nasıl “procedural” olarak izometrik harita oluşturulur ondan bahsedeceğim. İyi okumalar şimdiden.
Github kaynak dosyası:https://github.com/KaganAyten/Unity-Isometric-Procedural-Map-Generator

Öncelikle “procedural generation” nedir ondan bahsetmek istiyorum. Bu yöntem ile kullanılacak verilerin manuel olarak oluşturmak yerine çeşitli algoritmalar aracılığıyla oluşturulmaktadır. Peki biz bunu nasıl kullanacağız. Projeden örnek vermek gerekirse; oyunumuza bir map tasarlamak istiyoruz. Dağların, ovaların, göllerin olduğu bir harita. Ayrıca oyunumuz da 3 boyutlu bir oyun olsun. Her bir X ve Z ekseni için bir adet de yükseklik değeri olan Y değeri olması lazım. Biz manuel olarak tasarlamak istersek her bir X ve Z ekseni için bir Y yüksekliği oluşturuyoruz işin en temelinde. Çeşitli araçlarla haritamızı manuel olarak yükseltiler, göçükler, göller vs. ekleyebiliriz. Ama oyunumuz harita tasarlama yeteneğiniz yok ise ya da her bir yeni başlatılan oyun için yeni bir harita tasarlanması gerekiyorsa “procedural generation” yöntemine başvurmak kaçınılmaz oluyor. “Procedural generation” yöntemi ile oluşturulan algoritmada her bir X ve Z değeri için bize bir Y yüksekliği oluşturuyor ve o yüksekliğe göre de haritada girinti ya da çıkıntı oluşturuyor.

Peki nedir bu algoritma? Bu işlemi gerçekleştiren çeşitli algoritmalar var ama ben bu yazımda spesifik bir tanesini ele alacağım: “Perlin Noise”.

Perlin Noise nedir ve ne işe yarar?
Perlin Noise aslında rastgele sayı oluşturma algoritmasıdır. Ama diğer rastgele sayı oluşturma algoritmalarına göre daha doğal bir görünüme sahiptir. Doğal görünümden kasıt aralardaki sayı geçişleri yumuşaktır. Sayılar arasında büyük farklar bulunmamaktadır. Bu sayede de bizler oyunlarımızı geliştirirken bu algoritma ile harita, deniz, bulut gibi doğal olarak görünmesi daha önemli yapılar oluşturabiliyoruz.

Örnek bir Perlin Noise dokusu

Yukarıdaki fotoğrafta örnek bir Perlin Noise dokusu gözükmektedir. Tam siyah alanlar 1'i tam beyaz alanlar ise 0'ı temsil etmektedir. Ve dokuda da görüldüğü gibi kesinlikle keskin geçişler bulunmamaktadır.

Perlin Noise’ın kullanan en popüler oyunlardan biri de Minecraft adlı oyundur. Minecraft’ta her oyunda kendiliğinden farklı farklı haritalar oluşmaktadır. Ben de bu yazımda 2 boyutlu bir Minecraft haritasına benzer harita nasıl oluşturulur ondan bahsedeceğim.

Unity ve Perlin Noise

Unity içerisinde Perlin Noise algoritmasını direkt olarak kullanabileceğimiz bir algoritma mevcuttur ve biz de o algoritmayı kullanarak bir harita oluşturacağız.

Proje Ayarları:

Kodlamaya geçmeden önce Unity oyun motorunda 2D bir proje oluşturun. Ben bu yazımda izometrik olarak bir harita yapacağım için ayarları da ona göre anlatıyorum.

Öncelikle sahneye “Isometric Z as Y Tilemap” oluşturmamız gerekiyor. Bunu yapmak için yukarıdaki “GameObject” sekmesinden “2D Object” adlı kısmı bulup seçmek gerekiyor görseldeki gibi.

Ardından “Hierarchy” kısmından “Grid” adlı nesneyi seçin ve “Grid” ayarlarını yapın.
Ben bu projede aşağıdaki linkte bulunan çizimleri kullanacağım. O çizimler için uygun “Grid” ayarları fotoğraftaki gibidir:

Çizimlerin linki:https://admurin.itch.io/blocky-life

Ardından “Grid” altındaki “Tilemap”’e tıklayıp “Tilemap Renderer” bileşenindeki “Mode” kısmını “Individual” yapın.
Son olarak ise yukarıdaki “Edit” sekmesine tıklayın ve Edit->Project Settings->Graphics yolunu takip edin.
Açılan pencerede “Transparency Sort Mode” kısmından “Custom Axis” seçeneğini seçin ve X değerini 0, Y değerini 1, Z değerini ise “Grid” kısmındaki Y değerinin -0.5f ile çarpıp -0.01f çıkartın. Bu hesapla sıkça kullanılan bir hesaplama genelde sorun çıkartmıyor.

Yukarıdaki işlemlerden sonra kodlamaya geçebiliriz ama “Tile Palette”’nizi de hazırlamanız gerekiyor. Yani çizimleri “Tilemap” elemanı olarak kullanabiliyor olmak lazım. Eğer nasıl yapılacağını bilmiyorsanız yaklaşık 6 dakikalık benim tarafımdan çekilmiş bir eğitim videosu var ona göz atabilirsiniz:
https://youtu.be/STr_eILGZyA?list=PLAP1GY1YwkrdiEsjnp9xXe810eq-s8jPP

Kodlama kısmı:

Bir kod oluşturup açtıktan sonra aşağıdaki şekilde 6 adet değişken oluşturmamız gerekiyor.

public int width = 50;//haritanın genişliğipublic int height = 50;//haritanın yüksekliğipublic float[,] matrix = new float[1560, 1560];//oluşturulabilecek maksimum harita boyutufloat scale;//yükseklik boyutupublic float offsetX = 100;//x uzaklığıpublic float offsetY = 100;//y uzaklığı

Yukarıdaki “width” ve “height” değişkenleri ile haritamızın uzunluğunu ve genişliğini ayarlayabileceğiz. “scale” değişkeni haritanın yüksekliğinin saçma değerler olmaması için otomatik hesaplanacak bir değer. “offsetX” ve “offsetY” değeri ise her oluşturulan haritanın aynı olmaması için değişiklik katacak değişkenlerdir.

Bu tanımlamaların ardından içerisine X ve Y değerlerini alıp yükseklik değeri olarak Z değeri oluşturacak bir fonksiyon yazmamız gerekmektedir. 3 boyutta yüksekliği Y değeri oluşturmakta ama biz 2 boyutlu çalıştığımız için derinlik olması gerekiyor ve o yüzden de Z eksenini yükseklik olarak kullanıyoruz. Z derinliği 0'da arkada 1'se önde gibi düşünebilirsiniz.

float CalculateHeight(int x, int y){float xCoord = (float)x / width * scale+offsetX;float yCoord = (float)y / height * scale+offsetY;return Mathf.PerlinNoise(xCoord, yCoord);}

Yukarıdaki fonksiyon içerisine X ve Y koordinatının değerlerini alıyor ve onları gerekli eksenine bölüp “scale” değişkeni ile çarpıp “offset”’lerini ekledikten sonra Unity motorunun kendi fonksiyonu olan “Mathf.PerlinNoise()” fonksiyonuna yolluyoruz. Dönen değeri de direkt olarak return ediyoruz. Bu sayede her bir X ve Y koordinatı için artık yükseklik değeri oluşturabiliyoruz. Sadece bu fonksiyonu her bir X ve Y değeri için çağırmamız gerekiyor. Bunu yapmak için de iç içe 2 adet “for” döngüsü açıp her döngü indeks değerini X ve Y olarak yollamamız gerekiyor.

void GenerateMap(){
scale = Mathf.Min(height, width) / 8;
for (int x = 0; x < width; x++){for (int y = 0; y < height; y++){
matrix[x, y] = CalculateHeight(x, y);
int zDepth = (int)((matrix[x, y] * 10));
}}}

Yukarıda da görüldüğü gibi “GenerateMap” adında “private” bir fonksiyon oluşturdum. Yukarıda tanımladığım “scale” değişkenini burada ayarladım. X ve Y ekseninden hangisi küçükse o değeri 8'e bölüp “scale” değişkenini oluşturuyor. Bu değeri deneme yanılma yöntemiyle elde ettim siz de kendi projenizde farklı değerler deneyebilirsiniz.
Ardından 2 adet “for” döngüsü açtım ve ilk döngüyü genişlik, ikinci döngüyü ise yükseklik kadar dönmesini sağladım. Her bir X ve Y değerini “CalculateHeight” adlı fonksiyona yolladım ve dönüş olarak da “Perlin Noise” algoritması tarafından oluşturulan yükseklik değerini aldım. Bu değeri yine yukarıda tanımladığım “matrix” adlı değişkenin bulunduğu X ve Y koordinatına kaydettim.
Son olarak ise Z değeri 1 ve 0 arasında float bir değişken olduğu için ve bana da bir “int” değer lazım olduğu için değeri 10 ile genişlettim ve “int”’a çevirdim.

Artık çok bir şey kalmadı. X ve Y koordinatlarımıza göre Z değeri “Procedural” olarak oluşuyor. Sıradaki işimiz bu değişkenleri kullanarak haritaya bileşenleri çizdirmek.

Bu işlemi yapabilmek için Unity oyun motorunun “Tilemaps” adlı kütüphanesini kullanmamız gerekiyor. Projede kullanmak için en yukarıdaki kısıma “using UnityEngine.Tilemaps” yazmak yeterli olacaktır.

using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.Tilemaps;

Ardından üzerinde işlem yapacağımız “Tilemap”’i ve çizeceğimiz toprak ve taş bloğunu tanımlamamız gerekiyor. Onları da aşağıdaki şekilde tanımlayabiliriz:

public Tile grassTile, stoneTile;public Tilemap baseGrid;

Tanımlamalar bittiğine göre artık istediğimizi yapabiliriz. “GenerateMap” fonksiyonunda en son “zDepth” adlı değişkeni hesaplıyorduk. O kısmın altına(2 “for” döngüsünün içinde olması lazım) aşağıdaki kodu yazın:

baseGrid.SetTile(new Vector3Int(x, y, zDepth), grassTile);

Bu sayede “baseGrid” adında tanımladığımız “Tilemap”’e yine tanımladığımız “grassTile” adındaki çimen bloğunu o anki X ve Y konumunun oluşturduğumuz Z yüksekliğini konum olarak atıyoruz. Bu fonksiyonu bir kere çalıştırmak için Unity’nin Start fonksiyonunda bu fonksiyonunu çağırıyoruz.

private void Start(){GenerateMap();}

Bu yazdığımız kodu herhangi bir oyun nesnesine atıp gerekli atamaları yapın ve yükseklik, genişlik değişkenlerini dilediğiniz gibi(sınırlar içerisinde) ayarlayın ve oyunu çalıştırın.

Procedural olarak oluşturulan harita(13x13)

Ben boyutunu 13x13 seçtiğim için böyle bir harita oluştu. Değeri biraz yükselttiğim zaman ise:

150x150 harita

Harita yine oluşacaktır ama arada böyle boşluklar olacaktır. Bunu önlemek için “GenerateMap” fonksiyonunu şöyle yeniden düzenleyebiliriz:

void GenerateMap(){scale = Mathf.Min(height, width) / 8;for (int x = 0; x < width; x++){for (int y = 0; y < height; y++){matrix[x, y] = CalculateHeight(x, y);int zDepth = (int)((matrix[x, y] * 10));for(int z = 0; z < zDepth; z++){baseGrid.SetTile(new Vector3Int(x, y, z), stoneTile);}baseGrid.SetTile(new Vector3Int(x, y, zDepth), grassTile);}}}

Bu sayede yüksekliği kadar taş bloğu koyacaktır ve en son olarak ise çimen bloğunu koyacaktır. Bu sayede aradaki boşluklar gidecektir.

Son hali

Ben sonuçtan gayet memnunum umarım siz de sonuçtan ve yazıdan memnun olmuşsunuzdur. Yukarıda da belirttiğim gibi projenin gelişmiş halinin kaynak kodları Github’da mevcut dilerseniz oradan da indirip kurcalayabilirsiniz. Herkese iyi ve sağlıklı günler dilerim.

--

--