[الأجزاء
الأولى والثانية والثالثة من البرنامج التعليمي]
- دعم لأنواع مختلفة من الأبراج.
- إنشاء برج هاون.
- حساب مسارات مكافئ.
- إطلاق القذائف المتفجرة.
هذا هو الجزء الرابع من البرنامج التعليمي حول إنشاء لعبة
برج الدفاع بسيطة. فيه سنضيف أبراج هاون تطلق قذائف متفجرة في تصادم.
تم إنشاء البرنامج التعليمي في الوحدة 2018.4.4f1.
يتم قصف الأعداء.أنواع الأبراج
الليزر ليس هو النوع الوحيد من الأسلحة الذي يمكن وضعه على برج. في هذا البرنامج التعليمي ، سنضيف النوع الثاني من الأبراج ، والتي ستطلق قذائف تنفجر عند الاتصال ، مما يؤدي إلى إتلاف جميع الأعداء القريبين. للقيام بذلك ، نحن بحاجة إلى دعم لأنواع مختلفة من الأبراج.
برج مجردة
يعد الكشف عن الهدف وتتبعه إحدى الوظائف التي يمكن لأي برج استخدامها ، لذلك سنضعها في فئة الأبراج الأساسية المجردة. للقيام بذلك ، نستخدم ببساطة فئة
Tower
، ولكن أولاً ، نقوم بتكرار محتوياتها لاستخدامها لاحقًا في فئة محددة من
LaserTower
. ثم نزيل كل الكود المتعلق بالليزر من
Tower
. لا يجوز للبرج تتبع هدف محدد ، لذلك احذف الحقل
target
وقم بتغيير
AcquireTarget
و
TrackTarget
بحيث يتم استخدام معلمة الإخراج كمعلمة ارتباط. بعد ذلك ، سنقوم بإزالة التصور
OnDrawGizmosSelected
من
OnDrawGizmosSelected
، لكننا سنترك نطاق التصويب ، لأنه يستخدم لجميع الأبراج.
using UnityEngine; public abstract class Tower : GameTileContent { const int enemyLayerMask = 1 << 9; static Collider[] targetsBuffer = new Collider[100]; [SerializeField, Range(1.5f, 10.5f)] protected float targetingRange = 1.5f; protected bool AcquireTarget (out TargetPoint target) { … } protected bool TrackTarget (ref TargetPoint target) { … } void OnDrawGizmosSelected () { Gizmos.color = Color.yellow; Vector3 position = transform.localPosition; position.y += 0.01f; Gizmos.DrawWireSphere(position, targetingRange); } }
دعنا نغير الفئة المكررة بحيث يتحول إلى
LaserTower
يمتد
Tower
ويستخدم وظائف الفئة الأساسية الخاصة به ، والتخلص من الكود المكرر.
using UnityEngine; public class LaserTower : Tower { [SerializeField, Range(1f, 100f)] float damagePerSecond = 10f; [SerializeField] Transform turret = default, laserBeam = default; TargetPoint target; Vector3 laserBeamScale; void Awake () { laserBeamScale = laserBeam.localScale; } public override void GameUpdate () { if (TrackTarget(ref target) || AcquireTarget(out target)) { Shoot(); } else { laserBeam.localScale = Vector3.zero; } } void Shoot () { … } }
ثم تحديث الجاهزة من برج الليزر لاستخدام المكون الجديد.
مكون من برج الليزر.خلق نوع معين من البرج
لتتمكن من تحديد الأبراج الموضوعة في الحقل ، سنقوم بإضافة تعداد
TowerType
مشابه لـ
GameTileContentType
. سنقوم بإنشاء دعم لبرج الليزر الحالي وبرج الملاط ، والذي سننشئه لاحقًا.
public enum TowerType { Laser, Mortar }
نظرًا لأننا سننشئ فئة لكل نوع من أنواع البرج ، فسنضيف خاصية الحصول على الملخص إلى
Tower
للإشارة إلى نوعه. يعمل هذا بشكل مشابه لنوع سلوك شخصية في سلسلة البرامج التعليمية
لإدارة الكائنات .
public abstract TowerType TowerType€ { get; }
إعادة
LaserTower
في
LaserTower
بحيث تقوم بإرجاع النوع الصحيح.
public override TowerType TowerType€ => TowerType.Laser;
بعد ذلك ، قم بتغيير
GameTileContentFactory
بحيث يمكن للمصنع إنتاج البرج من النوع المرغوب. ننفذ هذا مع مجموعة من الأبراج وإضافة طريقة
Get
عامة بديلة مع معلمة
TowerType
. للتحقق من تكوين المصفوفة بشكل صحيح ، سنستخدم التأكيدات. سيتم تطبيق طريقة
Get
عامة أخرى الآن فقط على محتويات البلاط بدون أبراج.
[SerializeField] Tower[] towerPrefabs = default; public GameTileContent Get (GameTileContentType type) { switch (type) { … } Debug.Assert(false, "Unsupported non-tower type: " + type); return null; } public GameTileContent Get (TowerType type) { Debug.Assert((int)type < towerPrefabs.Length, "Unsupported tower type!"); Tower prefab = towerPrefabs[(int)type]; Debug.Assert(type == prefab.TowerType€, "Tower prefab at wrong index!"); return Get(prefab); }
سيكون من المنطقي إرجاع النوع الأكثر تحديدًا ، لذلك من الأفضل أن يكون نوع الإرجاع لأسلوب
Get
الجديد هو
Tower
. لكن الأسلوب
Get
الخاص المستخدم
GameTileContent
مثيل
GameTileContent
الجاهزة يعيد
GameTileContent
. هنا يمكنك إما إجراء التحويل ، أو جعل أسلوب
Get
الخاص عامًا. دعنا نختار الخيار الثاني.
public Tower Get (TowerType type) { … } T Get<T> (T prefab) where T : GameTileContent { T instance = CreateGameObjectInstance(prefab); instance.OriginFactory = this; return instance; }
بينما لدينا برج ليزر فقط ، سنجعله العنصر الوحيد في مجموعة أبراج المصنع.
مجموعة من الأبراج الجاهزة.إنشاء مثيلات لأنواع برج محددة
لإنشاء برج من نوع معين ، نقوم
GameBoard.ToggleTower
بحيث يتطلب معلمة
TowerType
إلى المصنع.
public void ToggleTower (GameTile tile, TowerType towerType) { if (tile.Content.Type == GameTileContentType.Tower€) { … } else if (tile.Content.Type == GameTileContentType.Empty) { tile.Content = contentFactory.Get(towerType); … } else if (tile.Content.Type == GameTileContentType.Wall) { tile.Content = contentFactory.Get(towerType); updatingContent.Add(tile.Content); } }
وهذا يخلق فرصة جديدة: تتحول حالة البرج عندما يكون موجودًا بالفعل ، ولكن الأبراج من أنواع مختلفة. حتى الآن ، يؤدي التبديل فقط إلى إزالة البرج الحالي ، ولكن سيكون من المنطقي استبداله بنوع جديد ، لذلك دعونا ننفذ هذا. نظرًا لأن المجموعة لا تزال مشغولة ، فلن تحتاج إلى البحث عن المسار مرة أخرى.
if (tile.Content.Type == GameTileContentType.Tower€) { updatingContent.Remove(tile.Content); if (((Tower)tile.Content).TowerType€ == towerType) { tile.Content = contentFactory.Get(GameTileContentType.Empty); FindPaths(); } else { tile.Content = contentFactory.Get(towerType); updatingContent.Add(tile.Content); } }
يجب أن تتبع
Game
الآن نوع البرج القابل للتحويل. نحن ببساطة نشير إلى كل نوع من البرج برقم. برج الليزر هو 1 ، سيكون البرج الافتراضي ، وبرج الهاون هو 2. بالضغط على مفاتيح الأرقام ، سنختار النوع المناسب من الأبراج.
TowerType selectedTowerType; … void Update () { … if (Input.GetKeyDown(KeyCode.G)) { board.ShowGrid = !board.ShowGrid; } if (Input.GetKeyDown(KeyCode.Alpha1)) { selectedTowerType = TowerType.Laser; } else if (Input.GetKeyDown(KeyCode.Alpha2)) { selectedTowerType = TowerType.Mortar; } … } … void HandleTouch () { GameTile tile = board.GetTile(TouchRay); if (tile != null) { if (Input.GetKey(KeyCode.LeftShift)) { board.ToggleTower(tile, selectedTowerType); } else { board.ToggleWall(tile); } } }
برج هاون
لن يكون من الممكن وضع برج الهاون بعد ، لأنه لا يوجد به حتى الآن الجاهزة. لنبدأ بإنشاء نوع
MortarTower
الحد الأدنى. تحتوي مدافع الهاون على عدد مرات إطلاق النار ، للإشارة إلى ما يمكنك استخدامه في حقل تكوين "اللقطات في الثانية". بالإضافة إلى ذلك ، سنحتاج إلى رابط إلى الهاون حتى يتمكن من تحقيق الهدف.
using UnityEngine; public class MortarTower : Tower { [SerializeField, Range(0.5f, 2f)] float shotsPerSecond = 1f; [SerializeField] Transform mortar = default; public override TowerType TowerType€ => TowerType.Mortar; }
الآن إنشاء الجاهزة لبرج هاون. يمكن القيام بذلك من خلال تكرار الجاهزة لبرج الليزر واستبدال مكون البرج الخاص به. ثم نتخلص من أشياء البرج وحزمة الليزر. أعد تسمية
turret
إلى
mortar
، وحركه لأسفل حتى يقف فوق القاعدة ، واعطيه لونًا رماديًا فاتحًا وأرفقه. يمكننا أن نترك مصادم الملاط ، في هذه الحالة ، باستخدام كائن منفصل ، وهو مصادم بسيط مركب على الاتجاه الافتراضي للهاون. قمت بتعيين مجموعة هاون من 3.5 وتردد 1 طلقة في الثانية الواحدة.
الجاهزة من برج هاون.لماذا يطلق عليهم مدافع الهاون؟كانت الأصناف الأولى من هذا السلاح عبارة عن أوعية حديدية ، على غرار قذائف الهاون ، حيث تم طحن المكونات باستخدام مدقة.
أضف قذائف الهاون الجاهزة إلى صفيف المصنع بحيث يمكن وضع أبراج الملاط على الحقل. ومع ذلك ، فإنهم لا يفعلون أي شيء حتى الآن.
نوعان من الأبراج ، أحدهما غير نشطحساب المسار
يطلق مورتيرا قذيفة من زاوية ، حتى يطير فوق العقبات ويضرب الهدف من الأعلى. عادة ، يتم استخدام الأصداف التي تنفجر عند التصادم مع أو فوق الهدف. من أجل عدم تعقيد الأشياء ، سنهدف دائمًا إلى الأرض حتى تنفجر القذائف عندما ينخفض ارتفاعها إلى الصفر.
تهدف أفقي
لهدف الهاون ، نحتاج إلى توجيهه أفقيًا إلى الهدف ، ثم تغيير موقعه العمودي بحيث تهبط القذيفة في المسافة الصحيحة. سنبدأ بالخطوة الأولى. أولاً ، سوف نستخدم النقاط النسبية الثابتة ، وليس الأهداف المتحركة ، للتأكد من صحة حساباتنا.
أضف طريقة
MortarTower
إلى
GameUpdate
، والتي تستدعي دائمًا طريقة
Launch
. بدلاً من إطلاق قذيفة حقيقية ، سنقوم بتصور الحسابات الرياضية في الوقت الحالي. نقطة إطلاق النار هي موقع الهاون في العالم ، والذي يقع فوق الأرض مباشرة. نضع نقطة الهدف من ثلاث وحدات منه على طول المحور X ونحذف المكون Y ، لأننا نهدف دائمًا إلى الأرض. ثم سنقوم بإظهار النقاط عن طريق
Debug.DrawLine
خط أصفر بينها عن طريق استدعاء
Debug.DrawLine
. سيكون الخط مرئيًا في وضع المشهد لإطار واحد ، لكن هذا يكفي ، لأنه في كل إطار نرسم خطًا جديدًا.
public override void GameUpdate () { Launch(); } public void Launch () { Vector3 launchPoint = mortar.position; Vector3 targetPoint = new Vector3(launchPoint.x + 3f, 0f, launchPoint.z); Debug.DrawLine(launchPoint, targetPoint, Color.yellow); }
نحن نهدف إلى نقطة ثابتة بالنسبة للبرج.باستخدام هذا الخط ، يمكننا تحديد مثلث صحيح. نقطته العليا هي في وضع هاون. فيما يتعلق بالهاون ، هذا هو
تبدأbmatrix00 endbmatrix . النقطة أدناه ، عند قاعدة البرج ، هي
تبدأbmatrix0y endbmatrix ، والهدف في الهدف هو
تبدأbmatrixxy endbmatrix حيث
x يساوي 3 و
ذ هو الوضع الرأسي السلبي للهاون. نحن بحاجة إلى تتبع هاتين القيمتين.
Vector3 launchPoint = mortar.position; Vector3 targetPoint = new Vector3(launchPoint.x + 3f, 0f, launchPoint.z); float x = 3f; float y = -launchPoint.y;
مثلث تهدف.بشكل عام ، يمكن أن يكون الهدف في أي مكان في نطاق البرج ، لذلك يجب أيضًا أخذ Z في الاعتبار. ومع ذلك ، لا يزال المثلث المستهدف ثنائي الأبعاد ، إنه يدور ببساطة حول المحور Y. لتوضيح ذلك ، سنضيف معلمة متجه الإزاحة النسبية في
Launch
وسنسميها بأربعة عمليات إزاحة في XZ:
تبدأbmatrix30 endbmatrix .
تبدأbmatrix01 endbmatrix .
تبدأbmatrix11 endbmatrix و
تبدأbmatrix31 endbmatrix . عندما تصبح نقطة الهدف مساوية لنقطة الالتقاط بالإضافة إلى هذا الإزاحة ، يصبح إحداثي Y صفراً.
public override void GameUpdate () { Launch(new Vector3(3f, 0f, 0f)); Launch(new Vector3(0f, 0f, 1f)); Launch(new Vector3(1f, 0f, 1f)); Launch(new Vector3(3f, 0f, 1f)); } public void Launch (Vector3 offset) { Vector3 launchPoint = mortar.position; Vector3 targetPoint = launchPoint + offset; targetPoint.y = 0f; … }
الآن x للمثلث المستهدف يساوي طول الموجه ثنائي الأبعاد الذي يشير من قاعدة البرج إلى نقطة الهدف. عن طريق تطبيع هذا المتجه ، نحصل أيضًا على متجه اتجاه XZ ، والذي يمكن استخدامه لمحاذاة المثلث. يمكنك إظهاره عن طريق رسم أسفل المثلث كخط أبيض تم الحصول عليه من الاتجاه و x.
Vector2 dir; dir.x = targetPoint.x - launchPoint.x; dir.y = targetPoint.z - launchPoint.z; float x = dir.magnitude; float y = -launchPoint.y; dir /= x; Debug.DrawLine(launchPoint, targetPoint, Color.yellow); Debug.DrawLine( new Vector3(launchPoint.x, 0.01f, launchPoint.z), new Vector3( launchPoint.x + dir.x * x, 0.01f, launchPoint.z + dir.y * x ), Color.white );
محاذاة تهدف المثلثات.زاوية النار
بعد ذلك ، يجب علينا معرفة الزاوية التي يجب عندها إطلاق النار على القذيفة. من الضروري استخلاصها من فيزياء مسار المقذوفات. لن نأخذ بعين الاعتبار السحب والرياح والعقبات الأخرى ، فقط سرعة اللقطة
v والجاذبية
g=9.81 .
الإزاحة
د القذيفة تتماشى مع مثلث التصويب ويمكن وصفها بواسطة مكونين. مع النزوح الأفقي ، الأمر بسيط: إنه
dx=vxt حيث
t - الوقت بعد الطلقة. مع كون كل عنصر رأسي متشابه ، فإنه يخضع لتسارع سلبي بسبب الجاذبية ، وبالتالي يكون لديه الشكل
dy=vyt−(gt2)/2 .
كيف يتم حساب الأوفست؟سرعة الخامس التي تحددها المسافة في الثانية الواحدة ، وبالتالي ، ضرب السرعة حسب المدة تي نحن نحصل على المسافة د = الخامس ر . عندما تشارك التسارع د و ل ا ، السرعة متغيرة. التسارع هو التغير في السرعة في الثانية ، أي المسافة في الثانية المربعة. في أي وقت ، والسرعة هي v = ف ي . في حالتنا ، هناك تسارع مستمر أ = - ز ، لذلك يمكننا تقسيمها إلى نصفين للحصول على متوسط السرعة ، وضرب الوقت للعثور على الإزاحة d=(في2)/2 بسبب الجاذبية.
نحن اطلاق النار قذائف في نفس السرعة
s التي لا تعتمد على زاوية النار
theta (ثيتا). هذا هو
vx=s cos theta و
vy=s sin theta .
حساب سرعة النار.أداء الاستبدال ، نحصل عليه
dx=st cos theta و
dy=st sin theta−(gt2)/2 .
يتم إطلاق القذيفة بحيث وقت الرحلة
t هي القيمة الدقيقة اللازمة لتحقيق الهدف. نظرًا لأنه من الأسهل التعامل مع التشرد الأفقي ، يمكننا التعبير عن الوقت
t=dx/vx . في نقطة النهاية
dx=x هذا هو
t=x/(s cos theta) . هذا يعني ذلك
y=x tan theta−(gx2)/(2s2 cos2 theta) .
كيفية الحصول على المعادلة ذ؟y=dy=s(x/(s cos theta)) sin theta−(g(x/(s cos theta))2)/2=x sin theta/ cos theta−(gx2)/(2s2 cos2 theta) و tan theta= sin theta/ cos theta .
باستخدام هذه المعادلة نجد
tan theta=(s2+− sqrt(s4−g(gx2+2ys2)))/(gx) .
كيفية الحصول على المعادلة تان؟أولاً سوف نستخدم الهوية المثلثية sec theta=1/ cos theta و 1+ tan2 theta= sec2 theta ليأتي ل y=x tan theta−(gx2)/(2s2)(1+ tan2 theta)=−(gx2)/(2s2) tan2 theta+x tan theta−(gx2)/(2s2) .
هذا هو تعبير عن النموذج au2+bu+c=0 حيث u= tan theta . a=−(gx2)/(2s2) . b=x و c=a−y .
يمكننا حلها باستخدام صيغة جذور المعادلة التربيعية u=(−b+− sqrt(b2−4ac))/(2a) .
بعد استبدالها ، سوف تصبح المعادلة مربكة ، ولكن يمكنك تبسيطها عن طريق ضرب ب m=s2/x حتى تحصل عليه tan theta=(−mb+−m sqrtr)/(2ma) حيث r=b2−4ac .
في هذه الحالة ، نحصل عليها tan theta=(s2+− sqrt(m2r))/(gx) .
نتيجة لذلك m2r=(s4/x2)r=s4+2gs2c=s4−g2x2−2gys2=s4−g(gx2+2ys2) .
هناك زاويتان محتملتان ، لأنك يمكن أن تستهدف ارتفاعًا أو انخفاضًا. المسار المنخفض أسرع لأنه أقرب إلى خط مستقيم من الهدف. لكن المسار العالي يبدو أكثر إثارة للاهتمام ، لذلك سوف نختار ذلك. هذا يعني أننا نحتاج فقط إلى استخدام الحل الأكبر.
tan theta=(s2+ sqrt(s4−g(gx2+2ys2)))/(gx) . نحسبها ، وكذلك
cos theta مع
sin theta ، لأننا نحتاج إليهم للحصول على ناقل سرعة اللقطة. لهذا تحتاج إلى تحويل
tan theta إلى زاوية راديان باستخدام
Mathf.Atan
. أولاً ، دعنا نستخدم سرعة تسديد ثابتة 5.
float x = dir.magnitude; float y = -launchPoint.y; dir /= x; float g = 9.81f; float s = 5f; float s2 = s * s; float r = s2 * s2 - g * (g * x * x + 2f * y * s2); float tanTheta = (s2 + Mathf.Sqrt(r)) / (g * x); float cosTheta = Mathf.Cos(Mathf.Atan(tanTheta)); float sinTheta = cosTheta * tanTheta;
دعنا نتصور المسار من خلال رسم عشرة مقاطع زرقاء تظهر الثانية الأولى من الرحلة.
float sinTheta = cosTheta * tanTheta; Vector3 prev = launchPoint, next; for (int i = 1; i <= 10; i++) { float t = i / 10f; float dx = s * cosTheta * t; float dy = s * sinTheta * t - 0.5f * g * t * t; next = launchPoint + new Vector3(dir.x * dx, dy, dir.y * dx); Debug.DrawLine(prev, next, Color.blue); prev = next; }
مسارات الطيران Parabola دائم ثانية واحدة.يمكن الوصول إلى أبعد نقطتين في أقل من ثانية ، لذلك نرى مساراتها بأكملها ، وتستمر الأجزاء إلى أبعد قليلاً تحت الأرض. بالنسبة للنقطتين الأخريين ، هناك حاجة إلى زوايا أكبر للرصاص ، حيث تصبح المسارات أطول ، وتستغرق الرحلة أكثر من ثانية.
سرعة اطلاق النار
إذا كنت تريد الوصول إلى أقرب نقطتين في الثانية ، فأنت بحاجة إلى تقليل سرعة اللقطة. لنجعلها تساوي 4.
float s = 4f;
سرعة اطلاق النار خفضت إلى 4.مساراتهم كاملة الآن ، لكن المسارين الآخرين قد ولت. حدث هذا لأن سرعة اللقطة ليست كافية الآن للوصول إلى هذه النقاط. في مثل هذه الحالات ، حلول ل
tan theta لا ، هذا هو ، نحصل على الجذر التربيعي لعدد سالب ، مما يؤدي إلى قيم NaN واختفاء الخطوط. يمكننا التعرف على هذا عن طريق التحقق
ص إلى السلبية.
float r = s2 * s2 - g * (g * x * x + 2f * y * s2); Debug.Assert(r >= 0f, "Launch velocity insufficient for range!");
يمكن تجنب هذا الموقف عن طريق ضبط سرعة إطلاق عالية بدرجة كافية. ولكن إذا كان حجمها كبيرًا جدًا ، فإن الوصول إلى الأهداف بالقرب من البرج سيتطلب مسارات عالية جدًا وفترة طيران طويلة ، لذا يجب أن تترك السرعة منخفضة قدر الإمكان. يجب أن تكون سرعة اللقطة كافية للوصول إلى الهدف في أقصى مدى.
في أقصى مدى
r=0 ، أي ل
tan theta هناك حل واحد فقط ، يتوافق مع مسار منخفض. هذا يعني أننا نعرف سرعة اللقطة المطلوبة.
s = s q r t ( g ( y + s q r t ( x 2 + y 2 ) ) ) .
كيفية استخلاص هذه المعادلة ل؟تحتاج إلى اتخاذ قرار s4−g(gx2+2ys2)=s4−2gys2−g2x2=0 إلى s .
هذا هو تعبير عن النموذج au2+bu+c=0 حيث u=s2 . a=1 . b=−2gy و c=−g2x2 .
يمكنك حلها باستخدام الصيغة المبسطة لجذور المعادلة التربيعية u=(−b+− sqrt(b2−4c))/2 .
بعد الاستبدال نحصل عليه s2=(2gy+− sqrt(4g2y2+4g2x2))/2=gy+−g sqrt(x2+y2) .
نحن بحاجة إلى حل إيجابي ، لذلك نأتي إلى s2=g(y+ sqrt(x2+y2)) .
نحتاج إلى تحديد السرعة المطلوبة فقط عندما تستيقظ الهاون (استيقظ) أو عندما نغير مداها في وضع التشغيل. لذلك ، سوف نقوم بتتبعه باستخدام الحقل
Awake
في
Awake
و
OnValidate
.
float launchSpeed; void Awake () { OnValidate(); } void OnValidate () { float x = targetingRange; float y = -mortar.position.y; launchSpeed = Mathf.Sqrt(9.81f * (y + Mathf.Sqrt(x * x + y * y))); }
ومع ذلك ، نظرًا للقيود في دقة حسابات الفاصلة العائمة ، قد يكون تحديد الهدف القريب جدًا من الحد الأقصى للنطاق خاطئًا. لذلك ، عند حساب السرعة المطلوبة ، نضيف كمية صغيرة إلى النطاق. بالإضافة إلى ذلك ، يوسع نصف قطر مصادم العدو بشكل أساسي أقصى نصف قطر لمجموعة البرج. لقد جعلناها تساوي 0.125 ، ولكن مع زيادة حجم العدو ، يمكن أن يتضاعف قدر الإمكان ، لذلك سنزيد النطاق الفعلي بحوالي 0.25 ، على سبيل المثال ، بمقدار 0.25001.
float x = targetingRange + 0.25001f;
بعد ذلك ، قم بتطبيق المعادلة المشتقة لسرعة اللقطة في
Launch
.
float s = launchSpeed;
قم بتطبيق السرعة المحسوبة على النطاق المستهدف 3.5.إطلاق نار
بعد الحساب الصحيح للمسار ، يمكنك التخلص من أهداف الاختبار النسبية. الآن تحتاج إلى تمرير نقطة
Launch
إلى الهدف.
public void Launch (TargetPoint target) { Vector3 launchPoint = mortar.position; Vector3 targetPoint = target.Position; targetPoint.y = 0f; … }
بالإضافة إلى ذلك ، لا يتم إطلاق الطلقات في كل إطار. نحتاج إلى تتبع عملية اللقطة بنفس طريقة عملية إنشاء الأعداء والتقاط هدف عشوائي عندما يحين وقت اللقطة في
GameUpdate
. لكن في هذه المرحلة ، قد لا يكون هناك أي أهداف متاحة. في هذه الحالة ، نواصل عملية إطلاق النار ، ولكن دون تراكم إضافي. لتجنب حلقة لا نهائية ، تحتاج إلى جعلها أقل قليلاً من 1.
float launchProgress; … public override void GameUpdate () { launchProgress += shotsPerSecond * Time.deltaTime; while (launchProgress >= 1f) { if (AcquireTarget(out TargetPoint target)) { Launch(target); launchProgress -= 1f; } else { launchProgress = 0.999f; } } }
نحن لا نتتبع الأهداف بين الطلقات ، لكننا نحتاج إلى تدوير الهاون بشكل صحيح أثناء الطلقات. يمكنك استخدام الاتجاه الأفقي
Quaternion.LookRotation
لتدوير الهاون أفقيًا باستخدام
Quaternion.LookRotation
. نحن بحاجة أيضا مع
t a n t h e t a قم بتطبيق زاوية اللقطة للمكون Y من ناقل الاتجاه. سيعمل هذا لأن الاتجاه الأفقي يبلغ طوله 1 ، أي
t a n t h e t a = s i n t h e t a .
تحلل بدوره بدوره من نظرة. float tanTheta = (s2 + Mathf.Sqrt(r)) / (g * x); float cosTheta = Mathf.Cos(Mathf.Atan(tanTheta)); float sinTheta = cosTheta * tanTheta; mortar.localRotation = Quaternion.LookRotation(new Vector3(dir.x, tanTheta, dir.y));
لمشاهدة مسار اللقطات ، يمكنك إضافة معلمة إلى
Debug.DrawLine
تسمح
Debug.DrawLine
لفترة طويلة.
Vector3 prev = launchPoint, next; for (int i = 1; i <= 10; i++) { … Debug.DrawLine(prev, next, Color.blue, 1f); prev = next; } Debug.DrawLine(launchPoint, targetPoint, Color.yellow, 1f); Debug.DrawLine( … Color.white, 1f );
الهدف.ذخيرة
معنى حساب المسارات هو أننا نعرف الآن كيفية إطلاق قذائف. التالي نحتاج إلى إنشائها واطلاق النار عليهم.
مصنع الحرب
نحن بحاجة إلى مصنع لإنشاء كائنات قذيفة. بينما في الجو ، توجد القذائف من تلقاء نفسها ولم تعد تعتمد على قذائف الهاون التي أطلقت النار عليهم. لذلك ، لا ينبغي معالجتها بواسطة برج الملاط ، كما أن مصنع محتوى البلاط غير مناسب لذلك.
لنقم بإنشاء كل ما يتعلق بالأسلحة ، وهو مصنع جديد ونطلق عليه مصنع الحرب. أولاً ، قم بإنشاء ملخص WarEntity
مع خاصية OriginFactory
وطريقة Recycle
. using UnityEngine; public abstract class WarEntity : MonoBehaviour { WarFactory originFactory; public WarFactory OriginFactory { get => originFactory; set { Debug.Assert(originFactory == null, "Redefined origin factory!"); originFactory = value; } } public void Recycle () { originFactory.Reclaim(this); } }
ثم قم بإنشاء كيان محدد Shell
للقذائف. using UnityEngine; public class Shell : WarEntity { }
ثم إنشاء WarFactory
واحدة من شأنها أن تنشئ قذيفة باستخدام خاصية getter العامة. using UnityEngine; [CreateAssetMenu] public class WarFactory : GameObjectFactory { [SerializeField] Shell shellPrefab = default; public Shell Shell€ => Get(shellPrefab); T Get<T> (T prefab) where T : WarEntity { T instance = CreateGameObjectInstance(prefab); instance.OriginFactory = this; return instance; } public void Reclaim (WarEntity entity) { Debug.Assert(entity.OriginFactory == this, "Wrong factory reclaimed!"); Destroy(entity.gameObject); } }
إنشاء الجاهزة للقذيفة. لقد استخدمت مكعبًا بسيطًا بنفس المقياس 0.25 والمواد المظلمة ، بالإضافة إلى مكون Shell
. ثم قم بإنشاء أصل المصنع وتعيينه الجاهزة للقذيفة.مصنع الحرب.سلوك اللعبة
لتحريك الأصداف يحتاجون إلى تحديث. يمكنك استخدام نفس الطريقة المستخدمة Game
لتحديث حالة الأعداء. في الواقع ، يمكننا حتى جعل هذا النهج معمم عن طريق إنشاء مكون تجريدي GameBehavior
يمتد MonoBehaviour
ويضيف طريقة افتراضية GameUpdate
. using UnityEngine; public abstract class GameBehavior : MonoBehaviour { public virtual bool GameUpdate () => true; }
الآن تفعل إعادة بيع المباني EnemyCollection
، وتحويلها إلى GameBehaviorCollection
. public class GameBehaviorCollection { List<GameBehavior> behaviors = new List<GameBehavior>(); public void Add (GameBehavior behavior) { behaviors.Add(behavior); } public void GameUpdate () { for (int i = 0; i < behaviors.Count; i++) { if (!behaviors[i].GameUpdate()) { int lastIndex = behaviors.Count - 1; behaviors[i] = behaviors[lastIndex]; behaviors.RemoveAt(lastIndex); i -= 1; } } } }
لنجعلها WarEntity
تتوسع GameBehavior
، لا MonoBehavior
. public abstract class WarEntity : GameBehavior { … }
سنفعل نفس الشيء من أجل Enemy
، هذه المرة تجاوز الأسلوب GameUpdate
. public class Enemy : GameBehavior { … public override bool GameUpdate () { … } … }
من الآن فصاعدًا ، Game
سيتعين عليها تتبع مجموعتين ، واحدة للأعداء ، والآخر لغير الأعداء. يجب تحديث غير الأعداء بعد كل شيء آخر. GameBehaviorCollection enemies = new GameBehaviorCollection(); GameBehaviorCollection nonEnemies = new GameBehaviorCollection(); … void Update () { … enemies.GameUpdate(); Physics.SyncTransforms(); board.GameUpdate(); nonEnemies.GameUpdate(); }
الخطوة الأخيرة في تطبيق ترقية shell هي إضافتها إلى مجموعة من غير الأعداء. لنقم بذلك من خلال وظيفة Game
ستكون واجهة ثابتة لمصنع حرب بحيث يمكن إنشاء المقذوفات عن طريق التحدي Game.SpawnShell()
. لكي يعمل هذا ، Game
يجب أن يكون لديك رابط إلى مصنع الحرب وتتبع حالتك الخاصة. [SerializeField] WarFactory warFactory = default; … static Game instance; public static Shell SpawnShell () { Shell shell = instance.warFactory.Shell€; instance.nonEnemies.Add(shell); return shell; } void OnEnable () { instance = this; }
لعبة مع مصنع الحرب.هل الواجهة الساكنة حل جيد؟, , .
نحن اطلاق النار قذيفة
بعد إنشاء مثيل للقذيفة ، يجب أن تطير على طول طريقها حتى تصل إلى الهدف النهائي. للقيام بذلك ، أضف إلى Shell
الطريقة Initialize
واستخدمها لتحديد نقطة اللقطة ونقطة الهدف وسرعة اللقطة. Vector3 launchPoint, targetPoint, launchVelocity; public void Initialize ( Vector3 launchPoint, Vector3 targetPoint, Vector3 launchVelocity ) { this.launchPoint = launchPoint; this.targetPoint = targetPoint; this.launchVelocity = launchVelocity; }
الآن يمكننا إنشاء قذيفة MortarTower.Launch
وإرسالها على الطريق. mortar.localRotation = Quaternion.LookRotation(new Vector3(dir.x, tanTheta, dir.y)); Game.SpawnShell().Initialize( launchPoint, targetPoint, new Vector3(s * cosTheta * dir.x, s * sinTheta, s * cosTheta * dir.y) );
حركة قذيفة
إلى Shell
التحرك، نحن بحاجة إلى تتبع مدة وجودها، وهذا هو الوقت الذي انقضى من النار. ثم يمكننا حساب موقفه في GameUpdate
. نحن نفعل هذا دائمًا فيما يتعلق بنقطة إطلاقه ، بحيث يتبع القذيفة المسار تمامًا بغض النظر عن معدل التحديث. float age; … public override bool GameUpdate () { age += Time.deltaTime; Vector3 p = launchPoint + launchVelocity * age; py -= 0.5f * 9.81f * age * age; transform.localPosition = p; return true; }
اطلاق النار المقذوفات.لمحاذاة الأصداف مع مساراتها ، نحتاج إلى جعلها تبدو على طول المتجه المشتق ، وهي سرعتها في الوقت المناسب. public override bool GameUpdate () { … Vector3 d = launchVelocity; dy -= 9.81f * age; transform.localRotation = Quaternion.LookRotation(d); return true; }
القذائف تحول.نحن تنظيف اللعبة
الآن ، أصبح من الواضح أن القذائف تطير تمامًا كما ينبغي ، يمكنك إزالة MortarTower.Launch
المسارات من التصور. public void Launch (TargetPoint target) { … Game.SpawnShell().Initialize( launchPoint, targetPoint, new Vector3(s * cosTheta * dir.x, s * sinTheta, s * cosTheta * dir.y) ); }
بالإضافة إلى ذلك ، نحتاج إلى التأكد من تدمير القذائف بعد إصابة الهدف. نظرًا لأننا نهدف دائمًا إلى الأرض ، يمكن القيام بذلك عن طريق التحقق Shell.GameUpdate
لمعرفة ما إذا كان الموضع العمودي أقل من الصفر. يمكنك القيام بذلك فورًا بعد حسابها ، قبل تغيير الموقف وتحويل المقذوفة. public override bool GameUpdate () { age += Time.deltaTime; Vector3 p = launchPoint + launchVelocity * age; py -= 0.5f * 9.81f * age * age; if (py <= 0f) { OriginFactory.Reclaim(this); return false; } transform.localPosition = p; … }
تفجير
نحن نطلق قذائف لأنها تحتوي على متفجرات. عندما يصل الصاروخ إلى هدفه ، يجب أن ينفجر ويلحق أضرارًا بجميع الأعداء في منطقة الانفجار. يعتمد نصف قطر الانفجار والأضرار التي لحقت به على نوع القذائف التي تطلقها الهاون ، لذلك سنضيف MortarTower
خيارات التكوين الخاصة بهم. [SerializeField, Range(0.5f, 3f)] float shellBlastRadius = 1f; [SerializeField, Range(1f, 100f)] float shellDamage = 10f;
انفجار دائرة نصف قطرها و 1.5 الضرر من 15 قذيفة.هذا التكوين مهم فقط أثناء الانفجار ، لذلك يجب إضافته Shell
وطريقة استخدامه Initialize
. float age, blastRadius, damage; public void Initialize ( Vector3 launchPoint, Vector3 targetPoint, Vector3 launchVelocity, float blastRadius, float damage ) { … this.blastRadius = blastRadius; this.damage = damage; }
MortarTower
يجب فقط نقل البيانات إلى المقذوف بعد إنشائه. Game.SpawnShell().Initialize( launchPoint, targetPoint, new Vector3(s * cosTheta * dir.x, s * sinTheta, s * cosTheta * dir.y), shellBlastRadius, shellDamage );
لإطلاق النار على الأعداء داخل النطاق ، يجب على القذيفة التقاط الأهداف. لدينا بالفعل رمز لهذا ، لكنه في Tower
. نظرًا لأنه مفيد لكل ما يحتاج إلى هدف ، انسخ وظائفه TargetPoint
وجعله متاحًا بشكل ثابت. أضف طريقة لملء المخزن المؤقت ، خاصية للحصول على الكمية المخزنة مؤقتًا ، وطريقة للحصول على الهدف المخزن مؤقتًا. const int enemyLayerMask = 1 << 9; static Collider[] buffer = new Collider[100]; public static int BufferedCount { get; private set; } public static bool FillBuffer (Vector3 position, float range) { Vector3 top = position; top.y += 3f; BufferedCount = Physics.OverlapCapsuleNonAlloc( position, top, range, buffer, enemyLayerMask ); return BufferedCount > 0; } public static TargetPoint GetBuffered (int index) { var target = buffer[index].GetComponent<TargetPoint>(); Debug.Assert(target != null, "Targeted non-enemy!", buffer[0]); return target; }
الآن يمكننا استلام جميع الأهداف ضمن النطاق حتى الحد الأقصى لحجم المخزن المؤقت وإلحاق أضرار عند التفجير Shell
. if (py <= 0f) { TargetPoint.FillBuffer(targetPoint, blastRadius); for (int i = 0; i < TargetPoint.BufferedCount; i++) { TargetPoint.GetBuffered(i).Enemy€.ApplyDamage(damage); } OriginFactory.Reclaim(this); return false; }
تفجير القذائف.يمكنك أيضًا إضافة TargetPoint
خاصية ثابتة للحصول على هدف عشوائي من المخزن المؤقت. public static TargetPoint RandomBuffered => GetBuffered(Random.Range(0, BufferedCount));
سيسمح لنا ذلك بالتبسيط Tower
، لأنه يمكنك الآن البحث عن هدف عشوائي TargetPoint
. protected bool AcquireTarget (out TargetPoint target) { if (TargetPoint.FillBuffer(transform.localPosition, targetingRange)) { target = TargetPoint.RandomBuffered; return true; } target = null; return false; }
انفجارات
كل شيء يعمل ، لكنه لا يزال لا يبدو مقبولاً للغاية. يمكنك تحسين الصورة عن طريق إضافة تصور للانفجار عند انفجار القوقعة. لن يبدو هذا أكثر إثارة للاهتمام فحسب ، بل سيعطي اللاعب ملاحظات مفيدة. للقيام بذلك ، سنقوم بإنشاء الجاهزة للانفجار مثل شعاع الليزر. فقط سيكون المجال أكثر شفافية من الألوان الزاهية. إضافة مكون كيان جديد Explosion
مع مدة مخصصة. نصف ثانية ستكون كافية. أضفها بطريقة Initialize
تحدد موقع الانفجار وقطره. عند ضبط المقياس ، تحتاج إلى مضاعفة نصف القطر ، لأن نصف قطر شبكة الكرة هو 0.5. إنه أيضًا مكان جيد للتعامل مع الأضرار لجميع الأعداء ضمن النطاق ، لذلك سنضيف أيضًا معلمة الضرر. بالإضافة إلى ذلك ، يحتاج إلى طريقة GameUpdate
للتحقق مما إذا كان الوقت ينفد. using UnityEngine; public class Explosion : WarEntity { [SerializeField, Range(0f, 1f)] float duration = 0.5f; float age; public void Initialize (Vector3 position, float blastRadius, float damage) { TargetPoint.FillBuffer(position, blastRadius); for (int i = 0; i < TargetPoint.BufferedCount; i++) { TargetPoint.GetBuffered(i).Enemy.ApplyDamage(damage); } transform.localPosition = position; transform.localScale = Vector3.one * (2f * blastRadius); } public override bool GameUpdate () { age += Time.deltaTime; if (age >= duration) { OriginFactory.Reclaim(this); return false; } return true; } }
إضافة انفجار ل WarFactory
. [SerializeField] Explosion explosionPrefab = default; [SerializeField] Shell shellPrefab = default; public Explosion Explosion€ => Get(explosionPrefab); public Shell Shell => Get(shellPrefab);
مصنع الحرب مع انفجار.أضف أيضًا إلى Game
طريقة الواجهة. public static Explosion SpawnExplosion () { Explosion explosion = instance.warFactory.Explosion€; instance.nonEnemies.Add(explosion); return explosion; }
الآن Shell
يمكنه توليد وبدء انفجار عند الوصول إلى الهدف. الانفجار نفسه سوف يسبب الضرر. if (py <= 0f) { Game.SpawnExplosion().Initialize(targetPoint, blastRadius, damage); OriginFactory.Reclaim(this); return false; }
انفجارات القذائف.انفجارات أكثر سلاسة
لا تبدو الأجسام الثابتة بدلاً من الانفجارات جميلة جدًا. يمكنك تحسينها عن طريق تنشيط التعتيم والحجم. يمكنك استخدام صيغة بسيطة لهذا ، ولكن دعنا نستخدم منحنيات الرسوم المتحركة التي يسهل إعدادها. إضافة لهذا Explosion
الحقلين التكوين AnimationCurve
. سنستخدم المنحنيات لضبط القيم على مدار عمر الانفجار ، وسيشير الوقت 1 إلى نهاية الانفجار ، بغض النظر عن مدته الحقيقية. الأمر نفسه ينطبق على حجم وشعاع الانفجار. هذا سوف تبسيط التكوين الخاصة بهم. [SerializeField] AnimationCurve opacityCurve = default; [SerializeField] AnimationCurve scaleCurve = default;
سيبدأ التعتيم وينتهي بصفر ، ويتم قياسه بسلاسة إلى قيمة متوسطة تبلغ 0.3. سيبدأ المقياس عند 0.7 ، ويزيد بسرعة ، ثم يقترب ببطء من 1.منحنيات الانفجار.لتعيين لون المادة ، سنستخدم كتلة خصائص المواد. حيث الأسود هو متغير العتامة. تم ضبط المقياس الآن على GameUpdate
، لكننا بحاجة إلى التتبع باستخدام مجال نصف القطر. في Initialize
يمكنك استخدام مقياس مضاعفة. تم العثور على قيم المنحنيات عن طريق الاتصال بهم Evaluate
باستخدام وسيطة ، يتم حسابها على أنها العمر الحالي للانفجار ، مقسومًا على مدة الانفجار. static int colorPropertyID = Shader.PropertyToID("_Color"); static MaterialPropertyBlock propertyBlock; … float scale; MeshRenderer meshRenderer; void Awake () { meshRenderer = GetComponent<MeshRenderer>(); Debug.Assert(meshRenderer != null, "Explosion without renderer!"); } public void Initialize (Vector3 position, float blastRadius, float damage) { … transform.localPosition = position; scale = 2f * blastRadius; } public override bool GameUpdate () { … if (propertyBlock == null) { propertyBlock = new MaterialPropertyBlock(); } float t = age / duration; Color c = Color.clear; ca = opacityCurve.Evaluate(t); propertyBlock.SetColor(colorPropertyID, c); meshRenderer.SetPropertyBlock(propertyBlock); transform.localScale = Vector3.one * (scale * scaleCurve.Evaluate(t)); return true; }
انفجارات متحركة.قذائف التتبع
نظرًا لأن القذائف صغيرة ولديها سرعة عالية إلى حد ما ، فقد يكون من الصعب ملاحظة ذلك. وإذا نظرت إلى لقطة شاشة لإطار واحد ، فإن المسارات غير مفهومة تمامًا. يمكنك جعلها أكثر وضوحًا عن طريق إضافة تأثير تتبع إلى الأصداف. بالنسبة للقذائف التقليدية ، هذا ليس واقعياً للغاية ، لكن يمكننا القول أن هذه عبارة عن مستكشفين. تم صنع هذه الذخيرة خصيصًا بحيث تترك علامة مضيئة ، مما يجعل مساراتها مرئية.هناك طرق مختلفة لإنشاء آثار ، لكنك ستستخدم طريقة بسيطة للغاية. نحن نعيد صنع الانفجارات بحيث Shell
يحدث انفجار صغير في كل إطار. لن تتسبب هذه الانفجارات في أي ضرر ، لذا فإن التقاط الأهداف سيكون مضيعة للموارد. أضف إلىExplosion
يتم دعم هذا الاستخدام عن طريق جعل الضرر إذا كان أكبر من الصفر ، ثم جعل المعلمة الضرر Initialize
اختيارية. public void Initialize ( Vector3 position, float blastRadius, float damage = 0f ) { if (damage > 0f) { TargetPoint.FillBuffer(position, blastRadius); for (int i = 0; i < TargetPoint.BufferedCount; i++) { TargetPoint.GetBuffered(i).Enemy.ApplyDamage(damage); } } transform.localPosition = position; radius = 2f * blastRadius; }
سننشئ انفجارًا في النهاية Shell.GameUpdate
بنصف قطر صغير ، على سبيل المثال 0.1 ، لتحويلها إلى قذائف تتبع. تجدر الإشارة إلى أنه من خلال هذا النهج ، سيتم إنشاء الانفجارات إطارًا تلو الآخر ، أي أنها تعتمد على معدل الإطار ، لكن لهذا التأثير البسيط يكون هذا مقبولًا. public override bool GameUpdate () { … Game.SpawnExplosion().Initialize(p, 0.1f); return true; }
مقتطفات قذيفة.مستودع تعليميPDF المادة