برنامه نویسی

ایمنی ، تکرارهای سفارشی و ساختارهای زمان اجرا را در دنیای واقعی تایپ کنید

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

امروز یک مرور کلی از ساختار داده ها و اشیاء مورد استفاده برای ایجاد سیستم prefab ارائه می دهم. در این مقاله به بررسی ایجاد تکرارهای سفارشی برای ایجاد حلقه های مبتنی بر دامنه می پردازیم ، چگونه ایمنی نوع مانع از مخلوط کردن بین کلیدهای مختلف (UUID) و نحوه نزدیک شدن به اشیاء JSON مانند در C ++ می شود. سرانجام تصمیمی را که از آن ناراضی بودم به اشتراک می گذارم و با پشتوانه ای که امروز دارم ، متفاوت می شوم. بشکه ای از نوشیدنی مورد علاقه خود را بگیرید و برای یادگیری چیز جدید آماده شوید!

خوشه های گره

من نمی خواهم سازنده آهنگ ویرایشگر عمومی باشد ، شما سلسله مراتب معمولی صحنه-گراف / شی را می دانید. من فکر من می توانم یک گردش کار بهتری را برای طراحی یک مسابقه اتومبیلرانی بدون آن ایجاد کنم ، اما با افزودن ویژگی ها ، جریان کاملاً ناخوشایند شد و در حال کار و همچنین اولین ماشین من بود. استفاده از وحدت برای تخم مرغ! سریال مرا متقاعد کرد که یک سیستم prefab بسازم. مقاله مقدمه را بخوانید تا اطلاعات بیشتری در مورد سیستم و چرا من آن را ساختم.

موتور بازی هیچ سیستم مؤلفه ای (ECS) ساخته شده در آن ندارد ، یا حداقل وقتی کار خود را روی سیستم prefab شروع کردم ، حداقل انجام ندادم. من در پایان مقاله بیشتر به این موضوع شیرجه می زنم اما منجر به تصمیماتی شد که بعداً پشیمان شدم. برای شروع کار “نهادهای” من “گره” نامیده می شوند و اساساً آنها یک نام ، موقعیت و شاید یک گره والدین دارند. هرگونه رفتار یا ویژگی های دیگر توسط مؤلفه هایی که به گره وصل شده اند تعریف می شود.

TrackBuilder گره ها را در یک NodeCluster که حاوی ؛

class NodeCluster; //Forward-declaration for PrefabTable
using ComponentContainer = std::vector;
using NodeHierarchyTable = std::unordered_map;
using NodeComponentsTable = std::unordered_map;
using PrefabTable = std::unordered_map;

class NodeCluster {
public:
    NodeHierarchyTable mNodeHierarchy;
    NodeComponentsTable mNodeComponents;
    DataResourceTable mResourceTable;
    PrefabTable mPrefabTable;
    ResourceKey mResourceKey;
    NodeKey mRootNodeKey;
};
حالت تمام صفحه را وارد کنید

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

این زمان وحشتناکی برای ذکر استاندارد C ++ نیست که به همه ظروف برای کار با انواع اعلام شده رو به جلو نیاز ندارد. GCC 11 همانطور که در Reddit به اشتراک گذاشتم ، اعلامیه prefabtable را در بالا دوست نداشت.

در واقع پیچیدگی های زیادی برای باز کردن نمونه در نمونه فوق وجود دارد ، در اصل درختان درختان درختان در تمام طول راه است. یک prefab به سادگی یک nodecluster است همانطور که می توانید از اعلامیه prefabtable در بالا مشاهده کنید. یک NodeCluster همچنین نشان دهنده ساختار کامل یا ساختار سطح است و هنگامی که یک گره در داخل (هر) خوشه ای دارای یک پیشانی معتبر است ، خواص را از پیش نمایش مورد نظر به آن کپی می کند ، مگر اینکه این خاصیت ناعادلانه باشد.

“اگر نمی فهمید ، نگران نباشید – من این سیستم را با درک به سختی عملکردی از آن نوشتم.”

نمونه ای از prefab گره ای است که در واقع وجود دارد که به سمت پیش نمایش برای کپی کردن اشاره می کند. این نمونه می تواند در داخل پیش نمایش دیگری وجود داشته باشد ، مانند من می توانم یک grandstand.pfab که حاوی دو گره است. بوها flag و الف spectator، جایی که آن تماشاگر نمونه ای از spectator.pfabبشر البته مادربزرگ می تواند نمونه ای در خود باشد و به سرعت شروع به دیدن پیچیدگی می کنید. وقتی طراح رنگ پیراهن را در spectator.pfab، هر چیز دیگری باید به روز شود مگر اینکه در جای دیگر زنجیره ای نادیده گرفته شد.

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

ایجاد UUID های نوع ایمن برای جلوگیری از تصادفات

از مرور کلی می توانید نقش “کلیدها” را در این سیستم بازی می کنند تا عناصر مختلف را به صورت منحصر به فرد شناسایی کنند. همه کلیدها uuid هستند اما نباید مخلوط یا مخلوط شوند. ما نمی خواهیم اشتباه بگیریم NodeKey برای ResourceKey یا چنین با استفاده از نوع ایمنی از نوع ، کامپایلر می تواند از ما محافظت کند! اصطلاح “کلید” برای زیبایی شناسی و استانداردهای کد نویسی من استفاده می شود ، ComponentKey احساس خیلی بهتر از ComponentIDبشر Sole :: UUID برای جلوگیری از اجرای جزئیات خاص UUID استفاده شد.

using NodeKey = sole::uuid;
using ComponentKey = sole::uuid;
using ResourceKey = sole::uuid;
// continued for other Key types
حالت تمام صفحه را وارد کنید

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

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

void FindNode(NodeKey& nodeKey) { /* do stuff */ }

// Later, somewhere in code:
{
    ComponentKey key = ComponentKey::uuid4();
    FindNode(key);
}
حالت تمام صفحه را وارد کنید

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

از آنجا که ساختن یک بسته بندی در اطراف کد خارجی یک ایده معقول است ، ما می توانیم آن را با یک الگوی برای ایجاد محافظت از نوع ایمنی از نوع اضافی ترکیب کنیم که تضمین می کند NodeKey با یک متفاوت است ComponentKey حتی اگر هر دو uuid زیر کاپوت مانند چنین باشند ؛

template class MyUUID {
public:
    //wrapper API here
private:
    sole::uuid id
};
حالت تمام صفحه را وارد کنید

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

و نوع نام مستعار ما را به ؛

enum class NodeKeyType { };
using NodeKey = MyUUID;

enum class ComponentKeyType { };
using ComponentKey = MyUUID;

enum class ResourceKeyType { };
using ResourceKey = MyUUID;

// continued for other Key types
حالت تمام صفحه را وارد کنید

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

این باعث می شود مثال قبلی در کامپایل نتواند ، زیرا NodeKey وت ComponentKey انواع متفاوت هستند. من از کامپایلر لذت می برم که وقتی کارهای غیر منتظره ای انجام می دهم.

تکرارهای سفارشی برای بازگرداندن درخت قابل نگهداری

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

int CalculateSum(int number) {
    if (0 == number) { return 0; } //The end case
    return number + CalculateSum(number - 1); // Recursive case
}
حالت تمام صفحه را وارد کنید

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

برای بازگشت درختان بسیار مفید است ، مانند NodeHierarchyبشر یک راه ساده و سریع به RescurseTree() به روش عمومی استفاده از پاسخ به تماس عملکردی است که برای هر گره فراخوانی می شود. موارد زیر با من متفاوت است Node مطالب اما متناسب با درخت مشترک بهتر است.

void RecurseTree(Node& node, std::function callback) {
    callback(node);
    for (Node& child : node.mChildren) {
        RecurseTree(child, callback);
    }
}
حالت تمام صفحه را وارد کنید

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

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

RecurseTree(rootNode, [](Node& node) {
    if (true == node.IsPrefabInstance()) {
        return;
    }

    node.DoSomething();
});
حالت تمام صفحه را وارد کنید

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

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

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

for (Node& node : RecurseTree(rootNode)) {
    if (true == node.IsPrefabInstance()) {
        continue;
    }

    node.DoSomething();
});
حالت تمام صفحه را وارد کنید

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

تفاوت آن اندک است ، به خصوص هنگامی که مانند این مثالها مشخص شده است. من نیازی به نوشتن یک تابع لامبدا یا نگرانی در مورد چگونگی continue یا break ممکن است کار کند این فقط انجام می دهد نزولی ، در صورت وجود ، پیچیدگی از کد مدیریت گره ها به داخل منتقل شد RecurseTree()بشر آنچه در زیر رویکردی است که من انجام دادم ؛

struct RangedBasedTree {
    typedef std::vector<:reference_wrapper>> ContainerType;
    ContainerType mRecursedNodes;

    RangedBasedTree(const Node& node) {
        RecurseTree(node, [this](Node& node) {
            mRecursedNodes.emplace_back(node);
        });
    }

    typename ContainerType::iterator begin(void) {
        return mRecursedNodes.begin();
    }

    typename ContainerType::iterator end(void) {
        return mRecursedNodes.end();
    }
};

RangedBasedTree RecurseTree(Node& node) {
    return RangedBasedTree(node);
}
حالت تمام صفحه را وارد کنید

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

من از صحت پشتیبانی پشتیبانی به عنوان یک تمرین برای شما می گذارم ، به عنوان یک اشاره ممکن است شامل قالب بندی باشد RangedBasedTree در حین استفاده std::conditional وت std::is_const… در عوض ، بیایید به نحوه ذخیره خصوصیات سفارشی برای اجزای موجود در هنگام ناشناخته بودن انواع در زمان کامپایل ، شیرجه بزنیم.

ساختارهای تعریف شده در زمان اجرا

مدتها پیش مقاله ای را خواندم یا شاید یکی از کتابهای استیو مایرز C ++ ، آرزو می کنم به یاد داشته باشم ، و الهام گرفته شدم تا یک نوع ساختار نیمه ایمن اما تعریف شده پویا ایجاد کنم ، DynamicStructureبشر بهترین راه من برای توصیف این موضوع استفاده از “JSON” مانند اشیاء در کد C ++ است. مطمئناً شما انواع دقیق را در زمان کامپایل نمی دانید ، از این رو نیمه ایمن از نوع ، اما اجرای من می تواند استثنائاتی را در هنگام عدم تطابق یا تبدیل ضمنی انجام دهد.

با گذشت سالها من به طور معمول از DynamicStructure برای بارگیری پرونده هایی مانند JSON ، YAML و غیره ، دست زدن به داده ها از درخواست های شبکه و اخیراً نگه داشتن ویژگی ها برای Component در سیستم prefab. مؤلفه ها دارای یک نوع و تعریف هستند اما برای ردیابی سازنده ناشناخته هستند ، زیرا اجزای قابل تنظیم به یک بازی هستند.

class DynamicStructure {
public:
    using String = std::string;
    using Array = std::vector;
    using Struct = std::unordered_map;

private:
    enum DynamicStructureValueType : uint8_t {
        kNil, kInteger, kFloat, kBoolean, kString, kArray, kStructure
    };

    DynamicStructureValueType mValueType;
    union { //unnamed anonymous union for access like this->mFloat;
        char mRawBytes[8];

        int64_t mInteger;
        bool mBoolean;
        float mFloat;

        String* mString;
        Array* mArray;
        Struct* mStructure;
    };
};
حالت تمام صفحه را وارد کنید

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

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

بازگرداندن این به سیستم گره / کامپوننت ، همانطور که گفته شد سازنده آهنگ قبلی نمی تواند خواص a را بشناسد Component در زمان کامپایل زیرا می توان آن نوع را با بازی تعریف کرد. این تعاریف یک پرونده JSON است. شخصی جاه طلب تر احتمالاً می تواند ابرداده را در کد بازی با مراحل پیش ساخت برای تولید فایل ComponentDefinitions اضافه کند ، اما ایجاد و نگهداری یک فایل JSON تلاش بسیار کمتری است.

{
    "version":2,
    "components":[
        {
            "display_name": "Hammer Obstacle",
            "type_key":"510fa5af-b454-41a3-9780-2c578a3cf645",
            "properties":[
                { "name":"timeout", "type":"float", "default":5.0 },
                { "name":"shockwave", "type":"boolean", "default":false },
                { "name":"shockwave_radius", "type":"float", "default":15.0 },
                { "name":"damage", "type":"integer", "default":25 }
            ]
        }
    ]
}
حالت تمام صفحه را وارد کنید

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

این مؤلفه به چکش هایی که در اتومبیل ها سقوط می کنند متصل می شود و باعث آسیب و شاید یک موج شوک می شود که آسیب جزئی را وارد می کند. از آنجا که TrackBuilder هیچ یک از این موارد را نمی دانست ، در زمان کامپایل ، خواص در یک ذخیره می شوند DynamicStructure و “فقط کار می کند”.

چگونه یک سلسله مراتب گره را ذخیره نکنیم

همه چیز فقط وقتی با پروژه های بزرگ مقابله می کنیم کار نمی کند ، و یکی از مهمترین نقاط درد هنگام کار از طریق سیستم prefab این بود که من داده ها را سازماندهی کردم. به طور خاص تکثیر یک گره/مؤلفه (داده ویرایشگر) در مقابل گره/مؤلفه (موتور در موتور). این سریال فقط به بررسی سمت داده می پردازد. در حین ساختن سیستم ، گذشته من با این فرض ساخته شده است که موتور نمی تواند یا دارای EC باشد.

عادلانه بودن در آن زمان موتور واقعاً این پشتیبانی را نداشت. گذشته من تصور می کردیم که می توانیم مزایای کار با “گره ها” و “مؤلفه ها” را در ویرایشگر بدست آوریم بدون الزامات بیشتر Past-Me همچنین امیدوار بود که برای ذخیره/بارگذاری بدون هیچ گونه پردازش یا تغییر اندازه پویا ، بلوک حافظه را به دیسک ریخته و بارگیری کند. همانطور که این تصمیمات گرفته شد ، NodeHierarchy یک ساختار مسطح بود ، به سادگی std::vector، که همه مزایای آن بعداً حل شدند.

با توجه به اینکه قطعات دارای خواص سفارشی بودند ، این همه ایده بسیار احمقانه ای بود و بخش هایی از این مورد دوباره مورد استفاده قرار گرفته است ، اما میراث کروفت ضخیم است و توابع ایجاد شده مانند ؛

NodeKeyContainer GetChildren(const NodeKey& nodeKey,
    const NodeHierarchyTable& nodes)
{
    NodeKeyContainer children;
    for (const auto& nodeIterator : nodeHierarchy)
    {
        if (nodeIterator.second.mParentNodeKey == nodeKey)
        {
            children.push_back(nodeIterator.second.mNodeKey);
        }
    }
    return children;
}
حالت تمام صفحه را وارد کنید

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

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

شرح تصویر

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

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

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

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

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