بررسی هوش مصنوعی و کامپایل اسکریپت در udk

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

نکته دیگری هم قابل ذکر است که روشها و اطلاعات مطرح شده در این مطالب صرفا برای بازی سازی خواهد بود و ممکن است در بعضی مسائل با هوش مصنوعی اکادمیک متفاوت باشد ( که البته بندرت این اتفاق می افتد ) . در صورتیکه نیاز بود دلیل این موضوع را توضیح خواهم داد.

روشهای متداول پیاده سازی هوش مصنوعی:

1- روش FSM: که معمولا حتی در بازیهای نه چندان پیشرفته امروزی از آن استفاده می شود. این روش برای State های کم بسیار خوب جواب خواهد داد ولی برای شماره وضعیت های بیشتر با پیچیدگی فراوانی همراه است.
2- روش HFSM: که همان FSM است اما می تواند در تعداد وضعیت های بالاتر پایایی خود را بیشتر حفظ کند همچنین بدلیل مجتمع سازی Action ها می تواند پیچیدگی طراحی آنرا کاهش دهد.

با ترکیب FSM یا HFSM با Decision Making ، اکشن ها قابلیت اجرای تصادفی را هم خواهند داشت و در این شرایط پیش بینی هوش سخت تر خواهد بود و بیشتر به هوش مصنوعی انسان نزدیک خواهد شد.

3- Planner: برای برطرف کردن مشکل دو یا سه روش فوق که با افزایش Action ها و Flag های محیط طراحی آن بسیار پیچیده می شود روش دیگری توسط Jeff Orkin که برنامه نویس هوش مصنوعی بازیهای No One Lives Forever و F.E.A.R است ابداء شد و آن هم Planner بود.
خود Planner الگوریتم های تعمیمی فراوانی دارد که فرصت بررسی آنها را نخواهیم داشت. در این قسمت فقط Planner را در ساده ترین حالتش مورد بررسی قرار می دهیم.

چند مفهوم اساسی برای Planner را تعریف خواهم کرد.

World State یا به عبارت ساده تر State : یک State یک Array از متغیرهایی است که می تواند بر روی هوش مصنوعی اثر بگذارد. بطور مثال، تعداد تیر، نوع تفنگ، داغ شدن تفنگ، وجود تفنگ بر روی زمین، نوع کمین حریف، وجود کاور، خالی بودن کاور، میزان انرژی، دید یار به دشمن، دید دشمن به یار و ... . به عبارتی هر چه تعداد این متغیر ها بیشتر باشد با State های بیشتری روبرو خواهیم بود. اگر تعداد وضعیت متغیر i ام را با Vi و تعداد متغیر ها را با n نمایش دهیم، در نهایت V1*V2*…*Vn وضعیت خواهیم داشت.

لایه های هوش مصنوعی: هوش مصنوعی در هر روشی به سه لایه استوار است.
1- انیمیشن Animation
2- حرکت Movement
3- مبارزه Combat

وظیفه لایه انیمیشن به قرار زیر است:
1- اجرای انیمیشنهای اصلی ( که معمولا Full Body ) هستند و با اتمام آنها اتفاقی نخواهد افتاد، نظیر دویدن
2- اجرای انیمیشنهای تزئینی ( که معمولا Full Body ) هستند و با اتمام آنها اتفاقی نخواهد افتاد، نظیر انیمیشنهای بیکاری
3- اجرای انیمیشنهای سینک و Blend که معمولا به قصد شبیه سازی دقیقتر هوش اجرا می شوند و جنبه نمایشی دارند.
4- اجرای انیمیشنهای وقفه ای که باعث وقوع اتفاق خاصی و یا تغییر state –ی می شوند.
5- اجرای انیمیشنهای مرتبط با Smart Object ها که باعث وقوع اتفاق خاصی و یا تغییر state –ی می شوند.

از آنجایی که گزینه های 1، 2، 3 بر روی World State اثری نمی گذارند دیباگ کردن آنها ساده تر است اما در مورد دو مدل 4 و 5 که باعث تغییر state ها خواهند شد مشکلات بسیاری بوجود می آید. که در آینده آنرا بررسی خواهیم کرد.

وظیفه لایه حرکت ( Movement ) به قرار زیر است:
- شناسایی مسیر های قابل حرکت با استفاده از روشهای Path Finding
- انتخاب کوتاهترین و یا امنترین و یا راحتترین و یا کم شیب ترین و یا بی صدا ترین مسیر بر اساس اطلاعات مسیر
- حرکت بر روی مسیر و بروز رسانی دو مرحله پیش در وقفه های زمانی یا مکانی خاص
- حرکت بر روی مسیر های از پیش تعیین شده

در تمامی حالات فوق State های محیط تغییر می کنند و عمومی ترین متغیر های تغییر پیدا کرده آنهایی هستند که به فاصله مربوط می شوند. بنابراین از این لایه باگهای زیادی بوجود خواهد آمد.

وظیفه لایه مبارزه ( Combat ) به قرار زیر است:
- انتخاب بی دردسر ترین یا مهمترین دشمن برای مرگ
- انتخاب مناسبترین و یا در دسترس ترین اسلحه برای تجهیز
- انتخاب بهترین محل برای پنهان شدن و ادامه مبارزه
- انتخاب بهترین و یا بی خطر ترین استراتژی برای حذف دشمن یا فرار از او
- و بسیاری دیگر که قابل ذکر نیست.

آنجایی که از دو لایه قبل با مشکلات بیشتری مواجه خواهیم شد این لایه است. در این شرایط است که نیاز ما فقط به یک سیستم کارامد انیمیشن و Path Finding بستگی ندارد بلکه به هسته برنامه نویسی و پوسته آن بستگی دارد. در این شرایط است که به تعداد غیر قابل شمارشی فانکشنها ( بدون از دست رفتن World State ) نیاز به تغییر و دیباگ دارند.

تا اینجا توضیحات کلی در مورد هوش و لایه های مربوط به آن بود. از اینجا به بعد باید به روش ترکیب این سه لایه بطور خلاصه بسنده کنیم. چون توضیح آن عملا غیر ممکن است و نیاز به هزاران خط کد دارد. کلیت ترکیب سه لایه فوق به صورت زیر خواهد بود.

ترکیب لایه های هوش مصنوعی:

1- ابتدا باید برای هر NPC پارامترها و خواصی تعریف کنیم تا اطلاعات مورد نیاز خود از محیط را دریافت کرده و ذخیره کنند. این کار به آن راحتی که گفتم نیست بلکه بسیاری از این اطلاعات نیاز به پردازش های سنگین دارند. بطور مثال می خواهیم بدانیم که آیا NPC به دشمن دید دارد یا خیر. در این شرایط باید از زوایای دید و Ray ها استفاده کنیم. در صورتیکه تعداد NPC بالا باشد مثلا 20 تا 25 نفر، و در هر 1 ثانیه نیاز به Update این اطلاعات باشد حجم بسیار زیادی Ray و Collision Detection رخ خواهد داد. این فقط یکی از پارامترهایی است که می تواند از محیط دریافت و در NPC ذخیره شود، مطمئن باشید به بیش از 10 نوع از این پارامترها نیاز خواهید داشت. البته بسیاری از اطلاعات نیازی به ذخیره شدن ندارند و در لحظه ایجاد قابل بررسی خواهند بود. مثال بارز آن Collision Detection Event ها هستند.

2- پس از ذخیره و بررسی تمامی World State های مورد نیاز، باید آنها را به سیستم بسیار پیچیده A* جهت بررسی بفرستیم و کم هزینه ترین مسیر برای رسیدن به هدف را به دست آوریم. این هدف می تواند « حذف دشمن » ، « فرار از دشمن » ، « پیدا کردن مهمات توسط NPC » ، « غافلگیر کردن دشمن» ، « پنهان شدن و فرار از مرگ » و ... باشد. بر اساس هر هدف سیستم جستجو در A* پیچیده و پیچیده تر خواهد شد تا جایی که ردیابی این مسیر تا رسیدن به بهترین جواب می تواند یک Log File حدود 1 مگابایتی برای شما ایجاد کند. مگر آنکه مرحله به مرحله در صورت وقوع باگ آنرا تغییر داده و با ایجاد فانکشنهای ردیاب که بر اساس نیاز شما قابل تغییرند آنها را دیباگ کنید.

برای تشریح بیشتر این حرفها باید مطلب زیر را هم توضیح بدهم.
توجه داشته باشید که A* یک روش برای پیدا کردن کوتاهترین مسیر است. اما در هوش مصنوعی یک NPC، چه نقشی را ایفا خواهد کرد. اگر A* برای مسیر یابی را کار کرده باشید و ماهیت آنرا بشناسید، میدانید که در مسیر یابی Node های یک A* ورتکس های گرید Navmesh هستند و خطوط بین Node ها ، مسیر های قابل عبور از Navmesh هستند. بنابراین با Search به روش A* به کوتاهترین مسیر خواهیم رسید ( برای راحتترین باید وزن دهی بر روی Node ها قرار گیرد. ). حالا از این الگوریتم چگونه برای بهترین ( کوتاهترین ) اکشن استفاده می کنیم. جواب در زیر است:

فرض کنید پر کردن یک اسلحه وزن معادل 5 داشته باشد و تعویض اسلحه به کلت وزنی معادل 2 داشته باشد. همینطور دشمن روبروی ماست و خطر مرگ جدی ما را تحدید می کند. از طرفی یک کاور کنار ماست که ورود به آن وزنی معادل 2 دارد و با ورود به آن به اندازه پر کردن یک اسلحه از شر دشمن خلاص می شویم. در همین شرایط کلت نمی تواند با توجه به سلامتی دشمن او را بکشد و او هم اسلحه یوزی در دست دارد. سخت شد؟ همه اینها می شوند یک State . در اینجا State ها Node های شما در A* و Action ها خطوط بین آنها خواهند بود. وظیفه A* در این قسمت انتخاب کوتاهترین ( کم وزن ترین ) سلسله عملیاتی است که به نابودی دشمن منجر می شود. اما می دانیم که در این شرایط دشمن ما را خواهد کشت. پس به همین شکل باید برای هدفهای فرار و ... جستجو کنیم. در نهایت هدف و سلسله عملیات توسط جستجوهای پی در پی با استفاده از A* برای یک لحظه تصمیم NPC انتخاب می شوند. ( مسلما بسیار پیچیده و پیگیری آن غیر ممکن به نظر می رسد. ) با هر ابزاری که آنرا دیباگ کنید پیچیده است، حتی با استفاده از فانکشنهای قابل Update بدون از دست رفتن State ها و اما آیا با روش دوم می توان آنرا دیباگ کرد؟ ( منظور بستن بازی و اجرای مجدد است. ) . شما دیگر نخواهید توانست چنین شرایط استثنایی را Reproduce کنید و یکی از مهمترین عوامل در شناسایی و برطرف کردن باگ Reproduce کردن ( دوباره تولید کردن ) آن باگ است. عده ای جواب این قسمت را اینگونه می دهند که بازی را Save می کنیم ، می بندیم، فانکشنها را تغییر می دهیم، مرحله را مجدد بارگذاری می کنیم و بعد تست می کنیم که آیا درست شده است یا نه. این جواب منطقی است اما خودتان تصور کنید چقدر طول خواهد کشید تا این چرخه را تا مرحله پاکسازی باگ تکرار کنید. در صورتیکه بدون بستن بازی و آپدیت کردن آن سرعت این دیباگ تا 100 ها برابر افزایش خواهد یافت.

3- اطلاعات مطرح شده در بند 2 به همین جا ختم نمی شود، یکی از مراحل رسیدن به Planner تا اجرای مناسب ، بالانس کردن اصطلاحا Effect ها و Flag ها و وزنهاست. فرض کنیم وزن برای همه یکسان باشد و مساوی عدد 1. به مبحث Effect و Flag خواهیم رسید. برای اینکه وضعیت را به بدتر از این نکشانم فرض کنیم که Flag ها همان Variable هایی هستند که World State را تغییر می دهند ( در صورتیکه نیستند و می توانند عبارت جبری از آنها باشند، که این عبارت جبری هم نیاز به تعریف و دیباگ دارد. ) بنابراین Flag ها را هم بررسی نمی کنم. میرسیم به Effect ها. هر Action که می تواند، « پر کردن اسلحه »، « پریدن »، « دویدن » ، « شلیک » و بسیاری دیگر باشد بر روی یک Variable اثر می گذارد که یا بر اساس وقفه زمانی یا وقفه انیمیشن این کار صورت می گیرد. بطور مثال پس از اتمام انیمیشن شلیک یکی از تعداد تیر ها کم می شود ( می تواند تغییر وضعیت در تعداد تیر را ایجاد کند) ، این تغییر را Effect آن Action می گویند.

بنابراین کار دیگر پر هزینه ، بالانس کردن Effect ها ، Flag ها و Weight ها می باشد، این کار فقط و فقط با یک روش میسر است. ایجاد یک Array و نمایش آن در یک ادیتور کاملا اختصاصی به این منظور با امکان بروز رسانی بلادرنگ و نمایش وضعیت های کنونی سه مفهوم بالا.

4- هر چه جلوتر برویم کار پیچیده تر خواهد شد، چون هوش مصنوعی از ترکیب غیر مجزائی از لایه های مطرح شده بوجود می آیند. می رسیم به وقفه های تولید شده توسط Animation که در لایه انیمیشن مورد بررسی قرار گرفت. مفهوم دیگری در اینجا مطرح می شود به نام Animation State که می تواند به سه لایه جدید Master, Slave, Blend تعمیم داده شود. به طور مثال Aim کردن در حین دویدن ، دو انیمیشن پایین تنه و بالاتنه را در لایه های Master, Slave قرار می دهد و با استفاده از لایه Blend که فقط در سرشانه ها و سر است Aim صورت می گیرد. در این حین نیاز به شلیک وجود دارد و شلیک هم انیمیشن مجزایی دارد، اگر این انیمیشن ها به درستی با هم ترکیب نشوند نتایج بدی مانند سرخوردن کاراکتر، چرخیدن کمر بی دلیل، عدم نشانه روی صحیح، انجام سلسله انیمیشن های متوالی بدلیل قرار گرفتن در یک State Loop و ... پدید می آید. اینجاست که سیستم بسیار قوی در Unreal Engine که به UDK هم سرایت کرده می تواند بسیاری از این Animation State ها را رفع و رجوع کند، اما تمام قضیه به اینجا ختم نمی شود و در Event های اتمام یا تریگر این انیمیشن ها نیاز به برنامه نویسی و دیباگ برای اتصال به سایر قسمتهای هوش وجود خواهد داشت.

5- نوبت به لایه Movement می رسد. در اینجا فرصت بحث در مورد نواقص و مشکلات مطرح شده در مورد Path Finding نخواهد بود و من فرض می کنم که این قسمت بدون مشکل ایجاد شود. در زمانهای متوالی بدون وقفه نیاز به Update شدن State ها وجود خواهد داشت. با رسیدن به مسیر هم نیاز به Update مجددا به چشم می خورد. بسیاری از باگها بین حرکت بین دو مسیر پیش می آیند. در این شرایط نیاز است که بارها NPC این مسیر را تکرار کند و ما او را به صورت فایل یا به صورت چشمی Log کنیم تا مشکلش را بفهمیم. شاید بتوان گفت این قسمت از سایر قسمتها ساده تر خواهد بود ولی باز هم آنرا ساده نگیرید.

6- همه اینها به حد مطلوب رسیدند ( پس از زمانی بالغ بر 3 تا 8 ماه کار )، بازی دارد برای Release آماده می شود و حجم انبوهی از تسترها از شما ایراد می گیرند. هم اکنون شما با این قضیه رو برو هستید:

یک کلاف با 100 سر گم و ناپیدا، که به هم پیچیده و در مکانهایی نامعلوم به هم گره خورده اند. رد کردن یک سر از لای دو سر کلاف می تواند باعث باز شدن یک گره شود و یا گره دیگری ایجاد کند ( قانون بقای باگ نام گرفته است ). گاهی شما فکر می کنید که با این حرکت گره را باز کردید اما بعد از مدتی متوجه می شوید باعث سه گره جدید در ابتدای طناب شدید و باید همه مسیر را به عقب برگردید. این چیزی که گفتم وضعیت کسی بود که بدون بستن بازی می تواند کدش را تغییر دهد و آنرا دیباگ کند. اما وضعیت نفر دوم که نمی تواند کدش را آپدیت کند و باید بازی را ببندد به همان شکل بالاست با این تفاوت که هر بار که شما سر کلاف را از لای دو سر دیگر رد می کنید کسی کل آنرا از شما می گیرد، می چرخاند و بعد به شما تحویل می دهد. در این شرایط است که نفر اول گره های این طناب را پس از 3 ماه دیباگ باز می کند و نفر دوم پس از 8 ماه هم نمی تواند آنرا باز کند.

راه حلی که عده دوم بدست می آورند چیست؟ یا از اول این مشکل را می دانند و تعداد سر کلاف را با کم کردن Feature های بازی از 100 به 10 می رسانند و یا در انتها، یک بازی پر از ایراد تحویل می دهند.

اما همه بدبختیها به اینجا ختم نمی شود. سرعت اجرای اسکریپت در UDK هر 20 تیک انجین یکبار انجام می شود و این یعنی سرعت شما 1/20 سرعت برنامه نوشته شده در C++ است. در این شرایط شما که به Source Code دسترسی ندارید مجبورید هسته مرکزی هوش را که A* ، سیستم های Ray و جمع آوری داده از محیط و بسیاری دیگر است را باید در اسکریپت بنویسید و اجرا کنید. این چنین می شود که بازی با UDK با تعداد NPC کم می خوابد ولی بازی با Unreal به خوبی و درست کار می کند.