برنامه نویسی

Hexaflare: کاوش در ساختارهای داده – انجمن DEV

Summarize this content to 400 words in Persian Lang
من در ابتدا وارد برنامه نویسی شدم زیرا می خواستم یک بازی بسازم، و اگرچه در حال حاضر توسعه وب انجام می دهم، این شانس را داشتم که یک بازی با جاوا اسکریپت وانیلی و CSS به نام Hexaflare بسازم که در اوایل سال 2020 ساختم. این یکی از بهترین ها بود. پروژه های برنامه نویسی سرگرم کننده ای که تا به حال روی آنها کار کرده ام، و ایده اصلی آن زمانی به ذهنم خطور کرد که در حال مطالعه ساختار داده ها و الگوریتم ها بودم.

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

هگزافلر چیست؟
شش ضلعی در طبیعت
الگوی کاشی شش ضلعی (ساختار داده؟)
Flare Star UI: نوشتن الگوی کاشی به صورت پویا
ورود به ناشناخته: خوشه های ستاره ای متحرک و چرخان
شبیه سازی جاذبه
تقریبا تسلیم شدن
شعله ور شدن: پاک کردن حلقه ها هنگام پر شدن
تایمر، سطوح، و اجرای نهایی

★ کد Blooper تالیف

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

اینجا بازی کن! https://hexaflare.fly.dev

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

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

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

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

در یک پشته حداکثر، ریشه بزرگترین مقدار در مجموعه داده است.

بیایید ببینیم هنگام پر کردن یک پشته حداکثر از مجموعه‌ای از داده‌ها، مقادیر چگونه مدیریت می‌شوند. در تجسم برنامه می بینید که هر مقدار یکی یکی اضافه می شود.

در اینجا می توانید ببینید که 2 زیر 5 اضافه شده است. به عنوان یک گره فرزند به 5 باقی می ماند زیرا 5 مقدار بزرگتری است. بیایید ببینیم وقتی 7 را اضافه می کنیم چه اتفاقی می افتد.

به عنوان یک قاعده، ما آن را به عنوان یک کودک اضافه می کنیم، اما هر بار که یک عنصر جدید اضافه می کنیم، مجموعه داده ها را با مقایسه والد و فرزند مرتب می کنیم. سپس اگر مقدار فرزند بزرگتر باشد، آنها را تغییر می دهیم، بنابراین مطمئن می شویم که ریشه مجموعه داده ها همیشه بزرگترین مقدار است. بنابراین، در اینجا 7 و 5 را سوئیچ می کنیم.

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

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

[
[1] # The center (root) of the pattern.
[1, 2, 3, 4, 5, 6], # Ring 1
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], # Ring 2
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18] # Ring 3
]

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

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

اولین ایده ای که داشتم این بود که ساختار را تا حد امکان به یک پشته (یا دیگر ساختارهای داده برای آن موضوع) نزدیک کنم. این بدان معنی است که ریشه در بالا قرار می گیرد و گره های فرزند به سمت پایین چکه می کنند. می توانید آن را در تصویر بالا و همچنین در این تصویر مشاهده کنید:

این قطعا به آنچه تصور می کردم نزدیک تر می شد. با یک ریشه شروع می شود، 1، و سپس به 6 گره فرزند که یک حلقه را نشان می دهد، فن می دهد. با پایین رفتن از هر کودک، چیزی عجیب در مورد حلقه ها می بینید. در حالی که حلقه اول دارای شش عنصر (1 تا 6) است، حلقه دوم دارای 12 عنصر است. سپس حلقه زیر دارای 18 عنصر است. من یک انتزاع جدید در الگو پیدا کرده بودم: هر حلقه متوالی دقیقاً 6 افزایش می یابد.

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

همانطور که در اینجا می بینید، حلقه اول فقط دارای 6 عنصر است، اما وقتی به حلقه دوم می رسیم، گره های فرزند زیر به طرفین می آیند. اینجا جایی است که من یک انتزاع جدید از الگوی کاشی شش ضلعی پیدا کردم. دو انتزاع پیدا کردم که آن ها را «شش ضلعی گوشه» و «شش ضلعی کناری» نامیدم. شش ضلعی گوشه ای هر شش ضلعی است که روی یک خط مستقیماً از ریشه امتداد می یابد (در تصویر بالا، می بینید که عدد 1 به طور پیوسته به سمت راست امتداد دارد. این اولین شش ضلعی گوشه ای در حلقه است و هر حلقه دارای کل است. از 6 گوشه شش ضلعی). شش ضلعی کناری هر چیز دیگری است که شش ضلعی گوشه ای نباشد (به سمتی که از شش ضلعی گوشه فاصله دارد امتداد می یابد).

بدون وارد شدن به جزئیات نحوه یافتن گره های والد و غیره، این آرایه دو بعدی در نهایت همان چیزی بود که من برای پیاده سازی مکانیک گرانشی که یک گره برگ را می گیرد و آن را تا ریشه ردیابی می کند، پیدا کردم. فکر کردن به این فرآیند و آزمایش بسیار سرگرم کننده است. می توانم بگویم که من واقعاً از یادگیری در مورد ساختارهای داده مانند heaps و سپس استفاده از مفاهیم مشابه در کد خودم لذت بردم.

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

function generateFlareStar(number_of_rings) {
// Generate Core (root)
var core = 1

// `flare_star` is the hexagonal tile pattern we’ll be returning.
var flare_star = [[core]]

// The rings start from the second step
// (In other words, the core itself isn’t a ring)
// `level` represents how many levels deep the structure is.
var level = core + number_of_rings

for(var i = 1; i < level; i++) {
// Generate new ring
var ring = []

// Here we’re just populating each array with numbers.
// 1-6 for ring 1, 1-12 for ring 2, 1-18 for ring 3, etc.
for(var value = 1; value <= i * 6; value++) {
ring.push(value)
}
flare_star[i] = ring
}
return flare_star
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

مشکل نگاشت این در CSS این بود که تمام پیکسل ها باید به صورت پویا در هنگام تولید هر شش ضلعی تعیین شوند. من در نهایت به دو عملکرد زیر رسیدم:

// This generates the array
// 12 here determines how many rings the structure has
flare_star = generateFlareStar(12)

// This generates the CSS
generateFlareStarUI(numberOfRings(flare_star))

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در مثال زیر صفحه بازی با 12 حلقه شروع می شود، سپس آن را به 7 تغییر می دهم و صفحه را Refresh می کنم تا اندازه آن تغییر کند.

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

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

من در نهایت به یک انتزاع نقشه برداری رسیدم که نقشه ای از بلوک را می گیرد و کل بلوک شش ضلعی را مطابق آن نقشه می چرخاند.

نقشه به خودی خود هر شش ضلعی را ردیابی می کند و اطلاعاتی در مورد مکان بعدی دارد. به عنوان مثال، این بخشی از یکی از انواع بلوک به نام Pleiades است، یک خط مستقیم از چهار شش ضلعی:

const PLEIADES_DATA = {

“hexagon_2”: {
“center_of_gravity”: true,
“rotation_pattern”: {
“position_1”: [[“left”, 1]],
“position_2”: [[“up_left”, 1]],
“position_3”: [[“up_right”, 1]],
“position_4”: [[“left”, 2]],
“position_5”: [[“up_left”, 2]],
“position_6”: [[“up_right”, 2]],
}
},
“hexagon_3”: {
“center_of_gravity”: false,
“rotation_pattern”: {
“position_1”: [[“left”, 2]],
“position_2”: [[“up_left”, 2]],
“position_3”: [[“up_right”, 2]],
“position_4”: [[“left”, 1]],
“position_5”: [[“up_left”, 1]],
“position_6”: [[“up_right”, 1]],
}
},

}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

هر یک position_1 برای مثال در اینجا اولین موقعیت هر شش ضلعی برای کل بلوک را نشان می دهد. بنابراین، هنگام چرخش یک بلوک به position_2، مقادیر x و y را از نسخه اصلی دریافت می کنیم div عناصر و داده های بلوک را برای محاسبه مختصات جدید به Mapper ارسال می کند، بنابراین کل بلوک را به طور مناسب می چرخاند. اگر علاقه مند هستید، می توانید Mapper را در اینجا بررسی کنید.

روشی در پایه کد وجود دارد به نام rotate که منطق بیشتری برای تحقق این امر دارد. متدی به نام را فراخوانی می کند getCoordinatesByMap که در فایل لینک شده در بالا یافت می شود، و این تا حد زیادی جالب ترین بخش پروژه بود. با این حال، من متوجه شدم که بسیار سرگرم کننده است، اگرچه مدتی طول کشید تا به درستی کار کند.

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

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

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

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

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

do {
// Gravitation logic…
gravitation_direction = getGravitationDirection(center_of_gravity)
} while(gravitation_direction != null && starClusterCanGravitateToCore(star_cluster, gravitation_direction))

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

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

حالا که مکانیک گرانش و برخورد کار می کرد، آخرین قدم بزرگ پاک کردن حلقه ها و بالا بردن امتیاز بود. مشابه ساخت یک خط در تتریس، ابتدا بررسی می کنیم که آیا حلقه های کامل وجود دارد یا خیر، و اگر وجود داشته باشد، ما آن را فراخوانی می کنیم. flare روش (که به سادگی شش ضلعی ها را حذف می کند) و 1 را به تعداد کل شعله ها اضافه می کنیم که بررسی می کند که آیا باید سطح را بالا ببریم یا نه.

while(fullRingExists(flare_star_rings)) {

// Flare 💫🔥
for (var i = 0; i < flare_star_rings.length; i++) {
if(ringIsFull(flare_star_rings[i])){
flare(flare_star_rings[i])

current_flare_count += 1
TOTAL_FLARE_COUNT += 1
document.getElementById(“flare_count”).innerHTML = TOTAL_FLARE_COUNT

// Level up here.
if(TOTAL_FLARE_COUNT >= 12 && TOTAL_FLARE_COUNT % 12 == 0 && CURRENT_LEVEL < 24) {
CURRENT_LEVEL = parseInt(CURRENT_LEVEL) + 1
document.getElementById(“level”).innerHTML = CURRENT_LEVEL
}

if(parseInt(flare_star_rings[i].dataset[“level”]) == 1) { flareTheCore() }
}
}


}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

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

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

// Just `if(UPDATE_TIMER){…}` below should be fine…
// Don’t mind the messy code!
function processTimerEvents() {
if(GAME_OVER == false && !GAME_PAUSED) {
var timer_speed = 0.2 + (0.08 * CURRENT_LEVEL)
if(UPDATE_TIMER == true) { current_prog -= timer_speed }
}

}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

current_prog مقدار درصدی را نشان می دهد که از 1 شروع می شود و به عقب شمارش می کند. پس از رسیدن به 0، بلوک به طور خودکار حذف می شود و اگر بازیکن سریعتر بلوک را رها کند، با فاکتور کردن پیشرفت فعلی تایمر، امتیاز بالاتری دریافت می کند.

و این در مورد آن است! بخشی از من می خواهد در مورد آن صحبت کند rotate روشی که قبلاً با جزئیات ذکر کردم، اما این مقاله در حال حاضر به اندازه کافی طولانی است، بنابراین من فقط موارد را در اینجا جمع بندی می کنم. با تشکر برای خواندن!

اولین چیزی که می خواهم بگویم این است که روشی که من این را نوشتم یک روش معمولی برای نوشتن جاوا اسکریپت نیست. من در واقع هیچ کلاسی ننوشتم (من فقط همه توابع را در فایل های خود سازماندهی کردم gravity.js، flare.js، mapping.js، و غیره.). من هم متغیرهای سراسری زیادی دارم 😬. هیچ راهی وجود ندارد که هنگام کار با یک تیم از این رویکرد استفاده کنم، اما این یک پروژه شخصی بود که من فقط می خواستم به اصطلاح “روی کاغذ بیاورم” و فکر می کنم اگر دوباره آن را بسازم، آن را انجام خواهم داد. در یونیتی با اصول برنامه نویسی بسیار بهتر.

با این اوصاف، این یک کد بسیار بد است که من نوشتم…

function togglePauseMenu() {
if(GAME_PAUSED) {
GAME_PAUSED = false
} else {
GAME_PAUSED = true
}

}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

به طور جدی؟ شاید بهتر باشد به جای نوشتن با این نحو فکر کنم:

این یکی شبیهه

if(level_to_adjust > 12) {
return 0
} else if (level_to_adjust < 1) {
return 0
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این مورد بعدی یک باگ UI است که هنگام تلاش برای انتقال کل بلوک به گوشه ای از صفحه بازی اتفاق می افتد و من هنوز از نظر فنی آن را برطرف نکرده ام … اما کار می کند؟ بنابراین فعلاً به آن دست نمی زنم. فوق العاده شکننده 😅

// Redraw!
// Again, this is really hacky, but it works.
// ¯\_(ツ)_/¯
var reverse_direction = direction == “clockwise” ? “counter-clockwise” : “clockwise”
rotate(direction, star_cluster, star_cluster_type)
moveAlongCorona(direction, star_cluster, star_cluster_type)
moveAlongCorona(reverse_direction, star_cluster, star_cluster_type)
rotate(reverse_direction, star_cluster, star_cluster_type)

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بنابراین وجود دارد که! من چیزهای زیادی یاد گرفتم و هنوز چیزهای بیشتری برای یادگیری وجود دارد، اما این یک پروژه بسیار سرگرم کننده بود و من مجبور شدم یک بازی بسازم! مطمئن نیستم که روزی آن را در یونیتی بازسازی کنم یا نه، اما تجربه جالبی بود، و خوشحالم که توانستم آن را بسازم.

ممنون که خواندید!

من در ابتدا وارد برنامه نویسی شدم زیرا می خواستم یک بازی بسازم، و اگرچه در حال حاضر توسعه وب انجام می دهم، این شانس را داشتم که یک بازی با جاوا اسکریپت وانیلی و CSS به نام Hexaflare بسازم که در اوایل سال 2020 ساختم. این یکی از بهترین ها بود. پروژه های برنامه نویسی سرگرم کننده ای که تا به حال روی آنها کار کرده ام، و ایده اصلی آن زمانی به ذهنم خطور کرد که در حال مطالعه ساختار داده ها و الگوریتم ها بودم.

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

  1. هگزافلر چیست؟
  2. شش ضلعی در طبیعت
  3. الگوی کاشی شش ضلعی (ساختار داده؟)
  4. Flare Star UI: نوشتن الگوی کاشی به صورت پویا
  5. ورود به ناشناخته: خوشه های ستاره ای متحرک و چرخان
  6. شبیه سازی جاذبه
  7. تقریبا تسلیم شدن
  8. شعله ور شدن: پاک کردن حلقه ها هنگام پر شدن
  9. تایمر، سطوح، و اجرای نهایی
  10. ★ کد Blooper تالیف

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

اینجا بازی کن! https://hexaflare.fly.dev

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

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

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

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

در یک پشته حداکثر، ریشه بزرگترین مقدار در مجموعه داده است.

توضیحات تصویر

بیایید ببینیم هنگام پر کردن یک پشته حداکثر از مجموعه‌ای از داده‌ها، مقادیر چگونه مدیریت می‌شوند. در تجسم برنامه می بینید که هر مقدار یکی یکی اضافه می شود.

توضیحات تصویر

توضیحات تصویر

در اینجا می توانید ببینید که 2 زیر 5 اضافه شده است. به عنوان یک گره فرزند به 5 باقی می ماند زیرا 5 مقدار بزرگتری است. بیایید ببینیم وقتی 7 را اضافه می کنیم چه اتفاقی می افتد.

توضیحات تصویر

به عنوان یک قاعده، ما آن را به عنوان یک کودک اضافه می کنیم، اما هر بار که یک عنصر جدید اضافه می کنیم، مجموعه داده ها را با مقایسه والد و فرزند مرتب می کنیم. سپس اگر مقدار فرزند بزرگتر باشد، آنها را تغییر می دهیم، بنابراین مطمئن می شویم که ریشه مجموعه داده ها همیشه بزرگترین مقدار است. بنابراین، در اینجا 7 و 5 را سوئیچ می کنیم.

توضیحات تصویر

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

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

[
  [1] # The center (root) of the pattern.
  [1, 2, 3, 4, 5, 6], # Ring 1
  [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], # Ring 2
  [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18] # Ring 3
]
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

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

توضیحات تصویر

اولین ایده ای که داشتم این بود که ساختار را تا حد امکان به یک پشته (یا دیگر ساختارهای داده برای آن موضوع) نزدیک کنم. این بدان معنی است که ریشه در بالا قرار می گیرد و گره های فرزند به سمت پایین چکه می کنند. می توانید آن را در تصویر بالا و همچنین در این تصویر مشاهده کنید:

توضیحات تصویر

این قطعا به آنچه تصور می کردم نزدیک تر می شد. با یک ریشه شروع می شود، 1، و سپس به 6 گره فرزند که یک حلقه را نشان می دهد، فن می دهد. با پایین رفتن از هر کودک، چیزی عجیب در مورد حلقه ها می بینید. در حالی که حلقه اول دارای شش عنصر (1 تا 6) است، حلقه دوم دارای 12 عنصر است. سپس حلقه زیر دارای 18 عنصر است. من یک انتزاع جدید در الگو پیدا کرده بودم: هر حلقه متوالی دقیقاً 6 افزایش می یابد.

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

توضیحات تصویر

همانطور که در اینجا می بینید، حلقه اول فقط دارای 6 عنصر است، اما وقتی به حلقه دوم می رسیم، گره های فرزند زیر به طرفین می آیند. اینجا جایی است که من یک انتزاع جدید از الگوی کاشی شش ضلعی پیدا کردم. دو انتزاع پیدا کردم که آن ها را «شش ضلعی گوشه» و «شش ضلعی کناری» نامیدم. شش ضلعی گوشه ای هر شش ضلعی است که روی یک خط مستقیماً از ریشه امتداد می یابد (در تصویر بالا، می بینید که عدد 1 به طور پیوسته به سمت راست امتداد دارد. این اولین شش ضلعی گوشه ای در حلقه است و هر حلقه دارای کل است. از 6 گوشه شش ضلعی). شش ضلعی کناری هر چیز دیگری است که شش ضلعی گوشه ای نباشد (به سمتی که از شش ضلعی گوشه فاصله دارد امتداد می یابد).

بدون وارد شدن به جزئیات نحوه یافتن گره های والد و غیره، این آرایه دو بعدی در نهایت همان چیزی بود که من برای پیاده سازی مکانیک گرانشی که یک گره برگ را می گیرد و آن را تا ریشه ردیابی می کند، پیدا کردم. فکر کردن به این فرآیند و آزمایش بسیار سرگرم کننده است. می توانم بگویم که من واقعاً از یادگیری در مورد ساختارهای داده مانند heaps و سپس استفاده از مفاهیم مشابه در کد خودم لذت بردم.

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

function generateFlareStar(number_of_rings) {
  // Generate Core (root)
  var core = 1

  // `flare_star` is the hexagonal tile pattern we'll be returning.
  var flare_star = [[core]]

  // The rings start from the second step
  // (In other words, the core itself isn't a ring)
  // `level` represents how many levels deep the structure is.
  var level = core + number_of_rings

  for(var i = 1; i < level; i++) {
    // Generate new ring
    var ring = []

    // Here we're just populating each array with numbers.
    // 1-6 for ring 1, 1-12 for ring 2, 1-18 for ring 3, etc.
    for(var value = 1; value <= i * 6; value++) {
      ring.push(value)
    }
    flare_star[i] = ring
  }
  return flare_star
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

مشکل نگاشت این در CSS این بود که تمام پیکسل ها باید به صورت پویا در هنگام تولید هر شش ضلعی تعیین شوند. من در نهایت به دو عملکرد زیر رسیدم:

// This generates the array
// 12 here determines how many rings the structure has
flare_star = generateFlareStar(12)

// This generates the CSS
generateFlareStarUI(numberOfRings(flare_star))
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در مثال زیر صفحه بازی با 12 حلقه شروع می شود، سپس آن را به 7 تغییر می دهم و صفحه را Refresh می کنم تا اندازه آن تغییر کند.

توضیحات تصویر

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

توضیحات تصویر

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

من در نهایت به یک انتزاع نقشه برداری رسیدم که نقشه ای از بلوک را می گیرد و کل بلوک شش ضلعی را مطابق آن نقشه می چرخاند.

نقشه به خودی خود هر شش ضلعی را ردیابی می کند و اطلاعاتی در مورد مکان بعدی دارد. به عنوان مثال، این بخشی از یکی از انواع بلوک به نام Pleiades است، یک خط مستقیم از چهار شش ضلعی:

const PLEIADES_DATA = {
  ...
  "hexagon_2": {
    "center_of_gravity": true,
    "rotation_pattern": {
      "position_1": [["left", 1]],
      "position_2": [["up_left", 1]],
      "position_3": [["up_right", 1]],
      "position_4": [["left", 2]],
      "position_5": [["up_left", 2]],
      "position_6": [["up_right", 2]],
    }
  },
  "hexagon_3": {
    "center_of_gravity": false,
    "rotation_pattern": {
      "position_1": [["left", 2]],
      "position_2": [["up_left", 2]],
      "position_3": [["up_right", 2]],
      "position_4": [["left", 1]],
      "position_5": [["up_left", 1]],
      "position_6": [["up_right", 1]],
    }
  },
...
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

هر یک position_1 برای مثال در اینجا اولین موقعیت هر شش ضلعی برای کل بلوک را نشان می دهد. بنابراین، هنگام چرخش یک بلوک به position_2، مقادیر x و y را از نسخه اصلی دریافت می کنیم div عناصر و داده های بلوک را برای محاسبه مختصات جدید به Mapper ارسال می کند، بنابراین کل بلوک را به طور مناسب می چرخاند. اگر علاقه مند هستید، می توانید Mapper را در اینجا بررسی کنید.

روشی در پایه کد وجود دارد به نام rotate که منطق بیشتری برای تحقق این امر دارد. متدی به نام را فراخوانی می کند getCoordinatesByMap که در فایل لینک شده در بالا یافت می شود، و این تا حد زیادی جالب ترین بخش پروژه بود. با این حال، من متوجه شدم که بسیار سرگرم کننده است، اگرچه مدتی طول کشید تا به درستی کار کند.

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

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

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

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

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

do {
  // Gravitation logic...
  gravitation_direction = getGravitationDirection(center_of_gravity)
} while(gravitation_direction != null && starClusterCanGravitateToCore(star_cluster, gravitation_direction))
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

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

حالا که مکانیک گرانش و برخورد کار می کرد، آخرین قدم بزرگ پاک کردن حلقه ها و بالا بردن امتیاز بود. مشابه ساخت یک خط در تتریس، ابتدا بررسی می کنیم که آیا حلقه های کامل وجود دارد یا خیر، و اگر وجود داشته باشد، ما آن را فراخوانی می کنیم. flare روش (که به سادگی شش ضلعی ها را حذف می کند) و 1 را به تعداد کل شعله ها اضافه می کنیم که بررسی می کند که آیا باید سطح را بالا ببریم یا نه.

while(fullRingExists(flare_star_rings)) {

  // Flare 💫🔥
  for (var i = 0; i < flare_star_rings.length; i++) {
    if(ringIsFull(flare_star_rings[i])){
      flare(flare_star_rings[i])

      current_flare_count += 1
      TOTAL_FLARE_COUNT += 1
      document.getElementById("flare_count").innerHTML = TOTAL_FLARE_COUNT

      // Level up here.
      if(TOTAL_FLARE_COUNT >= 12 && TOTAL_FLARE_COUNT % 12 == 0 && CURRENT_LEVEL < 24) {
        CURRENT_LEVEL = parseInt(CURRENT_LEVEL) + 1
        document.getElementById("level").innerHTML = CURRENT_LEVEL
      }

      if(parseInt(flare_star_rings[i].dataset["level"]) == 1) { flareTheCore() }
    }
  }

  ...
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

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

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

// Just `if(UPDATE_TIMER){...}` below should be fine...
// Don't mind the messy code!
function processTimerEvents() {
  if(GAME_OVER == false && !GAME_PAUSED) {
    var timer_speed = 0.2 + (0.08 * CURRENT_LEVEL)
    if(UPDATE_TIMER == true) { current_prog -= timer_speed }
  }
  ...
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

current_prog مقدار درصدی را نشان می دهد که از 1 شروع می شود و به عقب شمارش می کند. پس از رسیدن به 0، بلوک به طور خودکار حذف می شود و اگر بازیکن سریعتر بلوک را رها کند، با فاکتور کردن پیشرفت فعلی تایمر، امتیاز بالاتری دریافت می کند.

و این در مورد آن است! بخشی از من می خواهد در مورد آن صحبت کند rotate روشی که قبلاً با جزئیات ذکر کردم، اما این مقاله در حال حاضر به اندازه کافی طولانی است، بنابراین من فقط موارد را در اینجا جمع بندی می کنم. با تشکر برای خواندن!

اولین چیزی که می خواهم بگویم این است که روشی که من این را نوشتم یک روش معمولی برای نوشتن جاوا اسکریپت نیست. من در واقع هیچ کلاسی ننوشتم (من فقط همه توابع را در فایل های خود سازماندهی کردم gravity.js، flare.js، mapping.js، و غیره.). من هم متغیرهای سراسری زیادی دارم 😬. هیچ راهی وجود ندارد که هنگام کار با یک تیم از این رویکرد استفاده کنم، اما این یک پروژه شخصی بود که من فقط می خواستم به اصطلاح “روی کاغذ بیاورم” و فکر می کنم اگر دوباره آن را بسازم، آن را انجام خواهم داد. در یونیتی با اصول برنامه نویسی بسیار بهتر.

با این اوصاف، این یک کد بسیار بد است که من نوشتم…

function togglePauseMenu() {
  if(GAME_PAUSED) {
    GAME_PAUSED = false
  } else {
    GAME_PAUSED = true
  }
  ...
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

به طور جدی؟ شاید بهتر باشد به جای نوشتن با این نحو فکر کنم:

توضیحات تصویر

این یکی شبیهه

if(level_to_adjust > 12) {
  return 0
} else if (level_to_adjust < 1) {
  return 0
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این مورد بعدی یک باگ UI است که هنگام تلاش برای انتقال کل بلوک به گوشه ای از صفحه بازی اتفاق می افتد و من هنوز از نظر فنی آن را برطرف نکرده ام … اما کار می کند؟ بنابراین فعلاً به آن دست نمی زنم. فوق العاده شکننده 😅

// Redraw!
// Again, this is really hacky, but it works.
// ¯\_(ツ)_/¯
var reverse_direction = direction == "clockwise" ? "counter-clockwise" : "clockwise"
rotate(direction, star_cluster, star_cluster_type)
moveAlongCorona(direction, star_cluster, star_cluster_type)
moveAlongCorona(reverse_direction, star_cluster, star_cluster_type)
rotate(reverse_direction, star_cluster, star_cluster_type)
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بنابراین وجود دارد که! من چیزهای زیادی یاد گرفتم و هنوز چیزهای بیشتری برای یادگیری وجود دارد، اما این یک پروژه بسیار سرگرم کننده بود و من مجبور شدم یک بازی بسازم! مطمئن نیستم که روزی آن را در یونیتی بازسازی کنم یا نه، اما تجربه جالبی بود، و خوشحالم که توانستم آن را بسازم.

ممنون که خواندید!

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا