بهترین بازی‌های روگ‌لایک (roguelike) فقط شما را نمی‌کشند؛ بلکه باعث می‌شوند که خودتان بخواهید بمیرید.

شاید عجیب به نظر برسد، اما هر کسی که یک دور بازی را در نیمه‌شب باخته و بلافاصله در ساعت ۱۲:۰۳ دور دیگری را شروع کرده باشد، این حس را می‌شناسد. مرگ دردناک است. نوار سلامتی شما به صفر می‌رسد. صفحه نمایش غرق در شکست می‌شود. با این حال، انگشت شما از قبل روی دکمه play قرار گرفته است. چیزی در بیست دقیقه گذشته آن‌قدر اهمیت داشته که نمی‌توانید بازی را همان‌جایی که تمام شده رها کنید.

این همان چرخه «فقط یک دور دیگر» است و تصادفی نیست؛ بلکه طراحی شده است. اگر در حال ساخت یک بازی روگ‌لایک یا هر بازی دیگری با ویژگی مرگ دائمی (permadeath) هستید، تمام وظیفه شما ایجاد تعادل بین دو نیروی متضاد است. بازیکن باید به اندازه کافی شکست بخورد تا تنش را حس کند، و باید به اندازه کافی پیشرفت داشته باشد تا احساس امیدواری کند.

ارزِ شکست

در آخرین پروژه‌ام، Neon Survivor، دقیقاً همین تنش را می‌خواستم. وقتی بازیکن می‌میرد، همه چیزهایی که در طول آن دور جمع‌آوری شده از بین می‌رود، به جز یک چیز: طلا. آن طلا به‌طور خودکار ذخیره می‌شود. در منوی اصلی، بازیکن می‌تواند آن را صرف ارتقاهای دائمی کند. سپس دوباره وارد بازی می‌شود، در حالی که کمی قوی‌تر از قبل است.

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

ترفند ساده است: حتی وقتی همه چیز را از دست می‌دهید، چیزی را نگه می‌دارید. دشواری کار در این است که مطمئن شوید آن چیزی که نگه می‌دارید، بدون از بین بردن چالش بازی، اهمیت داشته باشد. اگر ارتقاها خیلی ضعیف باشند، بازیکن دیگر اهمیتی نمی‌دهد. اگر خیلی قوی باشند، بازی خودش به تنهایی انجام می‌شود. در هر دو حالت، چرخه از هم می‌پاشد.

دو ساعت، بدون سردرگمی

برای ساخت صحیح این سیستم، مجبور شدم دو خط زمانی مجزا را مدیریت کنم.

ساعتِ دور بازی (Run Clock) هر بار که دکمه play را می‌زنید، بازنشانی می‌شود. این ساعت، سلامتی، امتیاز فعلی، تعداد موج‌های دشمن و هرگونه قدرت‌های موقتی که در طول جلسه به دست آمده را ردیابی می‌کند. وقتی شخصیت می‌میرد، این ساعت به صفر برمی‌گردد.

ساعتِ متا (Meta Clock) هرگز بازنشانی نمی‌شود. این ساعت، کل طلای به‌دست‌آمده در تمام تلاش‌ها، بالاترین موجی که تا به حال رسیده‌اید و تمام ارتقاهای دائمی خریداری‌شده را نگه می‌دارد. این ساعت بدون توجه به اینکه صفحه مرورگر چند بار رفرش شود، به تیک‌تیک کردن ادامه می‌دهد.

ترکیب این دو با هم، باگ‌هایی ایجاد می‌کند که ردیابی آن‌ها سخت و رفع کردنشان دردناک است. من توسعه‌دهندگانی را دیده‌دهام که به‌طور تصادفی پیشرفت بازیکن را در حین یک بازنشانی روتینِ صحنه (scene reset) پاک کرده‌اند، چون یک تابع پاک‌سازی (cleanup function) به ذخیره‌ساز داده‌ای اشتباه دسترسی پیدا کرده است. داده‌های Meta Clock از بین می‌روند. بازیکن با صفر طلا و صفر ارتقا بازمی‌گردد. در آن لحظه، رابطه بین شما و بازیکنتان از هم می‌پاشد. آن‌ها یک دور جدید را شروع نمی‌کنند؛ آن‌ها یک کینه جدید را شروع می‌کنند.

جدا نگه داشتن این ساعت‌ها فقط یک انتخاب سبکی نیست؛ بلکه یک استراتژی برای بقاست.

نحوه مدیریت این تفکیک در Phaser v4

من Neon Survivor را با Phaser v4 ساختم که دو ابزار خاص برای این مشکل ارائه می‌دهد.

Registry داده‌های زنده را در حافظه برای جلسه فعلی نگه می‌دارد. سریع است. ساده است. اما به محض اینکه بازیکن صفحه را رفرش کند، از بین می‌رود.

LocalStorage داده‌ها را در خودِ مرورگر ذخیره می‌کند. در برابر بستن تب، راه‌اندازی مجدد مرورگر و قطع برق دوام می‌آورد. همچنین کندتر و کم‌اعتمادتر است. مرورگرها می‌توانند آن را مسدود کنند، محدود کنند یا اگر سهمیه ذخیره‌سازی پر شود، آن را پاک کنند.

انتخاب طراحی من سخت‌گیرانه بود. Registry تنها منبع حقیقت (source of truth) در طول گیم‌پلی است. بازی از آن می‌خواند، در آن می‌نویسد و کاملاً به آن اعتماد می‌کند. LocalStorage نقش نویسنده مشترک را ندارد؛ بلکه نقش یک آینه را ایفا می‌کند.

روال کار به این صورت است: بازی خرید یک ارتقا را در Registry می‌نویسد. یک کلاس مدیریت‌کننده (manager class) واحد، Registry را زیر نظر دارد. در زمان مناسب، آن مدیر داده‌های Registry را در LocalStorage منعکس (mirror) می‌کند. اگر مرورگر اجازه نوشتن ندهد، بازی دچار وقفه نمی‌شود. اگر ذخیره‌سازی با شکست مواجه شود، جلسه فعلی همچنان به‌درستی اجرا می‌شود. بازیکن ممکن است تنها در صورتی پیشرفت خود را از دست بدهد که دقیقاً در همان ثانیه تب را ببندد، اما خودِ جلسه هرگز کرش نمی‌کند.

این الگو از یک فاجعه نامحسوس جلوگیری می‌کند. اگر اجازه دهید هر سیستمی مستقیماً در LocalStorage بنویسد، وابستگی‌هایی به یک API شکننده ایجاد می‌کنید. بازیکنی که تنظیمات حریم خصوصی او بالا باشد یا دستگاهی با حافظه کم داشته باشد، ممکن است شاهد کند شدن یا فریز شدن بازی در حین مبارزه باشد، زیرا یک تابع پس‌زمینه سعی کرده است آمار را ذخیره کند. با تبدیل کردن Registry به تنها منبع حقیقت، سرعت اکشن را بالا نگه می‌دارید و ریسک را محدود می‌کنید.

اجازه دهید کد نفس بکشد

من همچنین از رویدادها (events) برای جداسازی سیستم‌ها (decouple) استفاده کردم. وقتی یک دور بازی تمام می‌شود، GameScene مراسم تدفین خودش را برگزار نمی‌کند. تابع ذخیره‌سازی را فراخوانی نمی‌کند. از یک ابزار ذخیره‌سازی وارد (import) نمی‌کند. بلکه صرفاً یک رویداد "run-ended" را همراه با داده‌های مربوطه منتشر (emit) می‌کند.

یک شنونده (listener) مجزا مسئولیت مدیریت داده‌ها را بر عهده دارد. این شنونده رویداد را دریافت می‌کند، Meta Clock را به‌روزرسانی می‌کند و به مدیر (manager) دستور می‌دهد تا مجموع‌های جدید را در LocalStorage منعکس کند.

این تفکیک بلافاصله نتیجه می‌دهد. من می‌توانم کل GameScene را بازنویسی کنم، شخصیت بازیکن را عوض کنم، زاویه دوربین را تغییر دهم، یا حتی سبک بازی را از survival به bullet hell تغییر دهم، بدون اینکه به سیستم ذخیره‌سازی دست بزنم. سیستم‌ها مستقل هستند. آن‌ها از طریق رویدادها با هم ارتباط برقرار می‌کنند، نه از طریق فراخوانی مستقیم توابع. این یعنی تداخل‌های ادغام (merge conflicts) کمتر، باگ‌های کمتر و کد‌بیسی که بعد از شش ماه به کد اسپاگتی تبدیل نمی‌شود.

ارتقاهایی که بازی را تغییر می‌دهند

داشتن یک ستون فقرات فنی اگر پاداش‌ها شبیه به یک صفحه گسترده (spreadsheet) باشند، بی‌فایده است. من زمان زیادی را صرف این کردم که ارتقاها در حین بازی واقعاً چه حسی داشته باشند.

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

ارتقاهای دیگر، قوانین را کاملاً بازنویسی می‌کنند. در Neon Survivor، من قابلیت "Piercing Rounds" را اضافه کردم. قبل از این ارتقا، گلوله با برخورد به اولین دشمن متوقف می‌شد. بعد از ارتقا، گلوله از میان دشمنان عبور می‌کند و پتانسیل این را دارد که با یک شلیک، کل یک ردیف را پاکسازی کند.

تفاوت چشمگیر است. سرعت و سلامتی ممکن است به شما اجازه دهند بیشتر زنده بمانید، اما Piercing Rounds نحوه موقعیت‌گیری شما را تغییر می‌دهد. شما شروع به ردیف کردن دشمنان می‌کنید. دیگر به جای کایت کردن (kiting) در لبه‌ها، شروع به نفوذ از میان مرکز می‌کنید. فضای تصمیم‌گیری در بازی گسترش می‌یابد.

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

پاداش واقعی

حلقه «فقط یک دور دیگر» (one more run) یک سیستم واحد نیست. بلکه رابطه‌ای میان از دست دادن و به دست آوردن است که بر پایه معماری تمیز و پاداش‌های معنادار بنا شده است.

دو خط زمانی متمایز بسازید و از Meta Clock طوری محافظت کنید که انگار اعتماد بازیکنان شما را در خود دارد، چون واقعاً همین‌طور است. از ابزارهای موتور بازی خود استفاده کنید تا داده‌های زنده را سریع و داده‌های ماندگار را ایمن نگه دارید. صحنه‌های خود را از سیستم ذخیره‌سازی جدا کنید (decouple) تا بتوانید بدون ترس، فرآیند توسعه را تکرار و بهبود کنید. و وقتی ارتقاها را طراحی می‌کنید، از خود بپرسید که آیا آن‌ها به بازیکن زمان بیشتری می‌دهند یا انتخاب‌های جذاب‌تر.

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