Tegnap meglátogattam egy kisgyermekes szülő ismerősömet. A majdhogynem három éves kislányuk bevallása szerint “Ő szeret ledönteni olyan dolgokat, amiket mások készítenek.” Ezért egész délután Duplo-várakat építettem, majd ő ezeket egy pillanat alatt a földel tette egyenlővé, azért hogy én építhessem is a következőt.
Kettőnk közül én voltama felnőtt, szóval hagytam rombolni, de hazudnék, ha azt mondanám, hogy egy nagyon kicsit nem irigyeltem őt. Felépíteni valami nagyot és látni ahogy az darabokra hullik a kezünk alatt valami olyasmi, aminek élvezetét úgy érzem sosem nőjük ki, csupán megtanuljuk visszafogni magunkat.
A gond az a rombolással, hogy ha nincs valakinek hozzám hasonló rabszolgája, aki folyamatosan fel is építi a Duplo-várat, akkor azért az egy másodperc rombolásért egészen sokat kell dolgozni.
Miért ne végezhetné a feladatot helyettünk egy program?
Írjunk tehát egy Unity programot, aminek lényege annyi lesz, hogy parancsra elkészít egy kockavárat egy mesh alapján. Olyan programot szerettem volna írni, ami bármilyen modellre működik és egy kattintással legyártja a modell klónját téglatestekből. Erre a célra én szereztem egy ingyenes középkori tavernát az Asset Store-ból. A fent leírt kockásítás ehhez az asset-hez például valahogy így nézne ki:
Ezeket a blokkokat voxel-eknek is nevezhetjük, mint a pixelek 3D megfelelői. Fontos viszont hogy acélom nem az volt, hogy egy összefüggő mesh-t hozzak létre, hanem egy rakás önálló kocka objektumot. Hogy is rombolhatnék egyébként?
Előszöris készítettem egy MonoBehaviour scriptet BlockMaker
néven, ami Editor időben automatikusan létrehozza a struktúrát egy másik objektum alapján. Ehhez beállításként felvettem a következő elemeket, mint szerializált field.
[SerializeField] Collider original; // Az eredeti test referenciája
[SerializeField] GameObject blockPrototype; // Egy prototípus a másolandó tégláról
[SerializeField] float blockScale = 1f; // Mekkora legyen egy tégla oldalhossza
Mivel a generálást editorban szerettem volna elvégezni, kellene egy gomb a komponensre, amivel kiadhatjuk a parancsot. Ehhez írtam egy rövid editor szkriptet, ami semmi mást nem tesz, csak kirajzolja a normál Inspektor felületét a komponensnek majd mögé még kirajzol egy gombot, aminek megnyomására hívja az osztály GenerateBlocks
metódusát.
# if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(BlockMaker))] // Editor szkript a BlockMaker komponenshez
class BlockMakerEditor : Editor
{
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
BlockMaker blockMaker = (BlockMaker) target;
if (GUILayout.Button("Generate Blocks"))
blockMaker.GenerateBlocks();
}
}
#endif
Az Editor szkripteknek mindenképp egy # if UNITY_EDITOR
feltételen belül kell lennie. Ezeket preprocessor direktíváknak nevezzük és utasításokat adhatunk vele a fordításhoz. Ebben az esetben az egész kódot kivettük az alól, hogy része legyen a végleges Build-nek. Ez a kód csak editorban fog létezni. Ezt muszáj megtenni egyéb esetben nem fogja a Unity engedni elkészíteni build-et.
(Azzal is megadhattuk volna mindezt, hogy pontosan “Editor” nevű mappába tesszük a szkriptet.)
Most végre végezzük el a lényegét a feladatnak: A generálást.
Először is számoljuk ki, hogy hány blokk fér bele a térrészbe, amit az eredeti test elfoglal. Ehhez lekértem annak Bound-ját és egy egyszerű osztással és kerekítéssel kiszámoltam három int számot, ami tengelyenként adja meg, mennyi elem fér bele a beállított méretből. Ezután pár egyéb szükséges adat kiszámolása után végig is iteráltam ezeken a pontokon 3 egymásba ágyazott for cikluson belül.
Bounds bounds = originalCollider.bounds;
int xCount = Mathf.RoundToInt(bounds.size.x / blockScale);
int yCount = Mathf.RoundToInt(bounds.size.y / blockScale);
int zCount = Mathf.RoundToInt(bounds.size.z / blockScale);
var blocks = new GameObject[xCount, yCount, zCount]; // 3D mátrixban a blokkoknak
Vector3 scale = Vector3.one * blockScale; // Egy blokk 3D mérete
for (int x = 0; x < xCount; x++) // X dimenzió
for (int y = 0; y < yCount; y++) // Y dimenzió
for (int z = 0; z < zCount; z++) // Z dimenzió
{
// ...
}
Egy 3D mátrixot (több dimenziós tömböt) is felvettem, hogy majd benne eltároljam, az összes létrehozott blokkot térbeli pozíció szerint. (Hátha jól jön még…)
Ezután kiszámoltam a térrészt, ahol minden egyes cikluslefutásnál a tégla helye lenne és ez után a Physics.CheckBox
függvénnyel ellenőriztem, a térrész ütközik-e Collider-rel.
Ha igen, akkor generáltam is egyet a prototípus téglánkból az Instantiat metódussal majd a megfelelő pozícióba illesztettem a szkriptet tartalmazó GameObject gyerekeként. Végül eltároltam az új blokkot a mátrix-omban.
Vector3 position = bounds.min + new Vector3(x + 0.5f, y + 0.5f, z + 0.5f) * blockScale;
bool inside = Physics.CheckBox(position, scale / 2f);
if(inside)
{
GameObject block = Instantiate(blockPrototype, transform); // Új blokk
block.transform.position = position; // Pozíció
block.transform.localScale = scale; // Skálázás
blocks[x, y, z] = block; // Eltárolás a mátrixban
}
Még kiegészítettem mindezt azzal, hogy minden generálás előtt törlöm az összes gyerekét a szkriptet tartalmazó GameObject-nek, hogy tiszta lappal induljak.
public void DestroyChildren()
{
int childCount = transform.childCount;
for (int i = childCount - 1; i >= 0; i--)
{
Transform child = transform.GetChild(i);
try
{
DestroyImmediate(child.gameObject);
}
catch (InvalidOperationException) { }
}
}
Mindez a következő eredményt adta ki játék közben:
Hát… A kockák jó helyen vannak, de nem valami stabil még az épület.
Ennek az egyik oka, hogy a kocka házunk üreges, mivel a CheckBox metódus csak az oldalakkal való ütközést tuja összehasonlítani egy MeshCollider esetén. Ezzel szemben egyébként kitöltött eredményt adna egy Box-, Sphere- vagy CapsuleCollider-rel. Most ne foglalkozunk mindezzel!
Mi lenne ha rögzítenénk mindegyik elemet a szomszédjához egy Fixed Joint komponenssel?
Ehhez azt a stratégiát alkalmaztam, hogy minden RigidBody-hoz, akkor kötök egy másikat, hogyha az egyes tengelyek mentén eggyel kisebb indexen szerepel egy elem. Ennek megállapítására jól jött a mátrix, amiben mindezt előre eltároltam.
// Újabb 3 mélységű forciklusban:
for (int x = 0; x < xCount; x++)
for (int y = 0; y < yCount; y++)
for (int z = 0; z < zCount; z++)
{
GameObject block = blocks[x, y, z];
if (x > 0) // Blara lévő elemmel összekötés, ha létezik
TryConnect(block, blocks[x - 1, y, z]);
if (y > 0) // Alatta lévő elemmel összekötés, ha létezik
TryConnect(block, blocks[x, y - 1, z]);
if (z > 0) // Mögötte lévő elemmel összekötés, ha létezik
TryConnect(block, blocks[x, y, z - 1]);
}
void TryConnect(GameObject block, GameObject block2)
{
if (block == null || block2 == null) return;
FixedJoint joint = block.AddComponent<FixedJoint>(); // Új komponens
joint.connectedBody = block2.GetComponent<Rigidbody>(); // Összekötés
joint.breakForce = breakForce; // Felvettem erre egy extra beállítást
}
Ha 1000 környékére állítottam a breakForce
-ot, minden téglának 1-es tömeget adva, akkor a ház elég stabilan megállt.
Na de nem azért dolgoztam, hogy a stabil házban gyönyörködjek, hanem, hogy darabokra rombolhassam. Ehhez készítettem egy egyszerű ágyú szkriptet. Ezt itt most nem mutatom be részletesen, ám a teljes projekttel együtt letölthető. Lásd később…
Kicsit kocsonyás lett a házunk. Kevésbé realisztikus, cserében jóval viccesebb. Ezt én most teljes sikernek könyvelem el.
Ha érdekel, hogy lehetne valósághűbb és jóval erőforráshatékonyabb rombolható házat építeni, akkor azt jelezd és előbb utóbb talán bemutatom azt is.
Marosi Csaba
+36 20 359 74 22
Ha érdekel a kódolás, játékfejlesztés fontold meg a jelentkezést egyik tanfolyamomra→