نگران نیستیم! در حاشیه انتشار کاریکاتور منتسب به خبرگزاری تسنیم

پوریا احمدی

۱۳۹۶/۱۰/۲۳

CBO
b

مدلسازی با Raymarching و میادین فاصله در یونیتی

سید مرتضی کمالی

۱۳۹۶/۱۱/۲۷

راهنمای ارسال مقالات برای گیمولوژی

علیرضا محمدی

۱۳۹۷/۰۱/۲۷

سردبیر
b

شیدر نویسی در یونیتی (بخش دوم)

سید مرتضی کمالی

۱۳۹۶/۰۸/۱۳

چالش‌های بازگشت بازیکنان به بازی

سمیرا دولت‌آبادی

۱۳۹۶/۰۸/۰۲

عضو تحریریه
b

شیدرنویسی در یونیتی (بخش اول)

سید مرتضی کمالی

۱۳۹۶/۰۷/۲۶

مارکتینگ بازی: طراحی برنامه

سمیرا دولت‌آبادی

۱۳۹۶/۰۷/۲۰

عضو تحریریه
b

باز‌ی سازان بزرگ چطور شما را گول می‌زنند؟

فرناز حقیقت

۱۳۹۶/۰۶/۱۶

روابط عمومی
b

قابلیت‌های آنلاین در بازی‌های تک نفره

رهام سجادی

۱۳۹۶/۰۷/۱۵

عضو تحریریه
b

چگونه یک بازی چندنفره ساده در یونیتی بسازیم

(مطلب پیش رو ترجمه‌ای از مقاله سایت gamedevacademy.org همراه با تغییراتی از سوی مترجم است)

در این آموزش ما می‌خواهیم یک دموی ساده بسازیم تا یاد بگیریم چگونه از امکانات چندنفره انجین Unity استفاده کنیم. بازی ما یک Scene ساده خواهد داشت، جایی که ما یک شوتر فضایی چندنفره را پیاده‌سازی خواهیم کرد. در دموی ما، چند بازیکن قادر خواهند بود تا به یک بازی واحد وارد شوند و به دشمنانی که تصادفی ظاهر می‌شوند تیر بزنند.

برای ادامه دادن این آموزش، انتظار می‌رود که با موارد زیر آشنا باشید:

·       برنامه‌نویسی C#

·       استفاده از ادیتور یونیتی و مواردی چون وارد کردن Asset ها، ساخت Prefab و اضافه کردن کامپوننت

 

ساخت پروژه و وارد کردن Asset

پیش از آغاز به خواندن آموزش، نیاز دارید که یک پروژه جدید در یونیتی بسازید و تمام اسپرایت‌های موجود در سورس کد را Import کنید. برای انجام دادن این کار، یک پوشه به نام Sprites بسازید و تمام اسپرایت‌ها را در آن کپی کنید. بخش Inspector یونیتی به طور خودکار آن‌ها را وارد پروژه‌تان خواهد کرد.

با این حال، تعدادی از این اسپرایت‌ها در Spritesheet قرار دارند، مثل موارد مربوط به دشمنان که نیاز است از هم جدا شوند. برای این کار، نیاز است تا Sprite Mode را روی Multiple قرار دهید و اسپرایت ادیتور را باز کنید.

در اسپرایت ادیتور (که در تصویر پایین نشان داده شده)، باید منوی Slice را بازی کنید و روی دکمه Slice کلیک کنید. بخش Slice Typer هم روی Automatic قرار دهید. در نهایت هم دکمه Apply را کلیک کرده و پنجره را ببندید.

فایل‌های سورس کد

می‌توانید فایل‌های سورس کد را از طریق این لینک دانلود کنید.

 

طرح پیش‌زمینه

اولین چیزی که قرار است انجام دهیم، ساخت یک طرح پیش‌زمینه است تا تصویر پیش‌زمینه را نمایش دهیم.

می‌توان این کار را با ساخت یک تصویر(Image) جدید در Hierarchy انجام داد که این کار به طور خودکار یک Canvas خواهد ساخت (یادتان نرود اسم طرح را به BackgroundCanvas تغییر دهید).

در BackgroundCanvas نیاز است که حالت Render Mode به Screen Space – Camera تنظیم شود (یادتان باشد که دوربین اصلی خود را به آن متصل(attach) کنید.). پس از آن، حالت UI Scale Mode را روی Scale With Screen Size تنظیم کنید. با این کار طرح در پیش‌زمینه قرار خواهد گرفته و جلوی آبجکت‌های دیگر ظاهر نخواهد شد.

در بخش BackgroundImage تنها نیاز است که مسیر تصویر پیش‌زمینه را انتخاب کنید.

حالا بازی را اجرا کنید. باید تصویر پیش‌زمینه را در بازی ببینید.

مدیریت شبکه

برای داشتن یک بازی چندنفره، ما به کامپوننت‌های GameObject و NetworkManager و NetworkManagerHUD نیاز داریم. پس به شروع به ساخت آن می‌کنیم.

این آبجکت مسئول مدیریت اتصال کلاینت‌های مختلف در یک بازی است و آبجکت‌های بازی را میان تمام کلاینت‌ها سینک می‌کند. بخش Network Manager HUD یک HUD ساده را برای اتصال به بازی به بازیکنان نشان می‌دهد.

برای مثال، اگر شما همین حالا بازی را اجرا کنید، باید تصویر زیر را ببینید:

در این آموزش قرار است که ما از گزینه‌های LAN Host و LAN Client استفاده کنیم. بازی‌های چندنفره یونیتی به این طریق کار می‌کنند: ابتدا یک بازیکن بازی را به عنوان Host شروع می‌کند (با انتخاب LAN Host). یک هاست همزمان هم به عنوان کلاینت و هم به عنوان سرور کار می‌کند. سپس، دیگر بازیکنان می‌توانند به عنوان کلاینت به این هاست متصل شوند ( با انتخاب LAN Client). کلاینت به سرور متصل می‌شود، اما هیچ کد مختص سرور را اجرا نمی‌کند. بنابراین، برای تست بازی‌مان قرار است دو نمونه داشته باشیم؛ یکی به عنوان هاست و دیگری به عنوان کلاینت.

با این حال، شما نمی‌توانید دو نمونه بازی را در ادیتور یونیتی باز کنید. برای انجام این کار، نیاز است که بازی را Build کنید و نمونه اول از طریق فایل exe اجرا کنید. حالا نمونه دوم را می‌توانید از طریق ادیتور یونیتی اجرا کنید (در حالت Play Mode)

برای این که بازی خود را Build کنید، نیاز است که یک Game Scene به بیلد اضافه کنید. پس به مسیر File -> Build Setting رفته و Game Scene را به بیلد اضافه کنید. سپس می‌توانید فایل اجرایی را از مسیر File -> Build & Run ساخته و اجرا کنید. این کار یک پنجره جدید در کنار بازی باز خواهد کرد. پس از انجام این کار، می‌توانید وارد بخش Play Mode در ادیتور یونیتی شوید تا نمونه دوم بازی را اجرا کنید. این کاری است که هر بار برای تست یک بازی چندنفره باید انجام دهیم.

حرکت سفینه فضایی

حالا که ما NetworkManager را داریم، می‌توانیم شروع به ساخت آبجکت‌هایی کنیم که با آن مدیریت می‌شوند. اولین چیزی که قرار است بسازیم سفینه بازیکن است.

در حال حاضر، سفینه تنها به صورت افقی در صفحه حرکت خواهد کرد و مختصات‌اش توسط NetworkManager به‌روز می‌شود. در مراحل بعد قابلیت شلیک گلوله و آسیب دیدن را هم به آن اضافه خواهیم کرد.

بنابراین اول از همه، یک گیم‌آبجکت جدید به نام Ship بسازید و آن را به یک Prefab تبدیل کنید. تصویر زیر اجزای Prefab سفینه را نشان می‌دهد که چیزی است که الان توضیح خواهیم داد.

برای این که یک گیم آبجکت توسط NetworkManager مدیریت شود، نیاز داریم تا کامپوننت NetworkIdentity را به آن اضافه کنیم. به علاوه، چون سفینه توسط بازیکن کنترل می‌شود، باید تیک گزینه Local Player Authority را نیز قرار دهیم.

کامپوننت NetworkTransform مسئول به‌روزرسانی مختصات سفینه در سرور و تمام کلاینت‌هاست. در صورت استفاده نکردن از آن، اگر سفینه را در صفحه حرکت دهیم، مکان‌اش در صفحه‌های دیگر بازیکنان تغییر نخواهد کرد. کامپوننت‌های NetworkIdentity و NetworkTransform دو کامپوننت ضروری برای امکانات بخش چندنفره هستند.

حالا، برای به دست‌گرفتن حرکت و برخوردها، نیاز داریم تا RigidBody2D و BoxCollider2D را اضافه کنیم. این را هم اضافه کنم که BoxCollider2D یک تریگر (با مقدار پیش‌فرض True) خواهد بود، چون ما نمی‌خواهیم که برخوردها فیزیک سفینه را تحت تاثیر قرار دهد

در نهایت، اسکریپت MoveShip را اضافه خواهیم کرد که یک پارامتر سرعت خواهد داشت. اسکریپت‌های دیگر بعدتر اضافه خواهد شد و در حال حاضر همین یک مورد را داریم.

اسکریپت MoveShip بسیار ساده است. در متد FixedUpdate مختصات حرکت را از محور افقی می‌گیریم و سرعت سفینه را بر اساس آن تنظیم می‌کنیم. با این وجود، دو مورد بسیار مهم و مربوط به شبکه وجود دارد که باید توضیح داده شوند.

اول، به طور معمول تمام اسکریپت‌ها در یک بازی ساخته شده با یونیتی از MonoBehaviour برای استفاده از API آن کمک می‌گیرند. برای این که از API شبکه استفاده کنیم، اسکریپت باید به جای MonoBehaviour از NetworkBehaviour بهره بگیرد. برای انجام این کار نیاز دارید که منبع(Namespace) شبکه را (با استفاده از UnityEngine.Networking) قرار دهید.

همچنین در یک بازی چندنفره در یونیتی، یک کد یکسان در تمام سمت‌ها (چه سرور و چه کلاینت) اجرا می‌شود. برای این که هر بازیکن تنها سفینه خودش را کنترل کند و به سفینه‌های دیگر دسترسی نداشته باشد، نیاز داریم که یک موقعیت شرطی (If Condition) در ابتدای متد FixedUpdate قرار دهیم که چک کند آیا بازیکن یک بازیکن محلی است یا خیر (اگر کنجکاو هستید که بازی چگونه بدون این شرط کار می‌کند، آن را پاک کنید. خواهید دید که موقع حرکت یک سفینه در صفحه، همه آن‌ها با هم حرکت می‌کنند).

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

قبل از بازی کردن، ما هنوز نیاز داریم به NetworkManager بگوییم که Prefab سفینه همان Prefab بازیکن است. ما این کار را با انتخاب Prefab سفینه در قسمت Player Prefab در کامپوننت NetworkManager انجام می‌دهیم. با این کار، هر بار که بازیکن یک نمونه جدید از بازی را آغاز می‌کند، یک سفینه توسط NetworkManager به آن اختصاص داده می‌شود.

حالا شما می‌توانید بازی را امتحان کنید. حرکت سفینه باید بین دو طرف (کلاینت) هماهنگ باشد.

مختصات ظاهر شدن

تا حالا تمام سفینه‌ها در وسط صفحه ظاهر می‌شدند، با این حال این می‌تواند جالب باشد که چند نقطه ظهور از پیش تعریف شده داشته باشید, کاری که انجامش با API بخش چندنفره یونیتی واقعا آسان است.

ابتدا احتیاج داریم که یک گیم آبجکت جدید بسازیم تا موقعیت ظاهر شدن‌مان باشد و آن را در نقطه ظاهر شدن دلخواه قرار دهد. سپس ما کامپوننت NetworkStartPosition را به آن اضافه می‌کنیم. ما قرار است تا دو نقطه ظاهر شدن خلق کنیم؛ یکی در مختصات (-4,-4) و دیگری در مختصات (4,-4).

حالا ما احتیاج داریم که تعریف کنیم چگونه NetworkManager آن موقعیت‌ها را استفاده کند. ما این کار را با تنظیم بخش Player Spawn Method انجام می‌دهیم. دو حالت برای این بخش وجود دارد؛ حالت Random و حالت Round Robin که در حالت اول، محل ظهور سفینه به طور تصادفی از میان یکی از مختصات از پیش تعیین شده انتخاب می‌شود و در حالت دوم، به ترتیب بین مختصات می‌چرخد تا وقتی که همه آن‌ها استفاده شود و پس از آن دوباره از ابتدای لیست شروع می‌کند. در این جا ما به سراغ حالت Round Robin می‌رویم.

حالا می‌توانید دوباره بازی را اجرا کنید و ببینید که آیا سفینه‌ها در مختصات درست ظاهر می‌شوند یا خیر.

شلیک گلوله

مورد بعدی که می‌خواهیم به بازی‌مان اضافه کنیم، قابلیت شلیک گلوله توسط سفینه‌هاست. لازم به ذکر است که گلوله‌ها باید بین تمام نمونه‌ها هماهنگ باشد و به طور همزمان برای هر دو بازیکن اجرا شود.

اول از همه نیاز داریم تا Prefab گلوله را بسازیم؛ پس یک گیم آبجکت با نام Bullet ایجاد کرده و آن را به Prefab تبدیل می‌کنیم. برای مدیریت این مورد در شبکه، همچون سفینه به دو کامپوننت NetworkIdentity و NetworkTransform نیاز داریم. با این حال وقتی یک گلوله ایجاد می‌شود، نیازی نیست که مختصاتش در شبکه منتشر شود، چون مختصات با موتور فیزیکی به‌روز می‌شود. بنابراین میزان قسمت Network Send Rate را روی صفر می‌گذاریم تا از بارگذاری اضافی روی شبکه جلوگیری شود.

گلوله‌ها همچنین دارای سرعت هستند و بعدتر باید به دشمنان نیز برخورد کنند. بنابراین کامپوننت‌های RigidBody2D و CircleCollider2D را به Prefab اضافه می‌کنیم. لازم است دوباره یادآوری کنیم که CircleCollider2D یک تریگر (متغیر بین دو حالت فعال و غیر فعال) است.

حالا که ما Prefab گلوله را داریم، می‌توانیم یک اسکریپت برای شلیک گلوله‌ها به سفینه اضافه کنیم. این اسکریپت پارامترهایی برای سرعت گلوله و Prefab گلوله خواهد داشت.

اسکریپت ShootBullets نیز از NetworkBehaviour بهره می‌برد و در پایین نشان داده شده است. در متد به‌روزرسانی، بررسی می‌شود که آیا بازیکن دکمه Space (به عنوان دکمه شلیک) را فشار داده یا خیر و در صورت فشار دادن، متدی را فرا می‌خواند تا گلوله شلیک کند. این متد یک گلوله جدید ایجاد کرده، سرعت آن را تنظیم و پس از یک ثانیه (مدتی که طول می‌کشد تا از صفحه خارج شود) آن را نابود می‌کند.

در این جا لازم است تا چند نکته مربوط به شبکه در اسکریپت توضیح داده شود. ابتدا این که یک تگ [Command] بالای متد CmdShoot وجود دارد. این تگ و عبارت Cmd در ابتدای نام متد آن را به یک متد خاص تبدیل می‌کند که «فرمان» (Command) نام دارد. در یونیتی، فرمان متدی است که در سرور اجرا می‌شود، اگر چه در کلاینت فرا خوانده می‌شود. در این مورد، وقتی بازیکن تیری شلیک می‌کند، به جای فرا خواندن متد در کلاینت، بازی یک درخواست به سرور می‌فرستد و سرور فرمان را اجرا خواهد کرد.

مورد بعد این که در متد CmdShoot یک فراخوانی به NetworkServer.Spawn وجود دارد. متد Spawn یا همان ظاهرسازی مسئول ایجاد گلوله در تمامی نمونه‌ها و برای تمام کلاینت‌هاست. بنابراین کاری که CmdShoot انجام می‌دهد این است که یک گلوله در سرور ایجاد و سپس سرور آن را بین تمامی کلاینت‌ها تکثیر می‌کند. نکته مهم این که این امر تنها به این خاطر ممکن است که CmdShoot یک فرمان است و یک متد معمول نیست. اسکریپت مورد نظر در ادامه آمده است:

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

در نهایت نیاز داریم به Network Manager بگوییم که می‌تواند گلوله‌ها را ظاهر کند. ما این کار را با اضافه کردن Prefab گلوله به فهرست بخش Registered Spawnable Prefab انجام می‌دهیم.

حالا می‌توانید دوباره بازی را امتحان کرده و این بار به شلیک گلوله بپردازید. گلوله‌ها باید بین تمامی نمونه‌ها و کلاینت‌ها هماهنگ و همزمان باشند.

ظاهر کردن دشمنان

قدم بعد در بازی ما اضافه کردن دشمنان است.

ابتدا ما به یک Prefab برای دشمنان نیاز داریم. بنابراین یک گیم‌آبجکت جدید با نام Enemy ساخته و آن را به Prefab تبدیل می‌کنیم. همچون سفینه، دشمنان هم دارای RigidBody2D و BoxCollider2D هستند تا حرکات و برخوردها کنترل شوند. همچنین به NetworkIdentity و NetworkTransform نیاز است که توسط NetworkManager کنترل می‌شوند. در مراحل جلوتر یک اسکریپت نیز اضافه خواهیم کرد، اما تا به این جا همین کارها باید انجام شود.

حالا یک گیم‌آبجکت جدید با نام EnemySpawner می‌سازیم که یک NetworkIdentity نیز خواهد داشت. اما اینجا ما بخش Server Only را در کامپوننت انتخاب می‌کنیم. با این کار، ظاهرکننده تنها در سرور موجود خواهد بود، چون نمی‌خواهیم که دشمنان در هر کلاینت ساخته شوند. این بخش یک اسکریپت نیز خواهد داشت که دشمنان را در یک فاصله معمول ظاهر می‌کند (پارامترها شامل Prefab دشمن، فاصله ظاهر شدن و سرعت دشمن است).

اسکریپت SpawnEnemies در پایین نشان داده شده است. نکته مهم این که در این جا از یک متد جدید در یونیتی استفاده می‌کنیم که OnStartServer نام دارد. این متد بسیار شبیه OnStart است، با این تفاوت که تنها در سرور فراخوانی می‌شود. وقتی این اتفاق می‌افتد، ما InvokeRepeating را فراخوانی می‌کنیم تا متد SpawnEnemy را هر یک ثانیه یک بار فراخوانی کند (بر طبق SpawnInterval).

متد SpawnEnemy یک دشمن جدید را در یک مختصات تصادفی ایجاد خواهد کرد و از NetworkServer.Spawn استفاده می‌کند تا آن را میان نمونه‌ها و کلاینت‌ها تکثیر کند. در نهایت، دشمن بعد از ده ثانیه نابود خواهد شد.

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

قبل از امتحان بازی، نیاز داریم تا Prefab دشمن را به فهرست بخش Registered Spawnable Prefabs اضافه کنیم.

حالا می‌توانید بازی را با وجود دشمنان امتحان کنید. لازم به ذکر است که بازی هنوز هیچ کنترل برخوردی ندارد، بنابراین نمی‌توانید به دشمنان شلیک کنید و این کار قدم بعدی‌مان خواهد بود.

آسیب رساندن

آخرین چیزی که قرار است به بازی‌مان اضافه کنیم، قابلیت صدمه زدن به دشمنان و متاسفانه، مردن و باختن به آن‌هاست. برای ساده نگه داشتن این آموزش، از یک اسکریپت یکسان برای سفینه و دشمنان استفاده می‌کنیم.

اسکریپتی که قرار است استفاده شود ReceiveDamage نام دارد که در پایین‌تر نشان داده شده است. این اسکریپت دارای پارامترهای maxHealth و enemyTag و destroyOnDeath است؛ پارامتر اول میزان سلامتی اولیه آبجکت را تعیین می‌کند، دومی برای تعیین چیزی که باعث آسیب می‌شود به کار می‌رود. مثلا مقدار enemyTag برای سفینه، دشمنان (Enemy) و برای دشمنان، گلوله (Bullet) است. با این کار، تنها گلوله به دشمن و تنها دشمن به سفینه شما آسیب می‌زند. پارامتر آخر هم تعیین می‌کند که آیا آبجکت پس از مردن نابود می‌شود یا دوباره ظاهر می‌گردد.

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

حالا به بررسی متدها می‌پردازیم. در متد آغازین، اسکریپت مقدار currentHealth را بالاترین میزان ممکن قرار می‌دهد و مختصات مکان اولیه را نگه می‌دارد (این مختصات برای وقتی که سفینه دوباره ظاهر می‌شود به کار می‌آید). همچنین اطلاع می‌دهد که تگ [SyncVar] در بالای تعریف currentHealth قرار گرفته است. این به معنی آن است که مقدار این ویژگی باید بین تمام نمونه‌ها یکسان و هماهنگ باشد. بنابراین اگر سفینه یک بازیکن آسیب ببیند، این آسیب در سرور و کلاینت‌های دیگر نیز دیده می‌شود.

متد OnTriggerEnter2D مسئول کنترل و مدیریت برخوردهاست (چون برخوردکننده‌ها به صورت تریگر تنظیم شده‌اند). ابتدا، چک می‌کنیم که تگ برخوردکننده همان باشد که در enemyTag دنبالش هستیم تا برخوردهای در برابر آبجکت مورد نظر را کنترل کند (دشمنان در برابر سفینه و گلوله در برابر دشمنان). پس از آن متد TakeDamage را فرا می‌خوانیم و آبجکت دیگر که آسیب رسانده را نابود می‌کنیم.

متد TakeDamage در نوبت خود تنها در سرور فراخوانی می‌شود، چون که currentHealth از نوع SyncVar است. بنابراین تنها در سرور به‌روز می‌شود و بین کلاینت‌ها آن را هماهنگ می‌کنیم. به غیر از این، متد TakeDamage ساده است؛ مقدار currentHealth را کاهش می‌دهد و چک می‌کند که کمتر مساوی صفر هست یا خیر. اگر این شرط برقرار شود آن را نابود می‌کند و مقدار destroyOnDeath را True می‌کند و در این صورت، مقدار currentHealth ریست و آبجکت دوباره در صفحه ظاهر می‌شود. برای تمرین، ما دشمنان را به گونه‌ای قرار خواهیم داد که در هنگام مرگ نابود شده و سفینه را به گونه‌ای خواهیم گذاشت که پس از مرگ دوباره ظاهر می‌شوند.

آخرین متد مربوط به دوباره ظاهر شدن (Respawn) است. در این جا از یکی دیگر از امکانات مخصوص بخش چندنفره به اسم ClientRpc استفاده می‌کنیم (به تگ [ClientRpc] در بالای تعریف متد توجه کنید). این تگ در واقع عملکردی مخالف عملکرد تگ Command یا فرمان دارد؛ فرمان‌ها از کلاینت به سرور فرستاده می‌شود، اما ClientRpc در کلاینت اجرا می‌شود. حتی اگر متد از سوی سرور فراخوانی شده باشد. بنابراین وقتی نیاز است تا یک آبجکت دوباره ظاهر شود، سرور یک درخواست به کلاینت می‌فرستد تا متد RpcRespawn را اجرا کند (اسم متد باید با Rpc شروع شود) که به سادگی مختصات را به حالت اولیه بر می‌گرداند. این متد باید در کلاینت اجرا شود، چرا که ما می‌خواهیم آن را برای سفینه‌ها فراخوانی کنیم و سفینه‌ها تنها توسط بازیکن‌ها کنترل می‌شود (ما مقدار بخش Local Player Authority را در کامپوننت NetworkIdentity روی True قرار داده‌ایم).

در نهایت، نیاز است تا این اسکریپت را به Prefab های سفینه و دشمن اضافه کنیم. حواس‌تان باشد که باید تگ دشمن را برای سفینه Enemy و برای دشمن Bullet قرار دهید (احتمالا نیاز خواهید داشت تا تگ‌های Prefab ها را نیز تعریف کنید). همچنین در Prefab دشمن باید ویژگی Destroy on Death را فعال قرار دهیم.

حالا می‌توانید بازی را امتحان کرده و به دشمنان شلیک کنید. بگذارید دشمنان به سفینه شما برخورد کنند و مطمئن شوید که آیا سفینه‌ها بعد از نابودی به درستی ظاهر خواهند شد یا خیر.

 

هر گونه انتقاد یا پیشنهاد در رابطه با این مطلب را در بخش کامنت‌ها در میان بگذارید.

دیدگاه‌ها

پاسخ
٢
٠

مهدی   |   ۱۳۹۷/۰۸/۰۱   |   ۱۹:۱۵

خیلی هم عالی
فقط یه پیشنهاد دارم این همه وقت میزارید برای نوشتن و عکس گذاشتن ویدیو اموزشی میزاشتید هم درک ما بهتر بود هم سریع تر تولید میشد