iAct

Act on Ai

ساخت نمونهٔ اولیهٔ بازی استراتژیک در Godot با کمک هوش مصنوعی


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

۱. ایدهٔ بزرگ را به یک بازی قابل‌آزمایش تبدیل کن

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

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

۲. پروژهٔ Godot را با صحنه و گره‌های مشخص بساز

Godot 4 را نصب کن و یک پروژهٔ تازه بساز؛ صحنهٔ دوبعدی را در مرحلهٔ بعد با ریشهٔ Node2D ایجاد می‌کنی. اگر محیط توسعه یا عامل هوش مصنوعی نصب ابزار را پیشنهاد داد، مسیر نصب و فایل‌هایی را که تغییر می‌دهد خودت بررسی کن؛ نصب موفق را با باز شدن پروژه در ویرایشگر بسنج، نه صرفاً با پیام عامل. برای آشنایی با محیط و مفاهیم پایه، مستندات رسمی Godot مرجع مناسبی است.

در Godot، صحنه مجموعه‌ای از گره‌هاست که ذخیره و اجرا می‌شود. گره یک وظیفه دارد؛ مثلاً Camera2D نمای بازیکن را کنترل می‌کند. اسکریپت رفتاری است که به گره وصل می‌کنی. یک صحنهٔ Main.tscn بساز، ریشهٔ آن را Node2D و نامش را Game بگذار، سپس یک Camera2D با نام دقیق Camera2D زیر آن اضافه کن. Main.tscn را در ریشهٔ پروژه ذخیره کن و در Project > Project Settings > Application > Run > Main Scene آن را صحنهٔ اصلی قرار بده؛ برای اجرای پروژه F5 را بزن. پوشهٔ scripts را نیز در ریشه بساز؛ دو فایل کد مرحله‌های بعد آنجا قرار می‌گیرند.

این چیدمان عمداً کوچک است. نقشه و قواعد بازی در Game می‌مانند و حرکت دوربین در Camera2D. پس اگر بعداً زوم خراب شد، می‌دانی ابتدا کدام اسکریپت را بررسی کنی. در پایان این مرحله باید بتوانی صحنهٔ خالی را اجرا کنی، حتی اگر هنوز چیزی روی صفحه رسم نمی‌شود.

۳. نقشه، دوربین و ورودی را از هم تفکیک کن

نقشهٔ دوبعدی را می‌توان به خانه‌های شبکه تقسیم کرد. در نمونهٔ آموزشی ما هر خانه ۴۸ پیکسل است و نقشه ۱۲ ستون و ۸ ردیف دارد. مختصات خانه، مثلاً (3, 3)، با موقعیت پیکسلی فرق دارد؛ همین تفاوت بعداً برای جلوگیری از ساخت روی یک خانهٔ اشغال‌شده مهم می‌شود.

فایل res://scripts/camera.gd را بساز و به گره Camera2D وصل کن. این اسکریپت فقط با کلیدهای جهت‌دار نما را جابه‌جا می‌کند و با چرخ ماوس زوم را تغییر می‌دهد:

extends Camera2D

const SPEED := 320.0

func _process(delta: float) -> void:
    var direction := Vector2.ZERO
    if Input.is_key_pressed(KEY_LEFT):
        direction.x -= 1.0
    if Input.is_key_pressed(KEY_RIGHT):
        direction.x += 1.0
    if Input.is_key_pressed(KEY_UP):
        direction.y -= 1.0
    if Input.is_key_pressed(KEY_DOWN):
        direction.y += 1.0
    position += direction.normalized() * SPEED * delta

func _unhandled_input(event: InputEvent) -> void:
    if event is InputEventMouseButton and event.pressed:
        if event.button_index == MOUSE_BUTTON_WHEEL_UP:
            zoom = Vector2.ONE * minf(zoom.x * 1.1, 2.0)
        elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
            zoom = Vector2.ONE * maxf(zoom.x / 1.1, 0.6)

موقعیت اولیهٔ Camera2D را در Inspector روی (288, 192) بگذار؛ این تقریباً مرکز نقشهٔ نمونه است. دوربین فعلاً به مرزهای نقشه محدود نشده، پس می‌توانی آن را از نقشه بیرون ببری. این نقصِ پنهان نیست: پیش از اضافه‌کردن محدودیت، می‌خواهیم مطمئن شویم خود حرکت و زوم کار می‌کنند. پس از رسم نقشه در مرحلهٔ بعد، باید با کلیدهای جهت‌دار نمای آن تغییر کند و با چرخ ماوس بزرگ‌نمایی عوض شود.

۴. انتخاب واحد، منبع و ساخت را در یک نمونهٔ کوچک وصل کن

حالا فایل res://scripts/game.gd را بساز و به گره ریشهٔ Game وصل کن. کد زیر مثال تکمیلی آموزشی است، نه کد نقل‌شده از پروژهٔ ضبط‌شده. برای اینکه بدون فایل تصویر یا افزونه اجرا شود، همه‌چیز را با شکل‌های ساده رسم می‌کند. فرض می‌کند نام گره‌ها و مسیر فایل‌ها دقیقاً مطابق مرحله‌های قبل‌اند.

extends Node2D

const TILE := 48
const SIZE := Vector2i(12, 8)
const BASE := Vector2i(1, 1)
const TREE := Vector2i(3, 3)

var unit_cell := Vector2i(2, 1)
var selected := false
var building_mode := false
var wood := 0
var buildings: Array[Vector2i] = []

func _ready() -> void:
    print("روی واحد کلیک کن؛ راست‌کلیک: حرکت؛ روی درخت: چوب؛ B: ساخت")
    print("چوب: ", wood)
    queue_redraw()

func _draw() -> void:
    for y in range(SIZE.y):
        for x in range(SIZE.x):
            var cell := Vector2i(x, y)
            var rect := Rect2(Vector2(cell * TILE), Vector2(TILE - 1, TILE - 1))
            var color := Color.DARK_OLIVE_GREEN if (x + y) % 2 == 0 else Color.OLIVE_DRAB
            draw_rect(rect, color)
    _draw_cell(BASE, Color.SLATE_GRAY)
    _draw_cell(TREE, Color.FOREST_GREEN)
    for cell in buildings:
        _draw_cell(cell, Color.SADDLE_BROWN)
    draw_circle(_center(unit_cell), 12.0, Color.GOLD if selected else Color.WHITE)

func _draw_cell(cell: Vector2i, color: Color) -> void:
    draw_rect(Rect2(Vector2(cell * TILE) + Vector2(6, 6), Vector2(36, 36)), color)

func _center(cell: Vector2i) -> Vector2:
    return Vector2(cell * TILE) + Vector2.ONE * (TILE / 2.0)

func _inside(cell: Vector2i) -> bool:
    return cell.x >= 0 and cell.y >= 0 and cell.x < SIZE.x and cell.y < SIZE.y

func _unhandled_input(event: InputEvent) -> void:
    if event is InputEventKey and event.pressed and not event.echo:
        if event.keycode == KEY_B:
            building_mode = not building_mode
            print("حالت ساخت: ", building_mode, " | چوب: ", wood)
        return

    if not (event is InputEventMouseButton and event.pressed):
        return
    if event.button_index != MOUSE_BUTTON_LEFT and event.button_index != MOUSE_BUTTON_RIGHT:
        return

    var cell := Vector2i(floori(get_global_mouse_position().x / TILE),
        floori(get_global_mouse_position().y / TILE))
    if not _inside(cell):
        return

    if event.button_index == MOUSE_BUTTON_LEFT:
        if building_mode:
            if wood < 2 or cell == BASE or cell == TREE or cell == unit_cell or cell in buildings:
                print("ساخت ممکن نیست: چوب کم است یا خانه اشغال شده.")
            elif abs(cell.x - BASE.x) + abs(cell.y - BASE.y) > 3:
                print("ساختمان باید نزدیک پایگاه باشد.")
            else:
                wood -= 2
                buildings.append(cell)
                building_mode = false
                print("ساختمان ساخته شد. چوب: ", wood)
        else:
            selected = cell == unit_cell
            print("واحد انتخاب شد." if selected else "انتخاب برداشته شد.")
    elif selected and cell != BASE and cell not in buildings:
        unit_cell = cell
        if cell == TREE:
            wood += 1
            print("یک چوب جمع شد. چوب: ", wood)
    queue_redraw()

پروژه را اجرا کن و پنل Output ویرایشگر را باز نگه دار. روی دایرهٔ واحد کلیک کن و دو بار روی خانهٔ درخت راست‌کلیک کن تا موجودی چوب به ۲ برسد؛ در این نمونه لازم نیست واحد را بین دو کلیک از خانهٔ درخت بیرون ببری. کلید B را بزن و روی خانه‌ای خالی نزدیک پایگاه کلیک کن. باید یک مربع قهوه‌ای ببینی و موجودی دوباره صفر شود. اگر فقط صحنهٔ خالی می‌بینی، اتصال اسکریپت game.gd، انتخاب Main.tscn به‌عنوان صحنهٔ اصلی و پیام‌های خطای Output را بررسی کن.

۵. فرق «نمونهٔ تعاملی» و «سیستم بازی» را جدی بگیر

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

در یک بازی استراتژیک واقعی‌تر، منبع باید ظرفیت یا نرخ تولید مشخص داشته باشد، واحد به مقصد برسد و تازه بعد از انجام کار، موجودی تغییر کند. ساخت نیز باید مالک، هزینه و محدودهٔ مجاز داشته باشد. در این نمونه، قاعدهٔ «نزدیک پایگاه» با فاصلهٔ خانه‌ای سنجیده می‌شود؛ این فقط یک محدودیت ساده برای جلوگیری از ساخت دلخواه در سراسر نقشه است. نتیجهٔ این مرحله یک قرارداد روشن است: دقیقاً می‌دانی کدام رفتارها پیاده شده‌اند و کدام رفتارها هنوز جایگزین موقت دارند.

۶. از هوش مصنوعی تغییر کوچک و قابل‌سنجش بخواه

وقتی پروژهٔ کوچک اجرا شد، می‌توانی از عامل برنامه‌نویسی کمک بگیری؛ اما به جای درخواست «یک بازی کامل بساز»، یک مسئلهٔ محدود به آن بده. متن زیر بازنویسی آموزشی پرامپت است و قرار نیست نقل‌قولی از درخواست‌های پروژهٔ اصلی باشد:

در پروژهٔ Godot 4 من، صحنهٔ Main.tscn یک Game از نوع Node2D
و یک Camera2D فرزند دارد. game.gd خانه‌های نقشه، واحد، درخت،
چوب و ساختمان را کنترل می‌کند؛ camera.gd فقط حرکت نما و زوم را.

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

این درخواست هم محدودهٔ فایل‌ها را روشن می‌کند، هم معیار پذیرش دارد: چوب پیش از رسیدن نباید زیاد شود. بعد از اعمال تغییر، پروژه را اجرا کن و همان مسیر انتخاب واحد تا برداشت چوب را تکرار کن. علاوه بر قابلیت جدید، ساخت بنا و زوم را هم امتحان کن تا مطمئن شوی چیزی که قبلاً کار می‌کرد از دست نرفته است. اگر عامل نصب، اجرای فرمان یا تغییر گستردهٔ فایل‌ها را پیشنهاد داد، اثر آن را پیش از تأیید بررسی کن؛ گزارش «انجام شد» جای آزمون دستی را نمی‌گیرد.

۷. نقشهٔ بزرگ‌تر، دوربین و رابط کاربری را با هم قاطی نکن

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

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

۸. دیوار، دروازه و دشمن را با آزمون مسیر بسنج

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

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

۹. برای ادامهٔ توسعه، یک نسخهٔ قابل‌تحویل نگه دار

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

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

ویدیوی کامل

اگر می‌خواهی روند کار و آزمون‌ها را هم ببینی، ویدیوی کامل این آموزش اینجاست:

تماشای ویدیو در یوتیوب · گروه گفت‌وگو و پرسش‌های آموزشی


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