کتاب Game Programming Patterns از Nystrom را در این صفحه میخواهیم بررسی کنیم:
در دنیای انبوه کتابهای الگوهای طراحی، کتاب Game Programming Patterns جایگاه منحصربهفردی دارد. تفاوت آن در سه محور اساسی است:
بسیاری از کتابهای الگوهای طراحی، نرمافزارهای عمومی مانند سیستمهای اطلاعاتی و نرمافزارهای سازمانی را هدف قرار میدهند. اما این کتاب، الگوها را در بستر سختگیرانهترین حوزه نرمافزاری بررسی میکند؛ محیطی که هر میلیثانیه حیاتی است، حافظه محدود است، و کاربر انتظار پاسخگویی بیوقفه دارد. نویسنده با طنز میگوید که الگوهای Gang Of Four برای نرمافزارهای بانکی نوشته شدهاند، اما در بازی هیچکس منتظر نمیماند تا تراکنشتان تایید شود.
Nystrom مثالهای خود را از چالشهای واقعی در توسعه بازی میگیرد؛ مانند مدیریت هزاران شیء همزمان مثل ذرات، دشمنان و گلولهها، تعامل پیچیده بین سیستمهای فیزیک، صدا، هوش مصنوعی و رندرینگ، و نیاز به انعطافپذیری برای اضافه کردن فیچرهای جدید بدون تخریب ساختار موجود میگوید.
برخلاف کتابهای خشک دانشگاهی، Nystrom با زبان طنز و مثالهای عجیب مفاهیم سنگین را شیرین میکند. این سبک باعث میشود کتاب نه یک منبع مرجع خشک، بلکه یک مکالمهی دوستانه با یک برنامهنویس باتجربه باشد. او جایی میگوید اگر تا به حال در یک بازی، نیمفریم از یک صحنه را دیدهاید که نصفش کشیده شده و نصفش نه، یعنی Double Buffer خراب شده است و آن لحظه، لعنت به حافظهای که بافر را خراب کرده و من به عنوان یک فرد علاقمند به بازی و بازیسازی عمیقا با او همدردی میکنم.

Nystrom در مقدمه و فصل اول، یک تز کلیدی مطرح میکند که با کلیشههای رایج تفاوت اساسی دارد. او معماری در بازیها را هنرِ مدیریتِ پیچیدگی در یک محیطِ با محدودیتِ شدیدِ زمان میداند. به عبارت دیگر، در یک نرمافزار معمولی مثل یک ویرایشگر متن، کار کردن هدف اصلی است. اما در یک بازی، کار کردنِ سریع و بیوقفه هدف است و این تفاوت، همهچیز را تغییر میدهد.
Nystrom در سراسر کتاب، پنج ویژگی کلیدی برای معماری خوب ترسیم میکند:
جداسازی: سیستمها نباید از وجود یکدیگر مطلع باشند. برای مثال، فیزیک نباید از سیستم دستاوردها خبر داشته باشد.
مدیریت زمان: معماری باید کنترل دقیقی بر زمان اجرا داشته باشد. حلقهی بازی باید بتواند سرعت را با سختافزار تطبیق دهد.
انعطافپذیری: باید بتوان فیچرهای جدید اضافه کرد بدون تخریب ساختار. الگوی Component امکان اضافه کردن قابلیت پرواز به هر موجودیتی را میدهد.
کارایی: معماری باید بهینهترین استفاده را از سختافزار ببرد. الگوی Data Locality برای استفاده بهینه از کش CPU طراحی شده است.
سادگی: پیچیدگی، دشمن اصلی برنامهنویس است. به همین دلیل نویسنده الگوی Singleton را به دلیل پیچیدگیهایش رد میکند.
Nystrom بارها تأکید میکند که معماری فقط برای امروز نیست. یک معماری خوب باید بتواند تغییرات آینده را به راحتی جذب کند. او میگوید اگر نتوانید به راحتی فیچر جدیدی به بازی اضافه کنید، معماری شما شکست خورده است، حتی اگر امروز عالی کار کند.
درک تفاوت بازیها با نرمافزارهای معمولی، کلید فهمیدن این کتاب است. نویسنده این تفاوت را در چند محور اساسی توضیح میدهد:
حلقهی بیوقفه:نرمافزارهای معمولی منتظر ورودی کاربر میمانند و وقتی کاری نیست، بیکار مینشینند. اما بازیها هرگز نمیایستند. حتی وقتی کاربر کاری نمیکند، انیمیشنها حرکت میکنند، هوش مصنوعی تصمیم میگیرد، و دنیای بازی به زندگی خود ادامه میدهد. این یعنی معماری بازی باید بتواند یک چرخهی بینهایت را به صورت روان مدیریت کند.
عملکرد حیاتی: در یک نرمافزار حسابداری، اگر پردازش یک تراکنش نیمثانیه طول بکشد، کسی متوجه نمیشود. اما در یک بازی، اگر یک فریم بیشتر از ۳۳ میلیثانیه طول بکشد، بازیکن لگ را احساس میکند و از بازی دلزده میشود. به عبارت دیگر، در بازیها زمان یک منبع حیاتی است که باید به دقت مدیریت شود.
مدیریت حافظه: نرمافزارهای معمولی میتوانند حافظه را تخصیص و آزاد کنند و سیستمعامل همهچیز را جمعوجور کند. اما در بازیها، تخصیص و آزادسازی مکرر حافظه باعث خردشدگی حافظه میشود و میتواند بازی را از کار بیندازد. به همین دلیل، بازیها معمولاً از تخصیص پویا حافظه اجتناب میکنند.
همزمانی موجودیتها: در یک نرمافزار معمولی، ممکن است چند شیء با هم کار کنند، اما تعدادشان محدود است. در یک بازی، ممکن است هزاران موجودیت همزمان وجود داشته باشند؛ هر کدام با رفتار، موقعیت و وضعیت خاص خود قرار دارند. مدیریت این همه موجودیت به صورت همزمان، یک چالش بزرگ معماری است.
تغییرپذیری مداوم: نرمافزارهای معمولی پس از انتشار، تغییرات کمی میکنند. اما بازیها هرگز تمام نمیشوند. همیشه فیچر جدید، دشمن جدید، آیتم جدید یا حالت جدید اضافه میشود. معماری بازی باید به گونهای باشد که این تغییرات را به راحتی جذب کند.
Nystrom از میان دهها الگوی طراحی، نوزده الگو را انتخاب کرده است. معیار انتخاب او ساده است: الگوهایی که مستقیماً چالشهای منحصربهفرد بازیسازی را حل میکنند.
چالش حلقهی بیوقفه: برخلاف نرمافزارهای معمولی که منتظر ورودی کاربر میمانند، بازیها همیشه در حال اجرا هستند، حتی وقتی کاربر کاری نمیکند. الگوی راهحل، Game Loop است. نویسنده با طنز میگوید که بازیها مثل یک دیوانهی خوشحال هستند که هیچوقت نمیایستند و باید بتوانی با آنها مسابقه دو بدهی.
چالش انفجار موجودیتها: در یک بازی، ممکن است صدها یا هزاران موجودیت همزمان وجود داشته باشند؛ مانند دشمنان، گلولهها، ذرات و آیتمها. الگوی راهحل، Object Pool و Component است. نویسنده میگوید اگر برای هر گلولهای که شلیک میشود، حافظه تخصیص دهید، بازی شما مثل یک لکلکِ پرخور، حافظه را میبلعد و هضم نمیکند.
چالش حالتهای متعدد: یک کاراکتر بازی ممکن است در حالتهای مختلفی باشد: ایستاده، در حال دویدن، پریدن، شیرجه، و غوطهوری. ترکیب این حالتها با عوامل دیگر مثل سلاح در دست میتواند به انفجار حالات منجر شود. الگوی راهحل، State و Hierarchical State Machine است. نویسنده میگوید به جای اینکه هزاران شرط if بنویسید، هر حالت را به یک کلاس مجزا تبدیل کنید تا اگر ایرادی باشد، فقط یک کلاس را و نه کل بازی را عوض کنید.
چالش ارتباطات بینسیستمی: سیستمهای مختلف بازی مانند فیزیک، صدا، هوش مصنوعی و رندرینگ باید با هم ارتباط داشته باشند، اما نباید از وجود یکدیگر مطلع باشند. الگوی راهحل، Observer، Event Queue و Service Locator است. نویسنده میگوید سیستمهای بازی مثل همسایههای بداخلاق در یک آپارتمان هستند؛ باید بتوانند از هم باخبر شوند، بدون اینکه به درِ هم بزنند.
چالش تغییرپذیری و افزودن فیچر: بازیها هرگز تمام نمیشوند؛ همیشه فیچر جدید، دشمن جدید، یا آیتم جدید اضافه میشود. الگوی راهحل، Type Object، Component و Bytecode است. نویسنده میگوید بازیها مثل یک رستوران هستند که هر روز غذای جدید به منو اضافه میکنند و اگر برای هر غذا یک آشپزخانهی جدید بسازید، دیوانه شدهاید.
چالش مدیریت حافظه: تخصیص و آزادسازی مکرر حافظه در بازیها منجر به خردشدگی حافظه میشود که میتواند بازی را از کار بیندازد. الگوی راهحل، Object Pool و Flyweight است. نویسنده میگوید تخصیص حافظه در بازی مثل پارک کردن در خیابانهای شلوغ است؛ اگر ماشینها را مرتب نکنید، جای پارک برای ماشین جدید پیدا نمیکنید.
پس از مطالعهی این کتاب، این درک دریافت میشود که معماری نرمافزار در بازیسازی، چیزی فراتر از انتخاب الگوی درست است. این یک هنر است که شامل موارد زیر میشود:
تشخیص چالشهای منحصربهفرد: بازیها با نرمافزارهای معمولی تفاوت اساسی دارند و درک این تفاوتها، اولین قدم برای معماری خوب است.
انتخاب الگوی مناسب برای چالش مناسب: هر الگو برای حل یک چالش خاص طراحی شده است و انتخاب الگوی اشتباه، بدتر از عدم استفاده از الگو است.
تعادل بین سادگی و پیچیدگی: Nystrom بارها تأکید میکند که سادگی، مهمترین ویژگی معماری خوب است. اما گاهی برای حل چالشهای پیچیده، به الگوهای پیچیدهتر نیاز داریم.
پذیرش تغییرات: بازیها هرگز تمام نمیشوند و معماری باید به گونهای باشد که اضافه کردن فیچرهای جدید، آسان و بدون تخریب ساختار موجود باشد.
اولویت با عملکرد: در بازیها، عملکرد از خوانایی یا زیبایی کد مهمتر است و معماری باید به گونهای باشد که بازی روان اجرا شود.
کتاب Game Programming Patterns، یک منبع ارزشمند برای هر بازیساز است. اما فراتر از آن، این کتاب به ما یاد میدهد که معماری نرمافزار یک هنر است. هنرِ تشخیص چالشها، انتخاب راهحلهای مناسب، و ایجاد تعادل بین سادگی، انعطافپذیری و عملکرد. Nystrom با سبک طنز و صمیمی خود، مفاهیم سنگین را شیرین میکند و نشان میدهد که معماری نرمافزار میتواند جدی ولی خشک نباشد. او به ما یادآوری میکند که در نهایت، هدف ما از معماری، ساختن بازیهایی است که مردم دوست داشته باشند، نه صرفاً کدی که از نظر تئوری درست باشد.
به عنوان کسی که همیشه به بازیسازی علاقه داشتهام، خواندن این کتاب برای من مثل پیدا کردن یک نقشهی گنج بود.
چندین بار در حین مطالعه، به پروژههای کوچکی که قبلاً نوشته بودم فکر میکردم و با خودم میگفتم انگار ناخوداگاه به این الگو ها در زمان نوشتن کد فکر کردهام و حتی تا حدودی مانند آنها کدنویسی کردهام یا اگر آن موقع این الگو را میدانستم، خیلی زودتر میتوانستم بازی را به پایان برسانم. همان موقع بود که فهمیدم چرا بعضی از بازیهای مستقل اینقدر سریع ساخته میشوند؛ چون از ابتدا معماری درستی دارند و معماری نجاتبخش است. یا گاها طراح بازی ایونتی در ذهنش نقش میبند و کد اجرایی را می خواهد و قرار نیست این کد دور ریخته شود و معماری درستی ندارد و همه چیز را به دردسر میاندازد.
بهتر است سری به محتوای این کتاب بزنیم
الگوی Command: این الگو تبدیل درخواست به یک شیء برای مدیریت ورودی، صفبندی، و قابلیت Undo/Redo است. در بازی برای مدیریت ورودی کاربر، شبکه و مالتیپلیر، ضبط و پخش مجدد، و سیستم Undo/Redo در ادیتورهای سطح کاربرد دارد. نویسنده با طنز میگوید که تعریف گانگ آف فور از Command، یکی از افتضاحترین جملاتی است که تا به حال خواندهام و به زبان ساده یعنی یک تابع را تبدیل به یک شیء میکنیم تا بتوانیم آن را در یک متغیر ذخیره کنیم، به یک تابع دیگر بفرستیم، یا حتی آن را از طریق شبکه ارسال کنیم.
Nystromتعریف گانگ آف فور از Command، را به یکی از افتضاحترین جملاتی است که تا به حال خواندهاست تشبیه میکند. "یک درخواست را به عنوان یک شیء کپسوله کنید..." یعنی چی؟ مگر میشود یک مشتری را پارامتریزه کرد!
الگوی Flyweight: این الگو برای بهاشتراکگذاری دادههای تکراری بین اشیاء مشابه به منظور کاهش مصرف حافظه استفاده میشود. در بازی برای جنگلهای هزاران درخت، سیستم تایل، و بهینهسازی رندر کاربرد دارد. نویسنده میگوید Flyweight یعنی دادههای مشترک را در یک جا نگه داریم و همهی اشیاء به آن اشاره کنند، مثل این است که همهی سربازان یک ارتش، از یک مدل کلاه استفاده کنند و درختان یک جنگل، همه شبیه هم هستند.
Nystrom میگوید که: درختان یک جنگل، همه شبیه هم هستند. مگر اینکه دیوانه باشید یا میلیاردر که هنرمندان را استخدام کنید تا هر درخت را جداگانه مدلسازی کنید!
الگوی Observer: این الگو برای ارتباط غیرمستقیم بین سیستمها بدون وابستگی مستقیم طراحی شده است. در بازی برای سیستم دستاوردها، سیستم صدا، و بهروزرسانی UI کاربرد دارد. نویسنده میگوید Observer یعنی یک سیستم فریاد میزند که من نمیدانم چه کسی گوش میدهد، اما این اتفاق افتاد و هرکس که علاقهمند است، پیام را دریافت میکند. این الگو آنقدر فراگیر است که جاوا آن را در کتابخانهی اصلی خود قرار داد و سیشارپ آن را به عنوان کلیدواژهی event به زبان اضافه کرد.
از نظر او این الگو آنقدر فراگیر است که جاوا آن را در کتابخانهی اصلی خود قرار داد و سیشارپ آن را به عنوان کلیدواژهی event به زبان اضافه کرد. انگار که این الگو، پدرخواندهی دنیای نرمافزار است!
الگوی Prototype: این الگو برای ایجاد اشیاء جدید با کپی کردن از یک شیء نمونه یا کلون کردن استفاده میشود. در بازی برای سیستم اسپاون و تولید موجودیتهای مشابه با تنظیمات متفاوت کاربرد دارد. نویسنده میگوید Prototype یعنی یک شیء بتواند از خودش کپی بگیرد و اگر یک Ghost دارید، میتوانید از آن Ghostهای بیشتری بسازید. این الگو در سال ۱۹۶۳ توسط ایوان ساترلند در پروژهی Sketchpad اختراع شد.
Nystrom میگوید : این الگو در سال ۱۹۶۳ توسط ایوان ساترلند در پروژهی Sketchpad اختراع شد. در همان سالی که بقیه داشتند به بیتلز و دیلن گوش میدادند، او مشغول اختراع مفاهیم پایهای CAD، گرافیک تعاملی و برنامهنویسی شیگرا بود.
الگوی Singleton: نویسنده این الگو را به دلایل مختلف رد میکند. او میگوید Singleton یک متغیر سراسری است و همهی مشکلات متغیرهای سراسری را دارد. همچنین دو مشکل را با یک راهحل حل میکند؛ محدودیت به یک نمونه با دسترسی سراسری دو مسئلهی مجزا هستند. او راهکارهای جایگزینی مانند Static Class، ارسال به عنوان پارامتر، و Service Locator را پیشنهاد میدهد.
الگوی State این الگو برای مدیریت حالتهای مختلف یک شیء بدون استفاده از شرطهای پیچیده استفاده میشود. در بازی برای حالتهای کاراکتر، هوش مصنوعی، و منوی بازی کاربرد دارد. نویسنده میگوید State یعنی هر حالت را به یک کلاس مجزا تبدیل کنیم و وقتی کاراکتر در حال پریدن است، فقط کدهای مربوط به پرش اجرا میشوند. ماشین حالت از دل تئوری اتوماتا آمده، اما State Machine سادهترین عضو این خانواده است.
الگوی Double Buffer: این الگو برای نمایش یک فریم کامل، بدون نمایان شدن مراحل میانی رندرینگ استفاده میشود. در بازی برای رندرینگ گرافیکی، سیستم فیزیک، و سیستم صدا کاربرد دارد. نویسنده میگوید Double Buffer یعنی دو صحنهی تئاتر داشته باشیم و در یکی نمایش اجرا میشود و در دیگری، صحنهی بعدی آماده میشود. وقتی نمایش تمام شد، چراغها را عوض میکنیم و نمایش جدید بدون وقفه شروع میشود.
الگوی Game Loop: این الگو برای جدا کردن زمان بازی از سرعت پردازنده و ورودی کاربر طراحی شده است. در بازی قلب هر بازی محسوب میشود و برای تطبیق با سختافزارهای مختلف و مدیریت زمان کاربرد دارد. نویسنده میگوید Game Loop یعنی بازی هرگز منتظر ورودی کاربر نمیماند و همیشه در حال چرخش است. اگر کاربر کاری نکند، باز هم انیمیشنها حرکت میکنند، جلوههای ویژه میدرخشند، و هیولاها قهرمان ما را میجوند.
در اینجا هم او بامزه میگوید: در روزگار قدیم، کامپیوترها دکمهی Turbo داشتند. چون بازیهای قدیمی روی کامپیوترهای جدید خیلی سریع اجرا میشدند و غیرقابل بازی بودند. حالا ما باید خودمان این کار را با کد انجام دهیم!
الگوی Update Method: این الگو برای شبیهسازی رفتار همزمان موجودیتهای مستقل استفاده میشود. در بازی برای هر موجودیت در بازی و سیستمهای مستقل کاربرد دارد. نویسنده میگوید Update Method یعنی به هر موجودیت بگوییم در این فریم، یک قدم از رفتارت را انجام بده. اگر یک اسکلت گشتزن دارید که باید به چپ و راست برود، به جای اینکه یک حلقهی بینهایت بنویسید و بازی را قفل کنید، به آن یک متد update بدهید که هر فریم یک قدم بردارد.
الگوی Bytecode: این الگو برای تعریف رفتار به صورت داده، برای ایمنی و انعطافپذیری استفاده میشود. در بازی برای سیستم جادوها، هوش مصنوعی، و مودینگ کاربرد دارد. نویسنده میگوید Bytecode یعنی رفتار را به صورت داده تعریف کنیم و یک ماشین مجازی کوچک آن را اجرا کند. اینطوری، رفتارهای جدید را میتوان بدون کامپایل مجدد، و در محیطی امن اجرا کرد.
الگوی Subclass Sandbox این الگو برای تعریف رفتار در زیرکلاسها با استفاده از عملیاتهای پایهای که کلاس پایه فراهم میکند، استفاده میشود. در بازی برای سیستم ابرقدرتها و سیستم آیتمها کاربرد دارد. نویسنده میگوید Subclass Sandbox یعنی به زیرکلاسها یک جعبهی شنی از ابزارها بدهیم و به آنها بگوییم با این ابزارها هر کاری دوست دارید بکنید، اما از جعبهی شنی خارج نشوید.
«اگر صد تا ابرقدرت دارید و هرکدام مستقیم به سیستم صدا متصل است، وقتی سیستم صدا عوض میشود، باید صد تا کلاس را عوض کنید. اما اگر همه از طریق کلاس پایه به صدا متصل شوند، فقط یک کلاس را عوض میکنید.
الگوی Type Object: این الگو برای ایجاد نوعهای جدید بدون تعریف کلاسهای جدید استفاده میشود. در بازی برای گونههای دشمنان، سیستم آیتمها، و سیستم جادوها کاربرد دارد. نویسنده میگوید Type Object یعنی به جای اینکه برای هر گونه دشمن، یک کلاس جدید تعریف کنیم، یک کلاس به نام Breed داریم که هر نمونهاش، یک گونهی جدید را تعریف میکند.
"اگر دویست گونه دشمن دارید و هرکدام یک کلاس مجزا هستند، روزگار شما به این میگذرد که ایمیل طراحان را بخوانید، یک عدد را در یک کلاس عوض کنید، کامپایل کنید، و دوباره ایمیل بفرستید. این الگو شما را از این کار نجات میدهد"
الگوی Component: این الگو برای جداسازی دامنههای مختلف یک موجودیت بدون وابستگی به یکدیگر استفاده میشود. در بازی برای موجودیتهای بازی و ساخت اشیاء ترکیبی کاربرد دارد. نویسنده میگوید Component یعنی موجودیت را به قطعات کوچک و مستقل تقسیم کنیم و فیزیک از رندرینگ خبر ندارد و رندرینگ از هوش مصنوعی خبر ندارد.
Component مثل یک رستوران سلفسرویس است. شما میتوانید فقط سالاد، فقط سوپ، یا ترکیبی از هر دو را انتخاب کنید. برخلاف منوی تکراری که هر ترکیب، یک غذای جداگانه است!
الگوی Event Queue: این الگو برای جداسازی زمان ارسال رویداد از زمان پردازش آن استفاده میشود. در بازی برای سیستم صدا، ارتباط بینسیستمی، و مدیریت ورودی کاربرد دارد. نویسنده میگوید Event Queue یعنی درخواستها را در یک صف قرار دهیم و بعداً پردازش کنیم.
الگوی Service Locator: این الگو برای دسترسی سراسری به سرویسها بدون وابستگی به پیادهسازی خاص استفاده میشود. در بازی برای سیستم صدا، سیستم لاگ، و سیستم فایل کاربرد دارد. نویسنده میگوید Service Locator مثل دفترچه تلفن است و هرکس نیاز به سرویسی دارد، در دفترچه تلفن نگاه میکند. وقتی سرویس عوض میشود، فقط دفترچه تلفن را عوض میکنیم، نه همهی افرادی که از آن استفاده میکنند.
الگوی Data Locality: این الگو برای چیدمان دادهها در حافظه برای استفاده بهینه از کش CPU استفاده میشود. در بازی برای آرایههای پیوسته برای کامپوننتها، مرتبسازی اشیاء بر اساس وضعیت فعال، و جداسازی دادههای داغ و سرد کاربرد دارد. نویسنده میگوید Data Locality یعنی دادههایی که با هم پردازش میشوند را کنار هم در حافظه قرار دهیم.
الگوی Dirty Flag: این الگو برای بهروزرسانی فقط زمانی که داده تغییر کرده است استفاده میشود. در بازی برای صحنهگراف، ذخیرهسازی خودکار، و بهروزرسانی UI کاربرد دارد. نویسنده میگوید Dirty Flag یعنی بهجای اینکه هر بار همهچیز را محاسبه کنیم، فقط وقتی داده تغییر کرده است، محاسبه را انجام دهیم.
الگوی Object Pool: این الگو برای استفاده مجدد از اشیاء به جای تخصیص و آزادسازی مکرر استفاده میشود. در بازی برای سیستم ذرات، گلولهها، و دشمنان کاربرد دارد. نویسنده میگوید Object Pool یعنی یک مجموعه از اشیاء از پیش تخصیصدادهشده داشته باشیم و وقتی به شیء جدید نیاز داریم، از این مجموعه برمیداریم و وقتی تمام شد، به مجموعه برمیگردانیم.
الگوی Spatial Partition: این الگو برای پیدا کردن سریع اشیاء بر اساس موقعیت مکانی استفاده میشود. در بازی برای برخوردیابی، رندرینگ، و هوش مصنوعی کاربرد دارد. نویسنده میگوید Spatial Partition یعنی اشیاء را بر اساس موقعیت مکانی در یک ساختار داده سازماندهی کنیم تا برای پیدا کردن اشیاء نزدیک به یک نقطه، نیازی به بررسی همهی اشیاء نداشته باشیم.