آموزش Unity : طراحی افکت دو بعدی آب

دسته بندی :بازی سازی, برنامه نویسی 4516
آموزش Unity : طراحی افکت دو بعدی آب

آموزش Unity : طراحی افکت دو بعدی آب

توی این آموزش، افکت دو بعدی آب رو با استفاده از Simpli Physics و نرم افزار Unity، شبیه سازی می کنیم. برای اینکار، از خروجی گیرنده خطی، خروجی گیرنده مش و سایر چیزها هم کمک می گیریم. نتیجه نهایی، از موج و Splash تشکیل شده و میشه اون رو به بازی اضافه کرد. بیاین شروع کنیم!

با ” آموزش Unity : طراحی افکت دو بعدی آب ” با ما همراه باشید…

  • سطح سختی: متوسط
  • مدت آموزش: 40 تا 50 دقیقه
  • نرم افزار استفاده شده: Unity

تنظیم Water Manager

بخش سطحی آب رو با استفاده از یکی از خروجی گیرنده های خطی Unity، خروجی می گیریم. برای این کار، از نودهای زیادی که ظاهری موج مانند دارن، استفاده می کنیم.

آموزش Unity : طراحی افکت دو بعدی آب

باید به موقعیت، سرعت و چگالی هر نود توجه کنیم. برای این کار، از آرایه ها استفاده می کنیم. بنابراین متغیرهای زیر رو اضافه می کنیم.

float[] xpositions;
float[] ypositions;
float[] velocities;
float[] accelerations;
LineRenderer Body;

LinearRenderer، یا خروجی گیرنده خطی، تمام نودها رو ذخیره می کنه و Outline آب رو شکل میده. اما خود آب رو هم باید طراحی کنیم. این کار رو با استفاده از Mesh ها انجام میدیم. برای نگه داشتن مش ها هم به Objects نیاز داریم.

GameObject[] meshobjects;
Mesh[] meshes;

به علاوه، برای ایجاد ارتباط بین عناصر آب از Collider استفاده می کنیم.

GameObject[] colliders;

و تمام اون ها را ذخیره می کنیم:

const float springconstant = 0.02f;
const float damping = 0.04f;
const float spread = 0.05f;
const float z = -1f;

این مولفه ها یکسان هستن به جز Z که Z-Offset آب هست. بنابراین از -1 استفاده می کنیم (بسته به اینکه دوست دارین چه چیزی در جلو و چه چیزی در پشت به نمایش در بیاد، می تونین این عدد رو عوض کنین. برای تعیین محل قرار گیری Sprites باید از مختصات Z استفاده کنین).
حالا میریم سراغ این مقادیر:

float baseheight;
float left;
float bottom;

این ها ابعاد آب هستن.
به تعدادی متغیر عمومی هم برای تنظیم در ادیتور نیاز داریم. اول، سیستم ذرات یا Particle System مورد نیاز:

public GameObject splash:

حالا متریال مورد استفاده برای خروجی گیرنده خطی (می تونیم از متریال acid, lava, chemicals یا غیره استفاده کنیم):

public Material mat:

به علاوه، مش مورد استفاده برای خود آب:

public GameObject watermesh:

تمام این ها به Prefabs بستگی داره.
می خوایم عنصری داشته باشیم که به عنوان Manager عمل کنه و بتونه تمام داده ها رو نگه داره. برای این کار، تابعی به اسم SpawnWater می نویسیم.
این تابع، ورودی های سمت چپ، عرض، بالا و پایین آب رو می گیره.

public void SpawnWater(float Left, float Width, float Top, float Bottom)
{

درست کردن نودها

حالا باید ببینیم به چند تا نود نیاز داریم:

int edgecount = Mathf.RoundToInt(Width) * 5;
int nodecount = edgecount + 1;

بر اساس هر واحد عرض، از پنج نود استفاده می کنیم. این طوری صاف تر میشه و به کار زیادی هم نیاز نداره (شاید شما بخواین از تعداد دیگه ای استفاده کنین).
اولین کاری که باید انجام بدیم اینه که با LineRenderer، از آب خروجی بگیریم:

Body = gameObject.AddComponent<LineRenderer>();
Body.material = mat;
Body.material.renderQueue = 1000;
Body.SetVertexCount(nodecount);
Body.SetWidth(0.1f, 0.1f);

اینجا در واقع متریال رو انتخاب کردیم و اون رو با انتخاب موقعیت در Render Quene، تنظیم کردیم. عدد مناسب رو به عنوان تعداد نودها در نظر گرفتیم و عرض خط رو روی 0.1 قرار دادیم. این مقدار بسته به ضخامت دلخواه تون فرق می کنه. همون طور که می بینین، SetWidth دو تا پارامتر داره. که یکی مربوط به شروع و دیگری مربوط به پایان خط هست. Width یا عرض باید ثابت بمونه.
حالا که نودها رو درست کردیم، میریم سراغ متغیرها:

xpositions = new float[nodecount];
ypositions = new float[nodecount];
velocities = new float[nodecount];
accelerations = new float[nodecount];
 
meshobjects = new GameObject[edgecount];
meshes = new Mesh[edgecount];
colliders = new GameObject[edgecount];
 
baseheight = Top;
bottom = Bottom;
left = Left;

حالا که تمام آرایه ها را داریم میریم سراغ داده ها:

for (int i = 0; i < nodecount; i++)
{
    ypositions[i] = Top;
    xpositions[i] = Left + Width * i / edgecount;
    accelerations[i] = 0;
    velocities[i] = 0;
    Body.SetPosition(i, new Vector3(xpositions[i], ypositions[i], z));
}

تمام y-position های سطح آب رو تنظیم می کنیم و نودها رو اضافه می کنیم. Velocity یا چگالی و Acceleration یا شتاب در حال حاضر روی صفر قرار دارن.
با قرار دادن هر نود در موقعیت مناسب در LinearRenderer، کار رو تموم می کنیم.

درست کردن مش ها

بخش مهمش اینجاست.
خط رو داریم اما خود آب رو نداریم. برای این کار از مش ها استفاده می کنیم. با این کد شروع می کنیم:

for (int i = 0; i < edgecount; i++)
{
    meshes[i] = new Mesh();

حالا مش ها یک مشت متغیر رو ذخیره می کنن. اولین متغیر خیلی ساده ست: تمام رئوس و زوایا رو در بر میگیره.

آموزش Unity : طراحی افکت دو بعدی آب

این نمودار ظاهر مش رو بهمون نشون میده. رئوس بخش اول هایلایت شده. در مجموع چهار تا می خوایم:

Vector3[] Vertices = new Vector3[4];
Vertices[0] = new Vector3(xpositions[i], ypositions[i], z);
Vertices[1] = new Vector3(xpositions[i + 1], ypositions[i + 1], z);
Vertices[2] = new Vector3(xpositions[i], bottom, z);
Vertices[3] = new Vector3(xpositions[i+1], bottom, z);

همون طور که می بینین، Vertex 0 بالای سمت چپ، 1 بالای سمت راست، 2 پایین سمت چپ و 3 بالای سمت راست قرار داره. اینها رو باید به خاطر بسپاریم.
مش ها به UV هم نیاز دارن. مش ها، دارای بافت هستن و برای مشخص کردن بخش منتخب، از UV ها استفاده می کنیم. توی این مورد، فقط به بالای سمت چپ، بالای سمت راست، پایین سمت چپ و پایین سمت راست بافت نیاز داریم.

Vector2[] UVs = new Vector2[4];
UVs[0] = new Vector2(0, 1);
UVs[1] = new Vector2(1, 1);
UVs[2] = new Vector2(0, 0);
UVs[3] = new Vector2(1, 0);

باز هم به همون اعداد قبلی نیاز پیدا می کنیم. مش ها از Triangle یا مثلث درست شدن و هر چهارگوش یا Quadrilateral از دو مثلث تشکیل شده. بنابراین باید به مش ها بگیم که این مثلث ها رو چطوری ایجاد کنن.

آموزش Unity : طراحی افکت دو بعدی آب

به زوایا نگاه کنین. مثلث A، نود 0 رو به 1 و 3 وصل کرده. مثلث B نود 3 و 2 و 0 رو به هم وصل کرده. بنابراین باید آرایه ای درست کنیم که هر شش عدد صحیح یا Integer رو در بر بگیره:

int[] tris = new int[6] { 0, 1, 3, 3, 2, 0 };

این طوری Quadrilateral حاصل میشه. حالا مقدار مش ها رو مشخص می کنیم.

meshes[i].vertices = Vertices;
meshes[i].uv = UVs;
meshes[i].triangles = tris;

حالا مش ها رو داریم، اما Game Object نداریم.

meshobjects[i] = Instantiate(watermesh,Vector3.zero,Quaternion.identity) as GameObject;
meshobjects[i].GetComponent<MeshFilter>().mesh = meshes[i];
meshobjects[i].transform.parent = transform;

مش رو طوری تنظیم می کنیم که Child مربوط به Water Manager باشه.

درست کردن Collisions

حالا میریم سراغ Collider:

colliders[i] = new GameObject();
colliders[i].name = "Trigger";
colliders[i].AddComponent<BoxCollider2D>();
colliders[i].transform.parent = transform;
colliders[i].transform.position = new Vector3(Left + Width * (i + 0.5f) / edgecount, Top - 0.5f, 0);
colliders[i].transform.localScale = new Vector3(Width / edgecount, 1, 1);
colliders[i].GetComponent<BoxCollider2D>().isTrigger = true;
colliders[i].AddComponent<WaterDetector>();

Collider رو درست می کنیم و براش اسم میزاریم تا توی صحنه ظاهر بهتری داشته باشه. بعد هم هر کدوم رو به Child مربوط به Water Manager تبدیل می کنیم. موقعیت اون رو طوری مشخص می کنیم که نیمی از فضای بین نودها رو پر کنه. اندازه رو مشخص می کنیم و یک WaterDetector بهشون اضافه می کنیم.
حالا که Mesh رو داریم، به تابعی هم برای آپدیت کردن حرکات آب نیاز داریم:

void UpdateMeshes()
    {
        for (int i = 0; i < meshes.Length; i++)
        {
 
            Vector3[] Vertices = new Vector3[4];
            Vertices[0] = new Vector3(xpositions[i], ypositions[i], z);
            Vertices[1] = new Vector3(xpositions[i+1], ypositions[i+1], z);
            Vertices[2] = new Vector3(xpositions[i], bottom, z);
            Vertices[3] = new Vector3(xpositions[i+1], bottom, z);
 
            meshes[i].vertices = Vertices;
        }
    }

شاید متوجه شده باشین که از این تابع برای کدی که قبلا نوشتیم هم استفاده شده. تنها فرقی که داره اینه که نیازی نیست tris و Uvs رو هم تنظیم کنیم چون یکسان هستن. کار بعدی که باید انجام بدیم اینه که بریم سراغ خود آب. برای اینکار از FixedUpdate() استفاده می کنیم.

void FixedUpdate()
{

اجرای Physics

اول از همه، قانون Hook رو با متد Euler ترکیب می کنیم تا موقعیت، شتاب و سرعت جدید رو پیدا کنیم. طبق قانون Hook داریم(F=kx\) \ جایی که \(F\) نیروی تولید شده توسط یک Spring و \(k\) هم Spring ثابت و \(x\) هم اختلاف مکان هست. اختلاف مکان یعنی Y-position هر نود منهای ارتفاع نودها.
حالا damping factor رو به صورت زیر اضافه می کنیم:

for (int i = 0; i < xpositions.Length ; i++)
        {
            float force = springconstant * (ypositions[i] - baseheight) + velocities[i]*damping ;
            accelerations[i] = -force;
            ypositions[i] += velocities[i];
            velocities[i] += accelerations[i];
            Body.SetPosition(i, new Vector3(xpositions[i], ypositions[i], z));
        }

متد Euler خیلی ساده ست. کافیه شتاب رو به سرعت و سرعت رو به موقعیت هر فریم اضافه کنیم.
نکته: فرض می کنیم که چگالی هر نود برابر با 1 هست.

accelerations[i] = -force/mass;

اگه بخواین می تونین عدد دیگه ای رو فرض کنین.
نکته: برای Physics دقیق، از Verlet Intergration استفاده می کنیم اما از اونجایی که فاکتور damping رو اضافه می کنیم، فقط می تونیم متد Euler رو مورد استفاده قرار بدیم که سرعت محاسبه اون بالاتر هست. اما در مجموع علی رغم این ویژگی، متد Euler انرژی جنبشی در سیستم Physics ایجاد می کنه بنابراین نباید برای محاسبه خیلی دقیق از این متد استفاده کرد. حالا میریم سراغ درست کردن موج.

float[] leftDeltas = new float[xpositions.Length];
float[] rightDeltas = new float[xpositions.Length];

اینجا دو تا آرایه درست کردیم. برای هر نود، ارتفاع نود قبلی رو نسبت به ارتفاع نود کنونی بررسی می کنیم و از leftDeltas استفاده می کنیم.
بعد هم ارتفاع نود دنباله رو نسبت به ارتفاع همین نود چک می کنیم و rightDeltas رو مورد استفاده قرار میدیم.

for (int j = 0; j < 8; j++)
{
    for (int i = 0; i < xpositions.Length; i++)
    {
        if (i > 0)
        {
            leftDeltas[i] = spread * (ypositions[i] - ypositions[i-1]);
            velocities[i - 1] += leftDeltas[i];
        }
        if (i < xpositions.Length - 1)
        {
            rightDeltas[i] = spread * (ypositions[i] - ypositions[i + 1]);
            velocities[i + 1] += rightDeltas[i];
        }
    }
}

سرعت رو بر اساس تفاوت ارتفاع هم می تونیم تغییر بدیم.

for (int i = 0; i < xpositions.Length; i++)
{
    if (i > 0) 
    {
        ypositions[i-1] += leftDeltas[i];
    }
    if (i < xpositions.Length - 1) 
    {
        ypositions[i + 1] += rightDeltas[i];
    }
}

بعد از جمع آوری داده های ارتفاع، اون ها رو اعمال می کنیم. نمیشه اولین نود از سمت راست و اولین نود از سمت چپ رو دید بنابراین از شرط i > 0 و i > xpositions.Length – 1 استفاده می کنیم.

اضافه کردن Splashes

حالا که آب و جریان آب رو داریم، میریم سراغ Splashes یا ریزش آب. برای اینکار، تابع Splash() رو اضافه می کنیم این تابع، موقعیت Spalsh روی بردار x و سرعت هر چیزی که بهش برخورد می کنه رو مشخص می کنه.

public void Splash(float xpos, float velocity)
{

اول، باید از موقعیت دقیق Splash درون محدوده آب، مطمئن بشیم.

if (xpos >= xpositions[0] && xpos <= xpositions[xpositions.Length-1])
{

بعد هم xpos رو تغییر میدیم تا موقعیت شروع آب هم تغییر کنه.

xpos -= xpositions[0];

حالا باید ببینیم این Splash با کدوم نود برخورد می کنه. این رو از طریق کد زیر میشه محاسبه کرد:

int index = Mathf.RoundToInt((xpositions.Length-1)*(xpos / (xpositions[xpositions.Length-1] - xpositions[0])));

بنابراین:

	
velocities[index] = velocity;

حالا سرعت شیئی که با آب برخورد می کنه رو داریم.
نکته: این خط رو اگه بخوایم می تونیم تغییر بدیم. برای مثال، می تونیم سرعت کنونی رو به عنوان سرعت در نظر گرفت.

آموزش Unity : طراحی افکت دو بعدی آب

حالا باید سیستمی از ذرات رو درست کنیم که تولید کننده Spalsh یا ریزش آب باشه. Splash رو قبلا درست کردیم. فقط مراقب باشید که اون رو با Splash() اشتباه نگیرید. اول از همه، پارامترهای Spalsh رو تنظیم می کنیم تا سرعت Object تغییر کنه.

float lifetime = 0.93f + Mathf.Abs(velocity)*0.07f;
splash.GetComponent<ParticleSystem>().startSpeed = 8+2*Mathf.Pow(Mathf.Abs(velocity),0.5f);
splash.GetComponent<ParticleSystem>().startSpeed = 9 + 2 * Mathf.Pow(Mathf.Abs(velocity), 0.5f);
splash.GetComponent<ParticleSystem>().startLifetime = lifetime;

Lifetime مربوط به ذرات رو طوری تنظیم می کنیم که بعد از برخورد با آب، خیلی زود فروکش نکنن. سرعت اون ها رو هم بسته به مجذور شتاب تغییر میدیم. شاید پیش خودتون بگید چرا startSpeed رو دو بار تنظیم کرد. حق دارید که تعجب کنید. یادتون باشه که از Particle System استفاده می کنیم که Start Speed یا سرعت شروع رو تصادفی انتخاب کردیم. متاسفانه خیلی نمی تونیم به اسکریپت Shuriken دسترسی داشته باشیم. بنابراین startSpeed رو دو بار تنظیم می کنیم. حالا یک خط دیگه رو اضافه می کنیم. شما اگه بخواین می تونین این کار رو انجام ندید.

Vector3 position = new Vector3(xpositions[index],ypositions[index]-0.35f,5);
Quaternion rotation = Quaternion.LookRotation(new Vector3(xpositions[Mathf.FloorToInt(xpositions.Length / 2)], baseheight + 8, 5) - position);

موقعی که ذرات به Object برخورد می کنن، از بین نمیرن. بنابراین اگه بخواین می تونین دو تا کار زیر رو انجام بدین:
ذرات رو به پس زمینه بچسبونین (برای اینکار، Z-position رو روی 5 قرار میدیم)
سیستم ذرات رو به شکلی در میاریم که همیشه به سمت مرکز آب قرار بگیرن. این طوری ذرات آب خیلی به خشکی نمی پاشن.
دومین خط کد مربوط میشه به موقعیت میانی و ذرات رو به سمت بالا حرکت میده.

        GameObject splish = Instantiate(splash,position,rotation) as GameObject;
        Destroy(splish, lifetime+0.3f);
    }
}

حالا که Splash رو هم درست کردیم، باید کاری کنیم که قبل از فروکش کردن ذرات، فروکش کنه. چرا؟ چون که سیستم ذرات، توالی اندکی تولید می کنه.

شناسایی Collision یا برخورد

حالا باید Object یا اشیا رو شناسایی کنیم یا همه این کارها رو بی دلیل انجام دادیم؟ یادتون میاد که قبلا تمام اسکریپت ها رو به Collider اضافه کردیم؟ یعنی همون WaterDetector؟
حالا بقیه کار رو انجام میدیم. برای اینکار به تابع زیر نیاز داریم:

void OnTriggerEnter2D(Collider2D Hit)
{

با استفاده از OnTriggerEnter2D()، می تونیم جایی که 2D Rigid Body وارد آب میشه رو تخصیص کنیم.

if (Hit.rigidbody2D != null)
{

فقط به شکل هایی که دارای rigidbody2D هستن نیاز داریم.

      transform.parent.GetComponent<Water>().Splash(transform.position.x, Hit.rigidbody2D.velocity.y*Hit.rigidbody2D.mass / 40f);
    }
}

حالا تمام Collider ها به Child مربوط به Water Manager تبدیل شدن. بنابراین مولفه Water رو از Parent می گیریم و اسمش رو میزاریم Splash(). در نهایت، میریم سراغ SpawnWater().

void Start()
{
    SpawnWater(-10,20,0,-10);
}

این هم از این. حالا تمام rigidbody2D با یک Collider به آب برخورد می کنه و یک Spalsh تولید می کنه.

آموزش Unity : طراحی افکت دو بعدی آب

امیدواریم ” آموزش Unity : طراحی افکت دو بعدی آب ” برای شما مفید بوده باشد…
توصیه می کنم دوره های جامع فارسی مرتبط با این موضوع آریاگستر رو مشاهده کنید:

صفر تا صد آموزش یونیتی سه بعدی – پک 1

صفر تا صد آموزش یونیتی سه بعدی – پک 2

صفر تا صد آموزش یونیتی دو بعدی

توجه : مطالب و مقالات وبسایت آریاگستر تماما توسط تیم تالیف و ترجمه سایت و با زحمت فراوان فراهم شده است . لذا تنها با ذکر منبع آریا گستر و لینک به همین صفحه انتشار این مطالب بلامانع است !

دوره های آموزشی مرتبط

مطالب مرتبط

قوانین ارسال دیدگاه در سایت

  • چنانچه دیدگاهی توهین آمیز یا بی ارتباط با موضوع آموزش باشد تایید نخواهد شد.
  • چنانچه دیدگاه شما جنبه ی تبلیغاتی داشته باشد تایید نخواهد شد.

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

لینک کوتاه: