في هذه المقالة ، سأخبرك عن كيفية إنشاء بوابات في Unreal Engine 4. لم أجد أي مصادر تصف مثل هذا النظام بالتفصيل (الملاحظة من خلال البوابات والمرور عبرها) ، لذلك قررت أن أكتب بلدي.
ما هي البوابة؟
لنبدأ بأمثلة وتوضيحات حول ماهية البوابة الإلكترونية. أسهل طريقة لوصف البوابات كوسيلة للمرور من مساحة إلى أخرى. في بعض الألعاب الشائعة ، يتم استخدام هذا المفهوم للتأثيرات المرئية وحتى لميكانيكا اللعب:
أمثلة بوابة اللعبة (GIF)Antichamber (2013) و Portal (2007)فري ، 2006 من بين الألعاب الثلاث ، ربما تكون البوابة الأكثر شهرة ، لكنني شخصياً أعجبت بري دائماً وكانت هي التي أردت نسخها. بمجرد أن حاولت تطبيق الإصدار الخاص بي في Unreal Engine 4 ، لكنني لم أفلح حقًا ، لأن المحرك يفتقر إلى الوظائف. ومع ذلك ، تمكنت من إجراء هذه التجارب:
ومع ذلك ، فقط في الإصدارات الجديدة من Unreal Engine تمكنت أخيرًا من تحقيق التأثير المطلوب:
بوابات - كيف تعمل؟
قبل متابعة التفاصيل ، دعونا نلقي نظرة على الصورة العامة لكيفية عمل البوابات.
في الواقع ، المدخل هو نافذة لا تخرج عن الأنظار ، ولكن إلى مكان آخر ، أي أننا نضع وجهة نظر محلية معينة بالنسبة للكائن ونكرر وجهة النظر هذه في مكان آخر. باستخدام هذا المبدأ ، يمكننا توصيل مسافتين ، حتى لو كانت بعيدة جدًا عن بعضها البعض. تشبه النافذة قناعًا يتيح لنا معرفة مكان وزمان عرض مساحة أخرى بدلاً من المساحة الأصلية. نظرًا لتكرار نقطة الانطلاق في أي مكان آخر ، فإن هذا يعطينا وهم الاستمرارية.
في هذه الصورة ، يقع جهاز الالتقاط (SceneCapture في UE4) أمام المساحة التي تتوافق مع المساحة المرئية من وجهة نظر اللاعب. يتم استبدال كل ما هو مرئي بعد السطر بما يمكن أن يراه الالتقاط. نظرًا لوجود جهاز الالتقاط بين الباب والكائنات الأخرى ، من المهم استخدام ما يسمى "طائرة القطع". في حالة موقع البوابة ، نريد أن تقوم طائرة القطع القريبة بإخفاء الكائنات المرئية أمام البوابة.
لتلخيص. نحتاج:
- موقع اللاعب
- بوابة دخول نقطة
- بوابة الخروج نقطة
- جهاز القطع مع طائرة القطع
كيفية تنفيذ هذا في محرك غير واقعي؟
لقد بنيت نظامي على أساس فئتين رئيسيتين
تديرهما PlayerController وشخصية . فئة
Portal هي نقطة دخول حقيقية للبوابة ، تكون نقطة العرض / الخروج الخاصة بها هي الفاعل المستهدف. هناك أيضًا
Portal Manager ، الذي يتم إنشاؤه بواسطة PlayerController ويتم تحديثه بواسطة Character لإدارة كل بوابة على المستوى وتحديثها ، وكذلك لمعالجة كائن SceneCapture (وهو أمر شائع في جميع البوابات).
ضع في اعتبارك أن البرنامج التعليمي يتوقع منك الوصول إلى فصول الأحرف و PlayerController من التعليمات البرمجية. في حالتي ، يطلق عليهم ExedreCharacter و ExedrePlayerController.
إنشاء فئة ممثل البوابة
لنبدأ مع ممثل البوابة ، والذي سيتم استخدامه لتعيين "النوافذ" التي سننظر من خلالها إلى المستوى. مهمة الممثل هي تقديم معلومات عن اللاعب لحساب المواقف والمنعطفات المختلفة. سيشارك أيضًا في التعرف على ما إذا كان اللاعب يعبر البوابة أم لا.
قبل البدء في مناقشة مفصلة للممثل ، اسمحوا لي أن أشرح بعض المفاهيم التي قمت بإنشائها لإدارة نظام البوابة:
- لرفض العمليات الحسابية بشكل ملائم ، تتمتع البوابة بحالة نشطة. يتم تحديث هذه الحالة بواسطة Portal Manager.
- تحتوي البوابة على جانبين أماميين وخلفيين يحددهما موقعها واتجاهها (الموجه الأمامي).
- لمعرفة ما إذا كان اللاعب يعبر البوابة ، يقوم بتخزين الموضع السابق للاعب ومقارنته بالموقع الحالي. إذا كان اللاعب في التدبير السابق أمام البوابة ، وفي التيار - خلفه ، فإننا نعتقد أن اللاعب تجاوزه. يتم تجاهل السلوك العكسي.
- يحتوي المدخل على حجم محدد ، حتى لا يؤدي العمليات الحسابية ويتحقق حتى يكون المشغل في هذا المجلد. مثال: تجاهل التقاطع إذا كان المشغل لا يلمس البوابة فعليًا.
- يتم حساب موقع اللاعب من موقع الكاميرا لضمان السلوك الصحيح عندما تعبر وجهة النظر البوابة ولكن ليس جسم اللاعب.
- يستقبل المدخل Render Target ، الذي يعرض وجهة نظر مختلفة في كل قياس في حال كان الملمس في المرة القادمة غير صحيح ويحتاج إلى استبداله.
- تخزن البوابة رابطًا إلى ممثل آخر يدعى Target ، من أجل معرفة مكان الاتصال بالمساحة الأخرى.
باستخدام هذه القواعد ، قمت بإنشاء فئة ExedrePortal جديدة موروثة من AActor كنقطة انطلاق. هنا هو عنوانها:
#pragma once #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "ExedrePortal.generated.h" UCLASS() class EXEDRE_API AExedrePortal : public AActor { GENERATED_UCLASS_BODY() protected: virtual void BeginPlay() override; public: virtual void Tick(float DeltaTime) override;
كما ترون ، هناك معظم السلوكيات الموضحة هنا. الآن دعونا نرى كيف تتم معالجتها في الجسم (.cpp).
المصمم هنا يعد مكونات الجذر. قررت إنشاء مكونين أساسيين ، لأن الفاعل المدخل سوف يجمع بين كل من التأثيرات الرسومية والتصادمات / الاعتراف. لذلك كنت بحاجة إلى طريقة بسيطة لتحديد مكان وجود نافذة / بوابة ، دون الحاجة إلى ميزات البلوتوث أو غيرها من الحيل. سيكون PortalRootComponent هو الأساس لجميع العمليات المتعلقة بالبوابة.
يتم تعيين جذر موقع البوابة على ديناميكي ، في حالة قيام فئة Blueprint بتنشيطه (على سبيل المثال ، يستخدم حركة فتح / إغلاق).
لا يوجد سوى وظائف Get و Set ، ولا شيء أكثر من ذلك. سندير حالة النشاط من مكان آخر.
bool AExedrePortal::IsActive() { return bIsActive; } void AExedrePortal::SetActive( bool NewActive ) { bIsActive = NewActive; }
أحداث المخطط ، أنا لا أفعل أي شيء في فئة C ++.
void AExedrePortal::ClearRTT_Implementation() { } void AExedrePortal::SetRTT_Implementation( UTexture* RenderTexture ) { } void AExedrePortal::ForceTick_Implementation() { }
إحضار وضبط وظائف للممثل الهدف. لا يوجد شيء أكثر تعقيدًا في هذا الجزء أيضًا.
AActor* AExedrePortal::GetTarget() { return Target; } void AExedrePortal::SetTarget( AActor* NewTarget ) { Target = NewTarget; }
من خلال هذه الوظيفة ، يمكننا بسهولة التحقق مما إذا كانت هناك نقطة أمام الطائرة ، وفي حالتنا هذه بوابة. تستخدم الوظيفة بنية FPlane لمحرك UE4 لإجراء العمليات الحسابية.
bool AExedrePortal::IsPointInFrontOfPortal( FVector Point, FVector PortalLocation, FVector PortalNormal ) { FPlane PortalPlane = FPlane( PortalLocation, PortalNormal ); float PortalDot = PortalPlane.PlaneDot( Point );
تتحقق هذه الوظيفة لمعرفة ما إذا كانت النقطة قد عبرت مستوى بوابة المدخل. هنا نستخدم الموقف القديم لمعرفة كيف تتصرف النقطة. هذه الوظيفة شائعة بحيث يمكنها العمل مع أي ممثل ، ولكن في حالتي يتم استخدامها فقط مع اللاعب.
تقوم الوظيفة بإنشاء اتجاه / قطعة بين الموقع السابق والحالي ، ثم تتحقق مما إذا كانت تتقاطع مع المستوى أم لا. إذا كان الأمر كذلك ، فإننا نتحقق مما إذا كانت تعبر في الاتجاه الصحيح (من الأمام إلى الخلف؟).
bool AExedrePortal::IsPointCrossingPortal( FVector Point, FVector PortalLocation, FVector PortalNormal ) { FVector IntersectionPoint; FPlane PortalPlane = FPlane( PortalLocation, PortalNormal ); float PortalDot = PortalPlane.PlaneDot( Point ); bool IsCrossing = false; bool IsInFront = PortalDot >= 0; bool IsIntersect = FMath::SegmentPlaneIntersection( LastPosition, Point, PortalPlane, IntersectionPoint );
ممثل النقل الفضائي
الجزء الأخير من ممثل البوابة الذي سننظر إليه هو وظيفة
TeleportActor () .
عند النقل عن بُعد لأحد الممثلين من النقطة A إلى النقطة B ، فأنت بحاجة إلى تكرار حركته وموقعه. على سبيل المثال ، إذا دخل اللاعب إلى البوابة ، ثم بالاقتران مع المؤثرات المرئية المناسبة ، سيظهر له أنه دخل من باب عادي.
يبدو تقاطع البوابة وكأنه يتحرك في خط مستقيم ، ولكن في الواقع يحدث شيء مختلف تمامًا. عند الخروج من البوابة ، قد يكون المشغل في سياق مختلف تمامًا. النظر في مثال من البوابة:
كما ترون ، عند عبور البوابة ، تدور الكاميرا نسبة إلى متجه إلى الأمام (تدور). وذلك لأن نقاط البداية والنهاية متوازية مع الطائرات المختلفة:
لذلك ، لكي ينجح هذا ، نحتاج إلى تحويل حركة اللاعب إلى المساحة النسبية للمدخل من أجل تحويله إلى المساحة المستهدفة. من خلال تطبيق هذا ، يمكننا التأكد من أنه بعد دخول البوابة والخروج من الجانب الآخر ، سيتم محاذاة اللاعب بشكل صحيح فيما يتعلق بالمساحة. هذا لا ينطبق فقط على موقف وتناوب الممثل ، ولكن أيضا على
سرعته .
إذا نقلنا عن ممثل دون تغيير ، وقمنا بتحويله إلى دورة محلية ، ونتيجة لذلك ، يمكن للممثل أن يجد نفسه رأسًا على عقب. قد يكون هذا مناسبًا للكائنات ، ولكن لا ينطبق على الشخصيات أو اللاعب نفسه. تحتاج إلى تغيير موضع الممثل ، كما هو موضح أعلاه في المثال من Portal.
void AExedrePortal::TeleportActor( AActor* ActorToTeleport ) { if( ActorToTeleport == nullptr || Target == nullptr ) { return; }
كما لاحظت على الأرجح ، للاتصال بالتناوب / الوضع ، أدعو الوظائف الخارجية. يتم استدعاؤها من فئة مستخدم UTool ، والتي تحدد الوظائف الثابتة التي يمكن استدعاؤها من أي مكان (بما في ذلك المخططات). يتم عرض الكود الخاص بهم أدناه ، ويمكنك تنفيذه بالطريقة التي تبدو أفضل لك (ربما يكون من الأسهل وضعهم في فئة ممثل البوابة).
FVector ConvertLocationToActorSpace( FVector Location, AActor* Reference, AActor* Target ) { if( Reference == nullptr || Target == nullptr ) { return FVector::ZeroVector; } FVector Direction = Location - Reference->GetActorLocation(); FVector TargetLocation = Target->GetActorLocation(); FVector Dots; Dots.X = FVector::DotProduct( Direction, Reference->GetActorForwardVector() ); Dots.Y = FVector::DotProduct( Direction, Reference->GetActorRightVector() ); Dots.Z = FVector::DotProduct( Direction, Reference->GetActorUpVector() ); FVector NewDirection = Dots.X * Target->GetActorForwardVector() + Dots.Y * Target->GetActorRightVector() + Dots.Z * Target->GetActorUpVector(); return TargetLocation + NewDirection; }
يتم إجراء التحويل هنا من خلال حساب الناتج القياسي للناقلات لتحديد زوايا مختلفة. لم يتم تطبيع ناقل الاتجاه ، أي أنه يمكننا مرة أخرى مضاعفة نتيجة النقاط بواسطة الموجهات المستهدفة للوصول إلى الموضع في نفس المسافة بالضبط في المساحة المحلية للممثل المستهدف.
FRotator ConvertRotationToActorSpace( FRotator Rotation, AActor* Reference, AActor* Target ) { if( Reference == nullptr || Target == nullptr ) { return FRotator::ZeroRotator; } FTransform SourceTransform = Reference->GetActorTransform(); FTransform TargetTransform = Target->GetActorTransform(); FQuat QuatRotation = FQuat( Rotation ); FQuat LocalQuat = SourceTransform.GetRotation().Inverse() * QuatRotation; FQuat NewWorldQuat = TargetTransform.GetRotation() * LocalQuat; return NewWorldQuat.Rotator(); }
تحول التحول كان أصعب قليلا لتنفيذ. في النهاية ، تبين أن أفضل حل هو استخدام
المربعات الرباعية ، لأن هذا أكثر دقة بكثير من العمل مع
زوايا Euler العادية ولا يتطلب سوى سطور قليلة من التعليمات البرمجية. يتم إجراء التدوير بواسطة فترات رباعية باستخدام الضرب ، لذلك في حالتنا ، بتطبيق Inverse () على التناوب الذي نريد تحويله ، سننقله إلى المساحة المحلية. بعد ذلك ، نحتاج فقط إلى ضربها مرة أخرى من خلال المنعطف المستهدف للحصول على المنعطف النهائي.
إنشاء شبكة بوابة
لتبدو جميلة من وجهة نظر اللاعب ، يستخدم نظام البوابة الخاص بي شبكة محددة. تنقسم الشبكة إلى طائرتين مختلفتين:
- المستوى 1 : الطائرة الرئيسية التي يتم عرض هدف تقديم البوابة عليها. تتميز هذه الطائرة بسلوك غير معتاد ، لأن مهمتها هي الابتعاد قليلاً عن المشغل أثناء اقترابه لتجنب القصاص بواسطة الكاميرا. نظرًا لأن حدود الطائرة لا تتحرك ، ولكن تتحرك قممها الوسطى فقط ، فإن هذا يسمح للاعب بتثبيته على تقديم البوابة بدون قطع بصرية. الحواف على الحواف لها الأشعة فوق البنفسجية الخاصة بها في النصف العلوي ، بينما الحواف الداخلية لها الأشعة فوق البنفسجية الخاصة بها في النصف السفلي ، مما يجعل من السهل حجبها في التظليل.
- المستوى 2 : يتم استخدام هذه الطائرة فقط لتمديد المربع المحيط للشبكة. يتم توجيه الأوضاع الطبيعية للقيع لأسفل ، لذلك حتى على الأرض غير المستوية ، لن تكون الشبكة مرئية بشكل افتراضي (لأن مادة العرض لن تكون ثنائية الاتجاه).
لماذا استخدام شبكة مثل هذا؟
قررت أن "الطائرة 1" ستمتد مع اقتراب اللاعب. يتيح ذلك للاعب تداخل البوابة والعبور فيها دون تقليم (القطع). يمكن أن يحدث هذا ، على سبيل المثال ، إذا لم تكن الكاميرا قد عبرت بعد مستوى الطائرة الخاصة بالمدخل ، لكن قدميها بالفعل. هذا يسمح لك بعدم قطع اللاعب وتكرار الشبكة من ناحية أخرى.
تتمثل مهمة "المستوى 2" في تمديد الصندوق المحيط القياسي للشبكة. بما أن "المستوى 1" مسطح ، فإن الصندوق المحيط على محور واحد بسمك 0 ، وإذا كانت الكاميرا وراءه ، فسيقوم المحرك بقطعه (أي أنه لن يعرضه). حجم الطائرة 1 128 × 128 ، لذلك يمكن تحجيمها بسهولة باستخدام المحرك. الطائرة 2 أكبر قليلاً وتحت الأرض (أقل من 0).
بعد إنشاء الشبكة ، نقوم ببساطة بتصديرها من محرر ثلاثي الأبعاد لجهة خارجية واستيرادها إلى Unreal. سيتم استخدامه في الخطوة التالية.
إنشاء مواد البوابة
لعرض الجانب الآخر من البوابة ، نحتاج إلى إنشاء المواد الخاصة بنا. قم بإنشاء مادة جديدة في متصفح المحتوى (دعوتها
MAT_PortalBase ):
افتحه الآن وقم بإنشاء الرسم البياني التالي:
إليك كيفية عمل المواد:
- FadeColor هو اللون الذي سيكون مرئيًا من خلال البوابة عندما تكون بعيدة جدًا. هناك حاجة لأننا لا نقدم دائمًا جميع البوابات ، لذلك نحن نحجب العرض عندما يكون المشغل / الكاميرا بعيدًا.
- لمعرفة مقدار المسافة بين المشغل والبوابة ، حدد المسافة بين Camera Position و Actor Position. ثم أقسم المسافة على القيمة القصوى التي أرغب في إجراء مقارنة بها. على سبيل المثال ، إذا كان الحد الأقصى الذي حددته هو 2000 ، وكانت المسافة إلى اللاعب هي 1000 ، فسنحصل على 0.5. إذا كان اللاعب أبعد من ذلك ، فسوف أحصل على قيمة أكبر من 1 ، لذلك يمكنني استخدام العقد المشبعة للحد منه. بعد ذلك ، تأتي العقدة Smoothstep ، والتي يتم استخدامها لقياس المسافة كالتدرج والتحكم بدقة أكبر في البوابة. على سبيل المثال ، أريد عندما يكون اللاعب قريبًا ، يختفي الظل تمامًا.
- أستخدم حساب المسافة كقيمة قناة ألفا لعقدة Lerp لخلط لون التظليل والملمس الذي سيجعل هدف المدخل.
- أخيرًا ، أقوم بعزل المكون Y لإحداثيات UV لإنشاء قناع يتيح لك معرفة رؤوس الشبكة التي سيتم دفعها. أضرب هذا القناع بمقدار الطرد الذي أحتاجه. أستخدم قيمة سالبة بحيث عندما يتم ضرب القيم الطبيعية للقمم بالرؤوس ، فإنها تتحرك في الاتجاه المعاكس.
بعد القيام بكل هذا ، أنشأنا مواد جاهزة للاستخدام.
إنشاء ممثل البوابة في مخطط
لنقم بإعداد فصل جديد للموروثات من ممثل Portal. انقر بزر الماوس الأيمن على متصفح المحتوى وحدد فئة Blueprint:
أدخل الآن "portal" في حقل البحث لتحديد فئة موقع البوابة:
افتح البلوتوث إذا لم يكن مفتوحًا بالفعل. في قائمة المكونات ، سترى التسلسل الهرمي التالي:
كما توقعنا ، هناك مكون الجذر وجذر البوابة. دعنا نضيف مكون شبكة ثابت إلى PortalRootComponent وتحميل الشبكة التي تم إنشاؤها في الخطوة السابقة فيه:
نضيف أيضًا مربع التصادم ، والذي سيتم استخدامه لتحديد ما إذا كان المشغل داخل وحدة تخزين البوابة:
يقع مربع التصادم أسفل مكون المشهد المرتبط بالجذر الرئيسي ، وليس تحت جذر موقع البوابة. أضفت أيضًا أيقونة (لوحة) ومكون سهم لجعل البوابة أكثر وضوحًا على المستويات. بالطبع ، هذا ليس ضروريا.
الآن لنقم بإعداد المواد في المخطط.
بادئ ذي بدء ، نحتاج إلى متغيرين - أحدهما من النوع
Actor والاسم هو
PortalTarget ، والثاني من النوع
Dynamic Material Instance ويسمى
MaterialInstance . يمثل PortalTarget إشارة إلى الموضع الذي تنظر إليه نافذة المدخل (وبالتالي ، فإن المتغير شائع ، مع أيقونة العين المفتوحة) حتى نتمكن من تغييره عندما يتم وضع الفاعل على المستوى. سيقوم MaterialInstance بتخزين رابط لمواد ديناميكية حتى نتمكن في المستقبل من تعيين هدف التجسيد الخاص بالبوابة على الفور.
نحتاج أيضًا إلى إضافة عقد الأحداث الخاصة بنا. من الأفضل فتح قائمة النقر بزر الماوس الأيمن في
Event Graph والعثور على أسماء الأحداث:
وهنا لإنشاء الرسم التالي:
- ابدأ التشغيل : هنا نسمي الدالة الأصل SetTarget () الخاصة بالبوابة لتخصيص رابط للممثل ، والذي سيتم استخدامه لاحقًا في SceneCapture. ثم نقوم بإنشاء مادة ديناميكية جديدة ونخصص لها قيمة متغير MaterialInstance. باستخدام هذه المواد الجديدة ، يمكننا تخصيصها لمكون شبكة ثابت. كما أعطيت المادة نسيجًا وهمية ، ولكن هذا اختياري.
- مسح RTT : الغرض من هذه الميزة هو مسح نسيج Render Target المخصص لمواد المدخل. يتم تشغيله بواسطة مدير البوابة.
- Set RTT : الغرض من هذه الوظيفة هو تعيين المادة المستهدفة للتجسيد الخاصة بالبوابة. يتم تشغيله بواسطة مدير البوابة.
حتى الآن انتهينا من استخدام البلوتوث ، لكننا سنعود إليه لاحقًا لتنفيذ وظائف التجزئة.
مدير البوابة
لذلك ، لدينا الآن جميع العناصر الأساسية اللازمة لإنشاء فئة جديدة موروثة من AActor ، والتي ستكون Portal Manager. قد لا تحتاج إلى فئة Portal Manager في مشروعك ، ولكن في حالتي ، يسهل العمل إلى حد كبير مع بعض الجوانب. فيما يلي قائمة بالمهام التي يؤديها مدير البوابة:
- Portal Manager هو ممثل تم إنشاؤه بواسطة "وحدة تحكم المشغل" وملحق به لتتبع حالة وتطور اللاعب داخل مستوى اللعبة.
- إنشاء وتدمير تقديم البوابة المستهدفة . تتمثل الفكرة في إنشاء نسيج مستهدف بشكل ديناميكي يطابق دقة شاشة اللاعب. بالإضافة إلى ذلك ، عند تغيير الدقة أثناء اللعبة ، يقوم المدير تلقائيًا بتحويلها إلى الحجم المطلوب.
- يبحث مدير البوابة عن مستوى ممثل البوابة ويقوم بتحديثه لمنحهم هدف التجسيد. يتم تنفيذ هذه المهمة بطريقة تضمن التوافق مع تدفق المستوى. عندما يظهر ممثل جديد ، يجب أن يحصل على نسيج. بالإضافة إلى ذلك ، إذا تغير هدف Render ، فيمكن للمدير أيضًا تعيين واحد جديد تلقائيًا. يعمل ذلك على تسهيل إدارة النظام ، بدلاً من الاتصال بكل مدير بوابة يدويًا بالمدير.
- يتم توصيل مكون SceneCapture بمدير Portal ، حتى لا يتم إنشاء نسخة واحدة لكل بوابة. بالإضافة إلى ذلك ، يسمح لك بإعادة استخدامه في كل مرة نتحول فيها إلى ممثل بوابة معين على المستوى.
- عندما يقرر المدخل النقل عن بُعد للاعب ، فإنه يرسل طلبًا إلى Portal Manager. يعد ذلك ضروريًا لتحديث كل من البوابة المصدر والوجهة (إن وجدت) ، بحيث يحدث النقل بدون وصلات.
- يتم تحديث Portal Manager في نهاية وظيفة Character (()) بحيث يتم تحديث كل شيء بشكل صحيح ، بما في ذلك كاميرا المشغل. هذا يضمن أن كل شيء على الشاشة متزامن ويتجنب تأخير إطار واحد أثناء التقديم بواسطة المحرك.
دعنا نلقي نظرة على رأس Portal Manager:
#pragma once #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "ExedrePortalManager.generated.h"
قبل الدخول في التفاصيل ، سأبين كيف يتم إنشاء ممثل من فئة Player Controller ، والتي يتم استدعاؤها من وظيفة BeginPlay ():
FActorSpawnParameters SpawnParams; PortalManager = nullptr; PortalManager = GetWorld()->SpawnActor<AExedrePortalManager>( AExedrePortalManager::StaticClass(), FVector::ZeroVector, FRotator::ZeroRotator, SpawnParams); PortalManager->AttachToActor( this, FAttachmentTransformRules::SnapToTargetIncludingScale); PortalManager->SetControllerOwner( this ); PortalManager->Init();
لذلك ، فإننا ننشئ ممثلًا ، ونعلقه على وحدة تحكم المشغل (هذا) ، ثم نحفظ الرابط ونستدعي الدالة Init ().
من المهم أيضًا ملاحظة أننا نقوم بتحديث الممثل يدويًا من فئة الحرف:
void AExedreCharacter::TickActor( float DeltaTime, enum ELevelTick TickType, FActorTickFunction& ThisTickFunction ) { Super::TickActor( DeltaTime, TickType, ThisTickFunction ); if( UGameplayStatics::GetPlayerController(GetWorld(), 0) != nullptr ) { AExedrePlayerController* EPC = Cast<AExedrePlayerController>( UGameplayStatics::GetPlayerController(GetWorld(), 0) ); EPC->PortalManager->Update( DeltaTime ); } }
وهنا هو منشئ مدير البوابة. لاحظ أنه تم تعطيل التجزئة ، لأننا سنقوم بتحديث Portal Manager يدويًا من خلال المشغل.
AExedrePortalManager::AExedrePortalManager(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { PrimaryActorTick.bCanEverTick = false; PortalTexture = nullptr; UpdateDelay = 1.1f; PreviousScreenSizeX = 0; PreviousScreenSizeY = 0; }
فيما يلي وظائف get / set Portal Manager (بعد ذلك سننتقل إلى أشياء أكثر إثارة للاهتمام):
void AExedrePortalManager::SetControllerOwner( AExedrePlayerController* NewOwner ) { ControllerOwner = NewOwner; } FTransform AExedrePortalManager::GetCameraTransform() { if( SceneCapture != nullptr ) { return SceneCapture->GetComponentTransform(); } else { return FTransform(); } } UTexture* AExedrePortalManager::GetPortalTexture() {
من الواضح أن أول شيء تبدأ به هو الدالة
Init () .
الهدف الرئيسي من هذه الوظيفة هو إنشاء مكون SceneCapture (أي جهاز الالتقاط المذكور أعلاه) وتكوينه بشكل صحيح. يبدأ بإنشاء كائن جديد وتسجيله كعنصر من عناصر هذا الممثل. ثم ننتقل إلى إعداد الخصائص المتعلقة بهذا الالتقاط.الخصائص التي يجب ذكرها:- bCaptureEveryFrame = خطأ : لا نريد تشغيل الالتقاط عندما لا نحتاج إليه. سنقوم بإدارته يدويًا.
- bEnableClipPlane = true : خاصية مهمة جدًا لتقديم التقاط المدخل بشكل صحيح.
- bUseCustomProjectionMatrix = صواب : هذا يتيح لنا استبدال إسقاط Capture بتوقعاتنا ، بناءً على وجهة نظر اللاعب.
- CaptureSource = ESceneCaptureSource :: SCS_SceneColorSceneDepth : هذا الوضع مكلف بعض الشيء ، ولكنه ضروري لتقديم كمية كافية من المعلومات.
تتعلق الخصائص المتبقية أساسًا بمعاملات ما بعد المعالجة. إنها طريقة ملائمة للتحكم في الجودة ، وبالتالي أداء التقاط.يستدعي الجزء الأخير الوظيفة التي تنشئ هدف التجسيد ، والذي سنرى أدناه. void AExedrePortalManager::Init() {
GeneratePortalTexture () هي وظيفة تسمى عند الضرورة عندما تحتاج إلى إنشاء نسيج Render Target جديد للبوابات. يحدث هذا في وظيفة التهيئة ، ولكن يمكن أيضًا استدعاءه أثناء ترقية Portal Manager. هذا هو السبب في أن هذه الوظيفة لديها فحص داخلي لتغيير دقة منفذ العرض. إذا لم يحدث ذلك ، فلن يتم إجراء التحديث.في حالتي ، قمت بإنشاء فئة مجمّع لـ UCanvasRenderTarget2D. دعوتها ExedreScriptedTexture ، وهو مكون يمكن إرفاقه إلى ممثل. لقد قمتُ بإنشاء هذه الفئة لإدارة أهداف التجسيد بشكل مريح مع الممثلين الذين لديهم مهام تقديم. يقوم بالتهيئة المناسبة لـ Render Target ومتوافق مع نظام واجهة المستخدم الخاص بي. ومع ذلك ، في سياق البوابات ، يكون نسيج RenderTarget2D العادي أكثر من كافي. لذلك ، يمكنك ببساطة استخدامه. void AExedrePortalManager::GeneratePortalTexture() { int32 CurrentSizeX = 1920; int32 CurrentSizeY = 1080; if( ControllerOwner != nullptr ) { ControllerOwner->GetViewportSize(CurrentSizeX, CurrentSizeY); } CurrentSizeX = FMath::Clamp( int(CurrentSizeX / 1.7), 128, 1920);
كما ذكرنا أعلاه ، قمت بإنشاء فصل خاص بي ، لذا يجب تكييف الخصائص المحددة هنا مع هدف العرض المعتاد.من المهم أن نفهم أين سيتم عرض الالتقاط. نظرًا لأن هدف التجسيد سيتم عرضه في اللعبة ، فهذا يعني أن هذا سيحدث قبل المعالجة الكاملة بأكملها ، وبالتالي نحتاج إلى تقديم المشهد بمعلومات كافية (لتخزين القيم أعلاه 1 لإنشاء Bloom). لهذا السبب اخترت تنسيق RGBA16 (لاحظ أنه يحتوي على التعداد الخاص به ، ستحتاج إلى استخدام ETextureRenderTargetFormat بدلاً من ذلك).لمزيد من المعلومات ، راجع المصادر التالية:
كذلك سوف ننظر في وظائف التحديث. الوظيفة الأساسية بسيطة للغاية وتتسبب في تعقيد أكثر. هناك تأخير قبل استدعاء دالة GeneratePortalTexture () لتجنب إعادة إنشاء هدف التجسيد عند تغيير حجم منفذ العرض (على سبيل المثال ، في المحرر). أثناء نشر اللعبة ، يمكن إزالة هذا التأخير. void AExedrePortalManager::Update( float DeltaTime ) {
نحن ندعو UpdatePortalsInWorld () للعثور على جميع البوابات الموجودة في العالم الحالي (بما في ذلك جميع المستويات المحملة) وتحديثها. تحدد الوظيفة أيضًا أي منها "نشط" ، أي مرئية للاعب. إذا وجدنا بوابة نشطة ، فإننا ندعو UpdateCapture () ، والذي يتحكم في مكون SceneCapture.
إليك كيفية عمل التحديث العالمي داخل UpdatePortalsInWorld () :- ( )
- iterator ,
- , , ClearRTT() , . (, ).
- , , , , .
التحقق الذي يحدد صحة البوابة بسيط: نعطي الأولوية للبوابة الأقرب إلى المشغل ، لأنه من المرجح أن يكون الأكثر وضوحًا من وجهة نظره. لإسقاط الأقارب ، ولكن ، على سبيل المثال ، البوابات الموجودة خلف المشغل ، ستكون هناك حاجة إلى إجراء اختبارات أكثر تعقيدًا ، لكنني لم أرغب في التركيز على ذلك في تعليمي ، لأنه قد يصبح أمرًا صعبًا للغاية. AExedrePortal* AExedrePortalManager::UpdatePortalsInWorld() { if( ControllerOwner == nullptr ) { return nullptr; } AExedreCharacter* Character = ControllerOwner->GetCharacter();
لقد حان الوقت للنظر في وظيفة UpdateCapture () .هذه ميزة ترقية تلتقط الجانب الآخر من البوابة. من التعليقات يجب أن يكون كل شيء واضحًا ، ولكن إليك وصف موجز:- نحصل على روابط إلى الحرف ومشغل التحكم.
- نتحقق مما إذا كان كل شيء صحيحًا (Portal ، مكون SceneCapture ، المشغل).
- Camera Target .
- , SceneCapture.
- SceneCapture Target.
- , SceneCapure , , .
- Render Target SceneCapture, .
- PlayerController.
- , Capture SceneCapture .
كما يمكننا أن نرى ، عند النقل عن بعد للاعب ، فإن العنصر الأساسي في السلوك الطبيعي الذي لا تشوبه شائبة في SceneCapture هو التحول الصحيح لموضع وتدوير البوابة إلى الفضاء المستهدف المحلي.للحصول على تعريف ConvertLocationToActorSpace () ، راجع "النقل عن بعد من ممثل".
void AExedrePortalManager::UpdateCapture( AExedrePortal* Portal ) { if( ControllerOwner == nullptr ) { return; } AExedreCharacter* Character = ControllerOwner->GetCharacter();
وظائف GetCameraProjectionMatrix () الافتراضية في فئة PlayerController لا وجود لها، وأضفت بنفسي. هو مبين أدناه: FMatrix AExedrePlayerController::GetCameraProjectionMatrix() { FMatrix ProjectionMatrix; if( GetLocalPlayer() != nullptr ) { FSceneViewProjectionData PlayerProjectionData; GetLocalPlayer()->GetProjectionData( GetLocalPlayer()->ViewportClient->Viewport, EStereoscopicPass::eSSP_FULL, PlayerProjectionData ); ProjectionMatrix = PlayerProjectionData.ProjectionMatrix; } return ProjectionMatrix; }
أخيرًا ، نحتاج إلى تنفيذ الدعوة إلى وظيفة النقل الفضائي. سبب المعالجة الجزئية للنقل عن بعد من خلال Portal Manager هو أنه من الضروري ضمان تحديث البوابات الضرورية ، لأن Manager فقط لديه معلومات حول جميع البوابات الموجودة في المشهد.إذا كان لدينا بوابتان متصلتان ، فعند التبديل من واحد إلى آخر ، نحتاج إلى تحديث كلاهما في علامة واحدة. بخلاف ذلك ، سيقوم المشغل بالاتصال عن بعد وسيكون على الجانب الآخر من البوابة ، لكن البوابة الهدف لن تكون نشطة حتى الإطار / التدبير التالي. سيؤدي ذلك إلى إنشاء فجوات مرئية مع مادة الإزاحة لشبكة الطائرة التي رأيناها أعلاه. void AExedrePortalManager::RequestTeleportByPortal( AExedrePortal* Portal, AActor* TargetToTeleport ) { if( Portal != nullptr && TargetToTeleport != nullptr ) { Portal->TeleportActor( TargetToTeleport );
حسنًا ، هذا كل شيء ، لقد انتهينا أخيرًا من Portal Manager!الانتهاء من مخطط
بعد الانتهاء من Portal Manager ، نحتاج فقط إلى إكمال ممثل Portal نفسه ، وبعد ذلك سيعمل النظام. الشيء الوحيد المفقود هنا هو ميزات التجزئة:إليك كيف تعمل:- نحن نقوم بتحديث المواد بحيث لا تبقى في حالة نشطة.
- إذا كان الموقع غير نشط حاليًا ، فسيتم تجاهل الجزء المتبقي من الإجراء.
- نحصل على فئة الأحرف للوصول إلى موقع الكاميرا .
- يتحقق الجزء الأول مما إذا كانت الكاميرا في مربع التصادم الخاص بالبوابة. إذا كانت الإجابة بنعم، ثم ننتقل شبكة البوابة من خلال و المواد A .
- الجزء الثاني هو إعادة التحقق من الموقع داخل مربع التصادم. إذا تم تنفيذه ، فإننا ندعو الدالة التي تحقق ما إذا كنا نعبر البوابة .
- , Portal manager, Teleport .
في لقطة شاشة الرسم البياني الخاص بي ، يمكنك ملاحظة نقطتين مهمتين: Is Point Inside Box و Get Portal Manager . لم أشرح بعد هاتين الوظيفتين. هذه وظائف ثابتة قمت بتعريفها في صفي حتى تتمكن من الاتصال بها من أي مكان. هذا هو نوع من الدرجة المساعد. يظهر رمز هذه الوظائف أدناه ، ويمكنك أنت بنفسك تحديد مكان إدراجها. إذا لم تكن بحاجة إليها خارج نظام المدخل ، فيمكنك إدراجها مباشرةً في فئة الفاعل في Portal.في البداية ، أردت استخدام نظام التصادم للعثور على الفاعل portal داخل مربع التصادم ، لكن بدا لي أنه غير موثوق به بدرجة كافية. بالإضافة إلى ذلك ، يبدو لي أن هذه الطريقة أسرع في الاستخدام ولديها ميزة: فهي تأخذ في الاعتبار دوران الممثل. bool IsPointInsideBox( FVector Point, UBoxComponent* Box ) { if( Box != nullptr ) {
AExedrePortalManager* GetPortalManager( AActor* Context ) { AExedrePortalManager* Manager = nullptr;
الجزء الأخير من ممثل Blueprint هو ForceTick . تذكر أنه يتم استدعاء Force Tick عندما يعبر اللاعب بوابة ما ويقع بجانب مدخل آخر يقوم Portal Manager بفرض تحديث عليه. نظرًا لأننا نقلنا عن بُعد ، فليس من الضروري استخدام نفس الرمز ، ويمكنك استخدام نسخته المبسطة:تبدأ العملية تقريبًا في نفس الوقت مثل وظيفة التجزئة ، لكننا ننفذ الجزء الأول فقط من التسلسل ، والذي يقوم بتحديث المادة.هل انتهينا؟
تقريبا.إذا قمنا بتطبيق نظام المدخل في هذا النموذج ، فعلى الأرجح سنواجه المشكلة التالية:ما الذي يحدث هنا؟في هذا gif ، يقتصر معدل إطارات اللعبة على 6 إطارات في الثانية لإظهار المشكلة بشكل أكثر وضوحًا. في إطار واحد ، يختفي المكعب لأن نظام القطع Unreal Engine يعتبره غير مرئي.هذا بسبب إجراء الاكتشاف في الإطار الحالي ، ثم استخدامه في الإطار التالي. هذا يخلق تأخير إطار واحد . يمكن حل هذا عادةً عن طريق توسيع المربع المحيط للكائن بحيث يتم تسجيله قبل أن يصبح مرئيًا. ومع ذلك ، لن ينجح هذا هنا ، لأنه عندما نعبر البوابة ، فإننا ننتقل من مكان إلى مكان مختلف تمامًا.يتعذر أيضًا تعطيل نظام القطع ، خاصةً لأن ذلك سيؤدي إلى انخفاض في الأداء في المستويات التي تحتوي على العديد من الكائنات. بالإضافة إلى ذلك ، جربت العديد من فرق محرك Unreal ، لكن لم أحصل على نتائج إيجابية: في جميع الحالات ، ظل تأخير إطار واحد. لحسن الحظ ، بعد دراسة مفصلة لكود مصدر Unreal Engine ، تمكنت من إيجاد حل (المسار طويل - استغرق الأمر أكثر من أسبوع)!كما هو الحال مع مكون SceneCapture ، يمكنك إخبار كاميرا المشغل بأننا قطعنا قفزة- قفز موضع الكاميرا بين إطارين ، مما يعني أنه لا يمكننا الاعتماد على معلومات الإطار السابق. يمكن ملاحظة هذا السلوك عند استخدام Matinee أو Sequencer ، على سبيل المثال ، عند تبديل الكاميرات: لا يمكن لطمس الحركة أو التنعيم الاعتماد على معلومات من الإطار السابق.للقيام بذلك ، نحتاج إلى النظر في جانبين:- LocalPlayer : يعالج هذا الفصل المعلومات المختلفة (على سبيل المثال ، منفذ عرض المشغل) ويرتبط بـ PlayerController. هذا هو المكان الذي يمكننا فيه التأثير على عملية عرض كاميرا اللاعب.
- PlayerController : عندما يقوم اللاعب بالاتصال عن بعد ، تبدأ هذه الفئة في الربط بفضل الوصول إلى LocalPlayer.
الميزة الكبيرة لهذا الحل هي أن التدخل في عملية تقديم المحرك هو الحد الأدنى وسهل الصيانة في التحديثات المستقبلية لـ Unreal Engine.
لنبدأ بإنشاء فئة جديدة موروثة من LocalPlayer. يوجد أدناه عنوان يحدد عنصرين رئيسيين: إعادة تحديد حسابات Scene Viewport ووظيفة جديدة لاستدعاء لصق الكاميرا. #pragma once #include "CoreMinimal.h" #include "Engine/LocalPlayer.h" #include "ExedreLocalPlayer.generated.h" UCLASS() class EXEDRE_API UExedreLocalPlayer : public ULocalPlayer { GENERATED_BODY() UExedreLocalPlayer(); public: FSceneView* CalcSceneView( class FSceneViewFamily* ViewFamily, FVector& OutViewLocation, FRotator& OutViewRotation, FViewport* Viewport, class FViewElementDrawer* ViewDrawer, EStereoscopicPass StereoPass) override; void PerformCameraCut(); private: bool bCameraCut; };
إليك كيفية تنفيذ كل شيء: #include "Exedre.h" #include "ExedreLocalPlayer.h" UExedreLocalPlayer::UExedreLocalPlayer() { bCameraCut = false; } FSceneView* UExedreLocalPlayer::CalcSceneView( class FSceneViewFamily* ViewFamily, FVector& OutViewLocation, FRotator& OutViewRotation, FViewport* Viewport, class FViewElementDrawer* ViewDrawer, EStereoscopicPass StereoPass) {
PerformCameraCut () فقط يبدأ Camera Cut بقيمة منطقية. عندما يقوم المحرك باستدعاء وظيفة CalcSceneView () ، نقوم أولاً بتشغيل الوظيفة الأصلية. ثم نتحقق ، نحن بحاجة إلى أداء الإلتصاق إذا كان الأمر كذلك ، فإننا نعيد تعريف متغير Camera Cut Boolean داخل بنية FSceneView ، والذي سيتم استخدامه بواسطة عملية تقديم المحرك ، ثم إعادة تعيين المتغير Boolean (استخدامه).
على جانب وحدة تحكم المشغل ، تكون التغييرات ضئيلة. تحتاج إلى إضافة متغير إلى الرأس لتخزين رابط إلى الفئة المحلية لـ LocalPlayer: UPROPERTY() UExedreLocalPlayer* LocalPlayer;
ثم في الدالة BeginPlay () : LocalPlayer = Cast<UExedreLocalPlayer>( GetLocalPlayer() );
لقد أضفت أيضًا وظيفة لإطلاق Cut بسرعة: void AExedrePlayerController::PerformCameraCut() { if( LocalPlayer != nullptr ) { LocalPlayer->PerformCameraCut(); } }
أخيرًا ، في وظيفة Portal Manager RequestTeleportByPortal () ، يمكننا تنفيذ أثناء النقل عن بعد في Camera Cut: void AExedrePortalManager::RequestTeleportByPortal( AExedrePortal* Portal, AActor* TargetToTeleport ) { if( Portal != nullptr && TargetToTeleport != nullptr ) { if( ControllerOwner != nullptr ) { ControllerOwner->PerformCameraCut(); } [...]
وهذا كل شيء!يجب استدعاء Camera Cut قبل تحديث SceneCapture ، وهذا هو السبب في بداية الوظيفة.النتيجة النهائية
الآن تعلمنا التفكير في البوابات.إذا كان النظام يعمل بشكل جيد ، فيجب أن نكون قادرين على إنشاء هذه الأشياء:إذا كنت تواجه مشكلات ، فعليك التحقق مما يلي:- تحقق من إنشاء Portal Manager وتهيئته بشكل صحيح.
- يتم إنشاء هدف التجسيد بشكل صحيح (يمكنك استخدام الهدف الذي تم إنشاؤه في متصفح المحتوى للبدء).
- يتم تنشيط البوابات وإلغاء تنشيطها بشكل صحيح.
- تحتوي المداخل على تعيين الفاعل الهدف بشكل صحيح في المحرر.
سؤال وجواب
الأسئلة الأكثر شيوعًا التي طُرحت علي حول هذا البرنامج التعليمي:هل من الممكن تطبيق ذلك على الأخطاء ، وليس من خلال C ++؟يمكن تنفيذ الجزء الأكبر من الكود في حالات غير واضحة ، باستثناء جانبين:- لا تتوفر وظيفة LocalPlayer GetProjectionData () المستخدمة للحصول على مصفوفة الإسقاط في المخططات.
- لا تتوفر وظيفة LocalPlayer CalcSceneView () ، التي تعد مهمة للغاية لحل مشكلة نظام القطع ، في المخططات.
لذلك ، تحتاج إلى استخدام تطبيق C ++ للوصول إلى هاتين الوظيفتين ، أو تعديل التعليمات البرمجية المصدر للمحرك لجعلها في متناول من خلال المخططات.هل يمكنني استخدام هذا النظام في الواقع الافتراضي؟نعم ، في الغالب. ومع ذلك ، سيتعين تكييف بعض الأجزاء ، على سبيل المثال:- تحتاج إلى استخدام Render Targets (هدف لكل عين) وقناعهم في مادة موقع البوابة لعرضه جنبًا إلى جنب في مساحة الشاشة. يجب أن يكون كل هدف مستهدف نصف عرض دقة جهاز VR.
- أنت بحاجة إلى استخدام منظرين SceneCapture لتقديم الهدف بالمسافة الصحيحة (المسافة بين العينين) لإنشاء تأثيرات مجسمة.
ستكون المشكلة الرئيسية هي الأداء ، لأن الجانب الآخر من البوابة سيتعين تقديمه مرتين.يمكن كائن آخر عبور البوابة؟لا يوجد في الكود ومع ذلك ، جعلها أكثر عمومية ليست صعبة للغاية. للقيام بذلك ، يحتاج المدخل إلى تتبع مزيد من المعلومات حول جميع الكائنات القريبة من أجل التحقق مما إذا كانت تعبره.هل يدعم النظام العودية (المدخل داخل المدخل)؟هذا البرنامج التعليمي ليس كذلك. للتكرار ، تحتاج إلى تقديم هدف إضافي و SceneCapture. سيكون من الضروري أيضًا تحديد RenderTarget الذي سيتم تقديمه أولاً ، وهكذا. هذا صعب للغاية ولم أرغب في القيام بذلك ، لأن هذا ليس ضروريًا بالنسبة لمشروعي.هل يمكنني عبور البوابة بالقرب من الجدار؟لسوء الحظ ، لا. ومع ذلك ، أرى طريقتين لتنفيذ هذا (من الناحية النظرية):- قم بتعطيل تصادم اللاعب حتى يتمكن من المرور عبر الجدران. إنه سهل التنفيذ ، ولكنه سيؤدي إلى العديد من الآثار الجانبية.
- اخترق نظام التصادم لإنشاء ثقب ديناميكي ، مما سيسمح للاعب بالمرور. للقيام بذلك ، تحتاج إلى تعديل النظام الفعلي للمحرك. ومع ذلك ، بناءً على ما أعرف ، بعد تحميل المستوى ، لا يمكن تحديث الفيزياء الثابتة. لذلك ، لدعم هذه الميزة سوف يتطلب الكثير من العمل. إذا كانت البوابات الخاصة بك ثابتة ، فمن المحتمل أن تتمكن من التغلب على هذه المشكلة باستخدام تدفق المستوى للتبديل بين التصادمات المختلفة.