بخش دهم آموزش یونیتی متوسطه (آموزش ساخت بازی ماجراجویی)
فهرست مطالب
آخرین به روزرسانی در 29/07/2022
در بخش دهم از آموزش یونیتی متوسطه قصد آموزش پروژه محور ساخت یک بازی ماجراجویی را داریم.
در این آموزش یاد خواهید گرفت که چگونه عملکرد اصلی بازی های text-based (مبتنی بر متن) مانند King’s Quest I را با استفاده از Unity پیاده سازی کنید.
هنگامی که King’s Quest I در سال 1984 منتشر شد، نقطه فروش اصلی آن استفاده از صفحه نمایش های غیر استاتیک بود که به طور پویا به ورودی بازیکن پاسخ می دادند.
به عنوان مثال، بازیکن می تواند شخصیت را به صورت ریل تایم حرکت دهد.
در این آموزش یاد خواهید گرفت که چگونه عملکرد اصلی King’s Quest I را با استفاده از Unity برای ساختن یک کلون ساده به نام Wolf’s Quest پیاده سازی کنید.
در این آموزش شما یاد خواهید گرفت که چگونه :
- دستورات متن را تجزیه کنید
- Fake 3D در دنیای دوبعدی
- تعاملات مبتنی بر متن را در ویرایشگر Unity پیاده سازی کنید.
پیش نیاز دوم این دوره ی آموزشی تسلط و درک مفاهیم اولیه ی زبان برنامه نویسی C# می باشد.
شروع کار
با کلیک بر روی دکمه Download Materials نیازمندی های این پروژه را در درجه ی اول دانلود کنید.
Unity را باز کنید، فایل فشرده را استخراج کنید و پروژه شروع را باز کنید.
این پروژه حاوی چند پوشه است که به شما در شروع کار کمک می کند.
Assets/RW را باز کنید و دایرکتوری های زیر را پیدا کنید :
انیمیشن ها شامل انیمیشن های از پیش ساخته شده و انیماتور برای شخصیت اصلی هستند.
Resources دارای فایل های فونت و یک فایل صوتی برای محیط است.
Scenes شامل صحنه اصلی است که با آن کار خواهید کرد.
اسکریپت تمام اسکریپت های پروژه را دارد.
Sprites شامل تمام هنرهای پیکسلی پروژه است.
در Unity Editor به Game View بروید و از منوی کشویی برای تنظیم نسبت تصویر روی 4:3 استفاده کنید.
اکنون به RW/Scenes بروید و Main را باز کنید.
روی Play کلیک کنید و یک پوشش متنی خواهید دید.
روی دکمه بستن در بالا سمت راست کلیک کنید تا پنهان شود.
به انیمیشن های شخصیت گرگینه به تنهایی توجه کنید.
انیمیشنهای گرگینه گنجانده شده است تا بتوانید روی اجرای اصلی تمرکز کنید.
در حال حاضر، نمیتوانید شخصیت را جابهجا کنید، بنابراین این چیزی است که بعداً روی آن کار خواهید کرد.
یک ناحیه سیاه برای تایپ دستورات در پایین Game View وجود دارد.
چیزی را در آنجا تایپ کنید و Return را فشار دهید.
هنوز هیچ چیز جالبی رخ نداده است، اما به زودی آن را تغییر خواهید داد.
بازی را متوقف کنید و به Hierarchy نگاه کنید.
Boundary GameObject دارای collider هایی است که از بیرون رفتن شخصیت از camera view جلوگیری میکند.
Main Camera تنظیم شده است و یک منبع صوتی متصل دارد که صدای محیطی خوبی را به صورت حلقه پخش می کند.
یک Canvas برای رابط کاربری همپوشانی تنظیم شده است.
این کامپوننت UI Manager متصل است و حاوی GameObject های فرزند زیر است :
InputField : این GameObject دارای یک کامپوننت Input Field است.
از آن برای وارد کردن دستورات درون بازی استفاده خواهید کرد.
Dialogue : این رابط کاربری همپوشانی متنی است که قبلاً مشاهده کردید.
یک کامپوننت Canvas به آن متصل شده است.
همچنین دارای یک GameObject فرزند دکمه Close است که با کلیک کردن، کامپوننت Canvas را غیرفعال می کند.
همچنین Character GameObject با کامپوننت Animator از قبل تنظیم شده است.
توجه داشته باشید که دارای Edge Collider و Rigidbody 2D نیز می باشد.
اکنون Environment GameObject را توسعه دهید.
توجه داشته باشید که چندین GameObject دارد. بعداً برخی از آنها را برای تعاملات درون بازی تنظیم خواهید کرد.
در حال حاضر، توجه کنید که چگونه اکثر آنها دارای یک رندر Sprite هستند در حالی که برخی از آنها دارای یک کامپوننت Box Collider 2D هستند.
در نهایت، به طور خلاصه به انیماتور گرگینه نگاه کنید.
پنجره Animator را باز کرده و Character را در hierarchy انتخاب کنید.
همانطور که می بینید ، دو پارامتر Int ، X و Y، برای انتقال در ماشین transitions وجود دارد.
states به شرح زیر است:
WalkRight : برای transition به این حالت، مقادیر پارامتر (X,Y) باید (1، 0) باشد.
WalkLeft : برای transition به این حالت، مقادیر پارامتر (X,Y) باید (-1، 0) باشد.
WalkUp : برای این حالت، مقادیر پارامتر (X,Y) باید (0، 1) باشد.
WalkDown : برای این حالت، مقادیر پارامتر (X,Y) باید (0، -1) باشد.
حرکت دادن کارکتر
در King’s Quest I، برای جابجایی کاراکتر ، یک بار کلید arrow را فشار دهید.
برای متوقف کردن کاراکتر نیز همان کلید را فشار دهید.
اگر هر کلید arrow دیگری را در حین حرکت فشار دهید ، جهت کاراکتر تغییر می کند.
شما آن سبک حرکت شخصیت را در این بخش تکرار خواهید کرد.
به RW/Scripts بروید و CharacterMovement.cs را در code editor خود باز کنید.
دستور //TODO: Require statement goes here را با [RequireComponent(typeof(Animator))] جایگذاری کنید.
این تمرین خوبی است که به جای اینکه فرض کنیم یک کامپوننت ضروری، در این مورد یک Animator، در GameObject که اسکریپت به آن متصل است، وجود دارد.
حالا کد زیر را داخل کلاس قرار دهید :
[SerializeField] private float speed = 2f;
private Animator animator;
private Vector2 currentDirection = Vector2.zero;
private void Awake()
{
animator = GetComponent();
animator.speed = 0;
}
کدی که اضافه کردید :
سرعت متغیری را اعلام می کند که سرعت حرکت کاراکتر را ذخیره می کند.
SerializeField این فیلد خصوصی را از طریق Inspector قابل دسترسی می کند.
متغیر animator اعلام شده و بعداً در داخل Awake مقداردهی اولیه می شود تا کامپوننت Animator متصل به GameObject ذخیره شود.
سرعت animator در Awake روی صفر تنظیم شده است ، بنابراین انیمیشن شخصیت در ابتدا متوقف می شود.
این اسکریپت همان طور که بعداً خواهید دید، سرعت انیماتور را بین صفر و یک تغییر می دهد.
CurrentDirection یک Vector2 است که جهت دوبعدی حرکت کاراکتر را ذخیره می کند.
به مقدار پیش فرض خود مقداردهی اولیه شده است.
اکنون باید حرکت واقعی را کدنویسی کنید.
زیر Awake را کپی و پیست کنید :
private void StopMovement()
{
animator.speed = 0;
StopAllCoroutines();
}
private void ToggleMovement(Vector2 direction)
{
StopMovement();
if (currentDirection != direction)
{
animator.speed = 1;
animator.SetInteger("X", (int)direction.x);
animator.SetInteger("Y", (int)direction.y);
StartCoroutine(MovementRoutine(direction));
currentDirection = direction;
}
else
{
currentDirection = Vector2.zero;
}
}
private IEnumerator MovementRoutine(Vector2 direction)
{
while (true)
{
transform.Translate(direction * speed * Time.deltaTime);
yield return null;
}
}
MovementRoutine کاراکتر را با مقدار سرعت در ثانیه در جهت ۲ بعدی مشخص شده توسط جهت پارامتر ورودی Vector2 ترنسلیت می کند.
این برنامه به کار خود ادامه می دهد تا زمانی که در جای دیگری متوقف شود.
StopMovement سرعت animator را صفر می کند.
همچنین هر گونه برنامههای در حال اجرا را متوقف میکند ، که اساساً MovementRoutine را متوقف میکند.
ToggleMovement جهت پارامتر Vector2 را می پذیرد.
قبل از انجام هر کاری ، StopMovement را صدا می کند.
سپس بررسی می کند که آیا جهت به روز شده است یا خیر.
اگر currentDirection همان جهت باشد، مقدار currentDirection به مقدار پیش فرض Vector2.zero بازنشانی می شود.
سپس متد برمی گردد ، که به طور موثر حرکت شخصیت را متوقف می کند.
با این حال، اگر جهت تغییر کرده باشد، ابتدا animator.speed روی یک تنظیم می شود تا انیمیشن فعال شود، سپس پارامترهای عدد صحیح انیماتور X و Y را به ترتیب روی مقادیر direction.x و direction.y تنظیم کنید تا state انیمیشن را تنظیم کنید.
بالاخره MovementRoutine شروع می شود.
برای اینکه این کد کار کند ، باید از آن در حلقه Update استفاده کنید.
بعد از Awake موارد زیر را بچسبانید :
private void Update()
{
if (Input.GetKeyDown(KeyCode.UpArrow)) ToggleMovement(Vector2.up);
if (Input.GetKeyDown(KeyCode.LeftArrow)) ToggleMovement(Vector2.left);
if (Input.GetKeyDown(KeyCode.DownArrow)) ToggleMovement(Vector2.down);
if (Input.GetKeyDown(KeyCode.RightArrow)) ToggleMovement(Vector2.right);
}
این کد ورودی کلیدهای جهت دار را نظرسنجی می کند و زمانی که کاربر یک کلید پیکان را فشار می دهد، ToggleMovement را فراخوانی می کند.
هر کلید arrow با جهت دو بعدی مرتبط است.
سپس این arrow را به ToggleMovement منتقل می کند.
همه چیز را ذخیره کنید و به صحنه اصلی در یونیتی بازگردید.
Play را فشار دهید و با فشار دادن هر کلید پیکان ، کاراکتر را حرکت دهید.
برای متوقف کردن کاراکتر ، همان کلید arrow را فشار دهید یا برای تغییر جهت ، کلید arrow دیگری را فشار دهید.
توجه داشته باشید که collider ها در صحنه ، مانع از عبور کاراکتر می شوند، به لطف Edge Collider که به شخصیتی که قبلا دیده اید و اجزای Box Collider 2D بر روی اشیاء دیگر متصل شده است.
همچنین ممکن است متوجه برخی مسائل عجیب و غریب در مرتب سازی sprite شوید.
نگران نباشید، بعداً این مشکلات را برطرف خواهید کرد.
برخی از مسائل وجود دارد که میخواهید فوراً آن را حل کنید : اگر شخصیت با collider برخورد کند به انیمیشن ادامه میدهد و MovementRoutine همچنان در حال اجراست.
برای اصلاح این رفتار ، بعد از همه متدهای موجود در CharacterMovement.cs، موارد زیر را جایگذاری کنید :
private void OnCollisionEnter2D(Collision2D other)
{
StopMovement();
}
این تضمین می کند که انیمیشن و همچنین MovementRoutine زمانی که شخصیت به collider برخورد کند متوقف می شود.
OnCollisionEnter2D کار می کند زیرا شخصیت دارای یک کامپوننت Rigid Body 2D متصل است.
هر فعل و انفعال فیزیكی كه گرگینه داشته باشد این متد را برای اجرای آن آغاز می كند.
همه چیز را ذخیره کنید و دوباره بازی کنید تا آن را آزمایش کنید.
این فقط مسائل sorting را برای “sorting ” باقی می گذارد.
در بخش بعدی آنها را برطرف خواهید کرد.
Faking 3d در دنیای 2d
King’s Quest I با نشان دادن شخصیت اصلی در پشت یا جلوی اسپرایت های درون بازی بر اساس موقعیت مکانی اش، افکت سه بعدی را فیکینگ (نمونه برداری)کرد.
با استفاده از قابلیت 2D Sorting می توانید این افکت را در Unity تکرار کنید.
Sorting Sprites
در یونیتی، تمام رندرهای دوبعدی ، که شامل رندرهای اسپریت میشود، با یک لایه مرتبسازی مرتبط هستند.
در لایه مرتبسازی، Order in Layer اولویت رندر را در صف رندر تعیین میکند.
به Edit ▸ Project Settings ▸ Tags and Layers بروید و Sorting Layers را گسترش دهید.
توجه داشته باشید که این پروژه دارای سه لایه است ، به استثنای لایه Default
- Landscape
- Foreground
- Darkness
بالاترین لایه مرتبسازی ابتدا در صف رندر قرار میگیرد و سپس بقیه.
بنابراین، اسپرایت های مرتبط با لایه Landscape ابتدا رندر می شوند و پس از آن Foreground و Darkness قرار می گیرند.
از آنجا که آنها ابتدا رندر کردند، آن sprites پشت سر آنهایی که بعداً رندر می شوند قرار خواهند گرفت.
در صحنه اصلی، رندر sprite برای Background مرتبسازی آن روی Landscape و رندر Darkness GameObject روی لایه Darkness تنظیم شده است.
بقیه رندرهای sprite، از جمله یکی برای Character، دارای یک لایه مرتبسازی هستند که روی Foreground تنظیم شده است.
نکته : لایه Darkness در صحنه Final در داخل پروژه Final کاربرد بیشتری دارد. پس از تکمیل آموزش می توانید به آن نگاهی بیندازید.
حتی اگر Sprites های Foreground یک لایه مرتب سازی مشترک دارند، می توانید ترتیب لایه ها را برای هر یک در زمان اجرا بر اساس فاصله عمودی از نمای بالای دوربین تغییر دهید.
این به شما کمک می کند جلوه سه بعدی را فیکینگ کنید.
برای محاسبه این فاصله عمودی، مختصات Y بالاترین نقطه در نمای دوربین را پیدا کنید. از فرمول زیر استفاده کنید:
نقطه بالا مختصات Y = دوربین Y مختصات + اندازه Orthographic دوربین
سپس ترتیب لایه را برای اسپرایت به عنوان یک تابع ریاضی مختصات Y نقطه بالایی و مختصات Y اسپرایت تنظیم کنید.
ساده ترین راه برای انجام این کار این است که تفاوت مطلق آنها را در نظر بگیرید.
تنظیم ترتیب در لایه
برای پیاده سازی ترتیب در layer adjustment در زمان اجرا ، به RW/Scripts رفته و SetSortingOrder.cs را باز کنید.
کد زیر را داخل کلاس قرار دهید:
[SerializeField] private float accuracyFactor = 100;
private Camera cam;
private SpriteRenderer sprite;
private float topPoint;
private void Awake()
{
sprite = GetComponent();
cam = Camera.main;
}
این کد متغیرها را برای ذخیره مرجع دوربین اصلی صحنه و مرجع رندرر Sprite GameObject اعلام می کند و بعداً در Awake مقداردهی اولیه می کند.
همچنین متغیرهای topPoint و accuracyFactor را اعلام می کند.
از آنها برای محاسبه ترتیب در لایه استفاده خواهید کرد.
بعد از Awake method body :
public void SetOrder()
{
topPoint = cam.transform.position.y + Camera.main.orthographicSize;
sprite.sortingOrder = (int)(Mathf.Abs(topPoint - transform.position.y) * accuracyFactor);
}
SetOrder ابتدا topPoint را بر اساس فرمول قبلی محاسبه می کند.
سپس sortingOrder یا ترتیب در لایه را تنظیم می کند.
ترتیب مرتب سازی را با در نظر گرفتن تفاوت مطلق topPoint و مختصات sprite Y، همانطور که قبلاً بحث شد، تنظیم می کنید.
سپس این عملیات را با ضرب اختلاف در accuracyFactor دنبال میکنید که محدوده خروجی را تغییر میدهد.
افزایش این مقدار منجر به تغییرات مرتب سازی دقیق تری با حرکت شخصیت در صحنه می شود.
اما به خاطر داشته باشید که مقدار سفارش در لایه باید بین -32768 و 32767 باشد.
حالا ()SetOrder ; به عنوان خط پایانی داخل Awake برای اطمینان از این که این متد هنگام بارگذاری صحنه فراخوانی می شود.
همه چیز را ذخیره کنید و به RW/Scripts بروید.
SetRelativeSorting.cs را باز کنید و موارد زیر را در body کلاس قرار دهید :
public SpriteRenderer referenceSprite;
public int relativeOrder;
private void Start()
{
GetComponent().sortingOrder =
referenceSprite.sortingOrder + relativeOrder;
}
این اسکریپت ترتیب لایه ها را برای یک sprite نسبت به یک referenceSprite تنظیم می کند.
ترتیب در لایه sprite با relativeOrder از ترتیب در لایه referenceSprite به بالا تغییر می کند.
این کار مواردی را کنترل می کند که مرتب سازی (sorting ) باید به جای وابسته به مختصات Y وابسته به مختصات گیم آبجکت باشد.
همه چیز را ذخیره کنید و به صحنه اصلی (Main scene) بازگردید.
کامپوننت Set Sorting Order را به sprite های زیر وصل کنید :
- Character
- Lit
- Unlit
- Tree Sprite
- well-sprite
اکنون کامپوننت Set Relative Sorting را به sprites زیر وصل کنید:
همه اشیاء فرزند Apples
- AppleOnTree
- bucket
- bucketGlow
- ropeInside
برای AppleOnTree و همه اشیاء فرزند Apple، Reference Sprite را روی Tree Sprite قرار دهید.
Relative Order را روی ۱ قرار دهید.
برای bucket باید bucketGlow و ropeInside، Reference Sprite را روی well-sprite تنظیم کنید.
سپس برای bucket و ropeInside، Relative Order را روی ۱ قرار دهید.
در نهایت ، برای bucketGlow، Relative Order را روی ۲ قرار دهید.
اکنون، هر یک از sprites های فوق الذکر را در Hierarchy انتخاب کنید و چشم خود را به Inspector نگاه دارید.
همه چیز را ذخیره کنید و Play را فشار دهید.
توجه کنید که چگونه مقدار Order in Layer در Sprite Renderer هنگام پلی صحنه به روز می شود.
اگر در این مرحله کاکتر را جابجا کنید، همچنان با مشکلات مرتبسازی مواجه خواهید شد زیرا SetOrder را فقط در متد Awake SetSortingOrder.cs فراخوانی میکنید.
برای رفع این مشکل، CharacterMovement.cs را باز کنید و [RequireComponent(typeof(Animator))] را با [RequireComponent(typeof(Animator), typeof(SetSortingOrder))] جایگزین کنید.
متغیر زیر را در بالا اعلام کنید :
private SetSortingOrder sortingScript;
سپس این خط را داخل Awake قرار دهید:
sortingScript = GetComponent();
در نهایت، موارد زیر را قبل از بازگشت تهی در داخل MovementRoutine قرار دهید:
sortingScript.SetOrder();
تا زمانی که کاراکتر در حال حرکت است ، به طور موثر SetOrder را فراخوانی می کند.
همه چیز را ذخیره کنید و به صحنه اصلی برگردید.
Play را فشار دهید و شخصیت را به اطراف حرکت دهید.
اکنون همانطور که در نظر گرفته شده است کار خواهد کرد.
توجه : برای اینکه این کار به درستی کار کند، بیشتر این اسپرایت ها محورهای خود را در نزدیکی پایین خود قرار داده اند.
شما می توانید این را با نگاه کردن به آنها در Unity Editor تأیید کنید.
Text Commands
قبل از اینکه شروع به کدنویسی text command parsing برای Wolf’s Quest کنید،
خوب است کمی از تئوری behind پیادهسازی که انجام میدهید یاد بگیرید.
[SerializeField] private float accuracyFactor = 100;
private Camera cam;
private SpriteRenderer sprite;
private float topPoint;
private void Awake()
{
sprite = GetComponent();
cam = Camera.main;
}
Text Parsing و فرم Backus-Naur
Text parsing یک موضوع گسترده است.
برای اهداف شما ، این ایده استخراج اطلاعات مفید از یک رشته معین است.
در بازیهای مبتنی بر متن، parsing شامل گرفتن یک فرمان متنی از بازیکن و استخراج اطلاعاتی است که بازی میتواند از آن استفاده کند در حالی که بقیه را دور میاندازد.
برای انجام این کار باید نوعی گرامر را ایجاد کنید.
این جایی است که فرم Backus-Naur یا BNF به تصویر کشیده می شود.
به زبان ساده، BNF راهی برای نمایش یک سینتکس به صورت نمادین فراهم می کند.
به عنوان مثال، می توانید از آن برای تعیین فرمت آدرس پستی یا توصیف قوانین یک زبان برنامه نویسی استفاده کنید.
در اینجا چند نکته کلیدی در مورد مشخصات BNF وجود دارد :
به صورت زیر نوشته می شود :
< symbol > ::= __ expression __.
علامت ::= به معنای < symbol > در سمت چپ معادل عبارت __ __ در سمت راست است.
یک عبارت از یک یا چند دنباله از نمادها تشکیل شده است.
نوار عمودی | انتخابی مشابه operator بیتی OR را نشان می دهد.
نمادهایی که هرگز در سمت چپ ظاهر نمی شوند، terminals نامیده می شوند.
نمادهای سمت راست که بین جفت <> محصور شده اند، non-terminals نامیده می شوند.
مثال زیر مشخصات BNF را در نظر بگیرید :
< whole number > ::= < digit > | < digit > < whole number >
< digit > ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
یک نماد < digit > می تواند مقادیر صفر تا نه را همانطور که توضیح داده شد، بگیرد. نماد <whole number > می تواند یک < digit > مانند 0 باشد، یا می تواند به صورت بازگشتی با ترکیب < digit > و <whole number > مانند 10 یا 110 نمایش داده شود.
در این مورد، مقادیر 0 تا 9 terminals هستند در حالی که <whole number > و < digit > به طور کلی non-terminals هستند.
علاوه بر قوانین استاندارد، از براکت های مربع در اطراف موارد اختیاری استفاده می شود.
این یک extension شناخته شده جهانی برای BNF است.
برای text parser (تجزیهکننده متنی) که پیادهسازی میکنید، از BNF برای تعیین گرامر به صورت زیر استفاده کنید:
< command > ::= < verb > [< preposition >] < noun > [< preposition > < noun >]< verb > ::= get | look | pick | pull | push
< preposition > ::= to | at | up | into | using
< noun > ::= < article > < entity >
< article > ::= a | an | the
همانطور که می بینید این علامت < entity > را مشخص نمی کند، اما اشکالی ندارد.
شما از این به عنوان پایه استفاده می کنید و موجودیت های داخل Unity را مشخص می کنید.
اکنون زمان نوشتن parser است.
پیاده سازی Command Parsing
Parsing شامل استخراج verb و entities از دستور است.
به اولین entities در مشخصات به عنوان primary entity و آخرین entity به عنوان secondary entity اشاره کنید.
به RW/Scripts بروید و CommandParser.cs را باز کنید.
موارد زیر را در بالای body کلاس CommandParser اما در namespace block قرار دهید :
public struct ParsedCommand
{
public string verb;
public string primaryEntity;
public string secondaryEntity;
}
این ساختار به عنوان container برای داده هایی که می خواهید از command استخراج کنید عمل می کند.
حال ، در داخل body کلاس Command Parser را قرار دهید :
private static readonly string[] Verbs = { "get", "look", "pick", "pull", "push" };
private static readonly string[] Prepositions = { "to", "at", "up", "into", "using" };
private static readonly string[] Articles = { "a", "an", "the" };
این سه متغیر آرایه رشتهای اندازههای ثابتی دارند و با verbs ، prepositions و article هایی که parser شما مطابق با مشخصات BNF شما استفاده میکند، پیکربندی شدهاند.
متد Parse که در ادامه اضافه خواهید کرد از اینها استفاده خواهد کرد.
حالا متد زیر را به کلاس CommandParser اضافه کنید:
//2
public static ParsedCommand Parse(string command)
{
var pCmd = new ParsedCommand();
var words = new Queue(command.ToLowerInvariant().
Split(new[] { ' ' }, System.StringSplitOptions.RemoveEmptyEntries));
try
{
if (Verbs.Contains(words.Peek())) pCmd.verb = words.Dequeue();
if (Prepositions.Contains(words.Peek())) words.Dequeue();
if (Articles.Contains(words.Peek())) words.Dequeue();
pCmd.primaryEntity = words.Dequeue();
while (!Prepositions.Contains(words.Peek()))
pCmd.primaryEntity = $"{pCmd.primaryEntity} {words.Dequeue()}";
words.Dequeue();
if (Articles.Contains(words.Peek())) words.Dequeue();
pCmd.secondaryEntity = words.Dequeue();
while (words.Count > 0)
pCmd.secondaryEntity = $"{pCmd.secondaryEntity} {words.Dequeue()}";
}
catch (System.InvalidOperationException)
{
return pCmd;
}
return pCmd;
}
//1
public static bool Contains(this string[] array, string element)
{
return System.Array.IndexOf(array, element) != -1;
}
یک متد کاملاً chunky به همراه یک متد اکستنشن کمکی وجود دارد.
در اینجا یک breakdown وجود دارد:
متد اکستنشن کمکی Contains for string arrays اگر آرایه رشته داده شده حاوی المان رشته باشد true را برمی گرداند. اگر نه، false برمی گردد.
متد Parse یک دستور رشته را می پذیرد و یک ساختار ParsedCommand را برمی گرداند.
این کار به صورت زیر انجام می شود :
- ابتدا یک متغیر ParsedCommand جدید pCmd برای ذخیره نتایج خود تعریف می کنید.
- سپس ، پس از تنظیم تمام حروف دستور به حروف کوچک ، به کلمات جداگانه تقسیم میشود و در صف کلمات ذخیره میشود، در حالی که مطمئن میشوید هیچ فضای خالی اضافی در دستور در صف گنجانده نشده است.
سپس یکی یکی به کلمه ای که در بالای صف کلمات قرار دارد نگاه می کنید.
اگر کلمه یک verb باشد، در pCmd.verb قرار می گیرد و ذخیره می شود.
اگر کلمه یک حرف اضافه است ، آن را بدون ذخیره کردن در ردیف قرار می دهید زیرا نیازی به استخراج آن ندارید. - در مرحله بعد، article ها را جستجو کرده و دور می اندازید.
سپس کلمات را مرور می کنید، آنها را به pCmd.primaryEntity الحاق می کنید تا زمانی که حرف اضافه دیگری پیدا کنید.
اگر حرف اضافه ای پیدا کردید، همان روند را دنبال می کنید و آن را کنار می گذارید.
سپس به دنبال article دیگری باشید و آن را نیز دور بریزید.
در نهایت، کلمات را در داخل pCmd.secondaryEntity قرار میدهید، الحاق میکنید و ذخیره میکنید تا زمانی که هیچ موردی در صف کلمات باقی نمانده باشد.
- شما از بلوک try-catch برای گرفتن InvalidOperationException استفاده می کنید که اگر عملیات Peek در یک صف خالی انجام شود پرتاب می شود.
این ممکن است اتفاق بیفتد اگر کلمات شما تمام شود. - در نهایت pCmd برگردانده می شود.
هنوز نمی توانید این کد را آزمایش کنید.
GameManager.cs را از RW/Scripts باز کنید و موارد زیر را در کلاس قرار دهید :
public void ExecuteCommand(string command)
{
var parsedCommand = CommandParser.Parse(command);
Debug.Log($"Verb: {parsedCommand.verb}");
Debug.Log($"Primary: {parsedCommand.primaryEntity}");
Debug.Log($"Secondary: {parsedCommand.secondaryEntity}");
}
private void Awake()
{
ExecuteCommand("get to the chopper");
}
در این کد ExecuteCommand را تعریف می کنید که یک دستور رشته را می پذیرد و سپس آن را در داخل Awake فراخوانی می کند.
ExecuteCommand parsedCommand را تعریف می کند و آن را به مقدار ParsedCommand که با ارسال دستور به CommandParser.Parse بازگردانده می شود مقدارده ی اولیه می کند.
در حال حاضر ، ExecuteCommand فقط مقادیر فعل ، primarEntity و secondaryEntity را در کنسول Unity ثبت می کند.
همه چیز را ذخیره کنید ، به ویرایشگر Unity برگردید و یک کامپوننت Game Manager را به Main Camera متصل کنید.
Play را فشار دهید و کنسول را بررسی کنید. خروجی زیر را خواهید دید :
اکنون به GameManager.cs برگردید و ExecuteCommand (“get to the chopper”) را جایگزین کنید.
با ExecuteCommand (“Push the boulder with the Wand”); ذخیره و پلی کنید تا خروجی زیر را دریافت کنید :
شما از موجودیت ثانویه در این آموزش استفاده نخواهید کرد.
اما با خیال راحت از آن برای بهبود پروژه استفاده کنید و بعداً آن را متعلق به خودتان کنید.
در مرحله بعد ، تعاملات را با استفاده از دستورات تجزیه شده پیاده سازی خواهید کرد.
Implementing Interactions
قبل از implement the interactions ، باید رویکرد اتخاذ شده را درک کنید :
دنیای بازی دارای اشیاء قابل تعامل است که هر کدام در وضعیت خاصی هستند.
وقتی به این اشیاء در این حالت تعاملی نگاه می کنید، مقداری پاسخ متنی مرتبط با عمل نگاه دریافت می کنید.
به آن به عنوان گفتگوی نگاه مراجعه کنید.
این حالت می تواند با verb و verbs دیگری همراه باشد.
این verb و verbs با دیالوگی مشابه دیالوگ ظاهر همراه است ، علاوه بر اقدامات مرتبط در بازی که آنها را تحریک می کنند.
همین تعامل را می توان با چند verbs مرتبط کرد.
به عنوان مثال، انتخاب و گرفتن می تواند همین کار را انجام دهد.
اگر یک تعامل مستلزم این است که کاراکتر به شی نزدیک باشد، اگر کاراکتر دور است باید یک پاسخ متنی وجود داشته باشد.
اسمش را بگذارید Dialogue away.
اکنون، خود را برای کدنویسی زیاد آماده کنید.
ابتدا InteractableObject.cs را باز کنید و موارد زیر را در بالای کلاس قرار دهید :
[System.Serializable]
public struct InteractableState
{
public string identifier;
[TextArea] public string lookDialogue;
public Interaction[] worldInteractions;
}
[System.Serializable]
public struct Interaction
{
public string[] verbs;
[TextArea] public string dialogue;
[TextArea] public string awayDialogue;
public UnityEngine.Events.UnityEvent actions;
}
این کد InteractableState و Interaction را تعریف می کند که نشان دهنده رویکردی است که قبلا دیدید.
متغیر کنشها هنگام فراخوانی ، هر کنش درون بازی را فعال میکند.
متغیر شناسه InteractableState را ذخیره می کند تا بتوانید با استفاده از فرهنگ لغت، states را نقشه برداری کنید.
اکنون ، زیر کدی که اضافه کردید، در کلاس InteractableObject قرار دهید:
[SerializeField] private float awayMinDistance = 1f;
[SerializeField] private string currentStateKey = "default";
[SerializeField] private InteractableState[] states = null;
[SerializeField] private bool isAvailable = true;
private Dictionary stateDict =
new Dictionary();
public string LookDialogue => stateDict[currentStateKey].lookDialogue;
public bool IsAvailable { get => isAvailable; set => isAvailable = value; }
public void ChangeState(string newStateId)
{
currentStateKey = newStateId;
}
public string ExecuteAction(string verb)
{
return ExecuteActionOnState(stateDict[currentStateKey].worldInteractions, verb);
}
private void Awake()
{
foreach (var state in states)
{
stateDict.Add(state.identifier.Trim(), state);
}
}
private string ExecuteActionOnState(Interaction[] stateInteractions, string verb)
{
foreach (var interaction in stateInteractions)
{
if (Array.IndexOf(interaction.verbs, verb) != -1)
{
if (interaction.awayDialogue != string.Empty
&& Vector2.Distance(
GameObject.FindGameObjectWithTag("Player").transform.position,
transform.position) >= awayMinDistance)
{
return interaction.awayDialogue;
}
else
{
interaction.actions?.Invoke();
return interaction.dialogue;
}
}
}
return "You can't do that.";
}
این یک بار براکتی از متد هایی است که شما به تازگی اضافه کرده اید.
در اینجا یک تفکیک کد آمده است :
بولین isAvailable یک flag ساده برای تعیین اینکه آیا شی برای هر نوع تعامل در دسترس است یا خیر است.
Awake دیکشنری stateDict را با mapping در InteractableState روی خودش پر می کند.
این برای همه اعضای states انجام می شود و به بازیابی سریع کمک می کند.
ChangeState یک متد کمکی عمومی برای به روز رسانی مقدار currentStateKey است.
ExecuteAction یک verb را می پذیرد و پس از بازیابی همان verb از stateDict با استفاده از currentStateKey ، آن را به ExecuteActionOnState همراه با worldInteractions وضعیت فعلی منتقل می کند.
ExecuteActionOnState تعامل با verb مرتبط را پیدا می کند.
اگر چنین تعاملی وجود نداشته باشد، پاسخ پیشفرض «شما نمیتوانید این کار را انجام دهید» را برمیگرداند.
اما اگر وجود داشته باشد، interaction.dialogue را برمی گرداند.
اگر awayDialogue برای تعامل خالی نباشد و فاصله بین کاراکتر و شیء قابل تعامل بزرگتر یا مساوی awayMinDistance باشد، interaction.awayDialogue را برمی گرداند.
همه چیز را ذخیره کنید و به GameManager.cs برگردید.
تغییرات نام چندگانه را برای یک شیء قابل تعامل برای اطمینان از تجربه بازیکن خوب مرتبط کنید.
این استاکچر را در بالای body کلاس GameManager قرار دهید :
[System.Serializable]
public struct InteractableObjectLink
{
public string[] names;
public InteractableObject interactableObject;
}
کلمات موجود در داخل نام ها با interactableObject مرتبط هستند.
اکنون تمام کدهای داخل کلاس GameManager را با جایگذین کنید :
[SerializeField] private InteractableObjectLink[] objectArray = null;
private UIManager uiManager;
private Dictionary sceneDictionary;
public void ExecuteCommand(string command)
{
var parsedCommand = CommandParser.Parse(command);
//1
if (string.IsNullOrEmpty(parsedCommand.verb))
{
uiManager.ShowPopup("Enter a valid command.");
return;
}
if (string.IsNullOrEmpty(parsedCommand.primaryEntity))
{
uiManager.ShowPopup("You need to be more specific.");
return;
}
if (sceneDictionary.ContainsKey(parsedCommand.primaryEntity))
{
//3
var sceneObject = sceneDictionary[parsedCommand.primaryEntity];
if (sceneObject.IsAvailable)
{
if (parsedCommand.verb == "look") uiManager.ShowPopup(sceneObject.LookDialogue);
else uiManager.ShowPopup(sceneObject.ExecuteAction(parsedCommand.verb));
}
else
{
uiManager.ShowPopup("You can't do that - atleast not now.");
}
}
else
{
//2
uiManager.ShowPopup($"I don't understand '{parsedCommand.primaryEntity}'.");
}
}
private void Awake()
{
uiManager = GameManager.FindObjectOfType();
sceneDictionary = new Dictionary();
foreach (var item in objectArray)
{
foreach (var name in item.names)
{
sceneDictionary.Add(name.ToLowerInvariant().Trim(), item.interactableObject);
}
}
}
این کد تعاریف Awake و ExecuteCommand را از قبل به روز می کند و چند متغیر نمونه اضافه می کند.
Awake با تکرار از طریق objectArray و نگاشت interactableObject هر عضو به نام خود، دیکشنری صحنه را پر می کند.
همچنین uiManager را به UIManager در صحنه مقداردهی اولیه می کند.
شما نیازی به دانستن عملکرد داخلی UImanager ندارید.
برای این آموزش بدانید که مقادیر ShowPopup است که یک رشته را می پذیرد و آن رشته را در پوشش رابط کاربری که در ابتدا مشاهده کردید نشان می دهد.
ExecuteCommand ابتدا دستور را تجزیه می کند سپس به صورت زیر عمل می کند :
- اگر parsedCommand یک verb خالی یا یک PrimaryEntity خالی داشته باشد، ShowPopup را با مقداری متن موجود که همان را به پلیر نشان می دهد، فراخوانی می کند.
- اگر PrimaryEntity خالی نباشد، اما به عنوان key در داخل sceneDictionary وجود نداشته باشد، ShowPopup را با متنی فراخوانی میکند که به پلیر میگوید بازی آن کلمه را نمیفهمد.
- اگر InteractableObject با موفقیت از دیکشنری صحنه بازیابی شود اما در دسترس نباشد، ShowPopup با متنی که همان را منتقل می کند فراخوانی می شود.
اگر در دسترس باشد، موارد زیر رخ می دهد : - اگر verb (فعل) “look” باشد ، ShowPopup با LookDialogue فراخوانی می شود.
اگر verb (فعل) “look” نباشد، ExecuteAction از قبل با ارسال parsedCommand.verb به آن فراخوانی می شود.
مقدار رشته برگشتی به ShowPopup ارسال می شود.
Scene Interaction Setup
ابتدا، Object Interactable را به GameObject های زیر متصل کنید :
- Apples
- Bucket
- Rope
سپس InputField را انتخاب کنید و GameManager.ExecuteCommand را به subscriber list رویداد On End Edit آن مانند شکل زیر اضافه کنید:
این تضمین می کند که GameManager.ExecuteCommand زمانی فراخوانی می شود که پلیر پس از تایپ دستور، کلید Return را فشار دهد.
حالا Main Camera را انتخاب کنید.
اندازه Object Array Game Manager را روی ۳ قرار دهید.
شما باید هر المان از فیلد Object Array’s Interactable Object را به ترتیب روی Apples، Bucket و Rope تنظیم کنید.
سپس هر کدام باید مجموعه آرایه نامهای خود را با تغییراتی از آنچه میتوان نامید، داشته باشد.
به عنوان مثال، سیب ها را می توان به عنوان “Apple” یا “Apples” نامید.
همانطور که در تصویر زیر نشان داده شده است، باید به پیکربندی برسید :
سپس Bucket را انتخاب کنید و ویژگی های Object قابل تعامل آن را به صورت زیر تنظیم کنید:
Size of States را روی ۱ تنظیم کنید.
شناسه را به پیشفرض تغییر دهید و به رشته Dialogue نگاه کنید.
Change Is Available to false (یعنی بدون علامت).
در اینجا چگونه باید به نظر برسد :
سپس سیب ها را انتخاب کنید و ویژگی های Object Interactable آن را به صورت زیر تنظیم کنید :
حداقل فاصله را روی ۴ تنظیم کنید.
اندازه حالت ها را به ۱ تغییر می دهد.
در قسمت States، شناسه را روی پیشفرض و Look Dialogue را روی رشته «The ground near the tree is covered with lots of apples» تنظیم کنید.
اندازه World Interactions را به ۱ تغییر دهید.
Size of Verbs را در World Interactions روی ۲ قرار دهید.
ورب های pick and get را برای دو المان ورب وارد کنید.
اکنون Dialogue و Away Dialogue را روی «You picked up a delicious apple» تنظیم کنید.
و “I need to get near the apples first” به ترتیب.
توجه داشته باشید که چگونه از get and pick برای همان تعامل استفاده می کنید.
همچنین، به استفاده از Away Min Distance برای پیکربندی تفاوت بین پاسخ گفت و گوی معمولی و پاسخ گفتگوی دور توجه کنید.
در نهایت، طناب را انتخاب کنید و ویژگی های Object Interactable آن را تنظیم کنید تا به داخل واکنش نشان دهد و شناسه ها را بکشد.
تنظیمات این کامپوننت Interactable Object مشابه نحوه پیکربندی سیب ها است، با این تفاوت که در این مورد ۵ اکشن نیز اضافه خواهید کرد.
کامپوننت Rope Interactable Object باید به شکل زیر پیکربندی شود :
توجه داشته باشید که بعد از کشیدن باید حالت مربوط به طناب را تغییر دهید.
همچنین توجه کنید که چگونه Bucket برای تعاملات پس از تعامل کششی در دسترس قرار می گیرد.
همه چیز را ذخیره کنید و پلی کنید.
دستورات را در داخل فیلد ورودی تایپ کرده و آنها را تست کنید. برای تنظیمات موجود سعی کنید :
به طناب نگاه کن
طناب را بکش
به سیب ها نگاه کن
یک سیب بردارید
به سطل نگاه کن
شما همچنین می توانید برخی از تغییرات را در این موارد امتحان کنید، مانند :
- به طناب نگاه کن (look at rope)
طناب بکش (pull rope)
یک سیب بگیر (get an apple)
سیب ها را بگیرید (get the apples)
به سطل بزرگ نگاه کن (look at the large bucket)
مهرسا امینی
برنامه نویس ، انیماتور ، سئوکار
زندگي در حين گرفتن چيزهايي از تو موقعيت هاي جديدي را مي بخشد