الأجزاء 1-3: الشبكة والألوان وارتفاعات الخليةالأجزاء 4-7: المطبات والأنهار والطرقالأجزاء 8-11: الماء والأشكال الأرضية والأسوارالأجزاء 12-15: الحفظ والتحميل ، القوام ، المسافاتالأجزاء 16-19: إيجاد الطريق وفرق اللاعبين والرسوم المتحركةالأجزاء 20-23: ضباب الحرب ، بحث الخرائط ، الجيل الإجرائيالأجزاء 24-27: دورة الماء ، التآكل ، المناطق الأحيائية ، الخريطة الأسطوانيةالجزء الرابع: الخشونة
جدول المحتويات
- اختبر نسيج الضجيج.
- تحريك القمم.
- نحافظ على تسطيح الخلايا.
- تقسيم حواف الخلايا.
بينما كانت شبكتنا نمطًا صارمًا من أقراص العسل. في هذا الجزء ، سنضيف نتوءات لجعل الخريطة تبدو أكثر طبيعية.
لا مزيد من السداسيات.الضجيج
لإضافة المطبات ، نحتاج إلى التوزيع العشوائي ، ولكن ليس العشوائية الحقيقية. نريد أن يكون كل شيء متسقًا عند تغيير الخريطة. خلاف ذلك ، عند إجراء أي تغيير ، ستقفز الكائنات. أي أننا بحاجة إلى شكل من أشكال الضوضاء العشوائية الزائفة القابلة للتكرار.
المرشح الجيد هو ضجيج بيرلين. يمكن استنساخه في أي مكان. عند الجمع بين عدة ترددات ، فإنه يخلق أيضًا ضوضاء ، والتي يمكن أن تختلف اختلافًا كبيرًا عند مسافات كبيرة ، ولكنها تظل كما هي تقريبًا في المسافات الصغيرة. بفضل هذا ، يمكن إنشاء تشوهات سلسة نسبيًا. عادةً ما تبقى النقاط المجاورة لبعضها البعض في مكان قريب ، وليست مبعثرة في اتجاهات معاكسة.
يمكننا توليد ضوضاء بيرلين برمجياً. في شرح
الضوضاء ، أشرح كيفية القيام بذلك. ولكن يمكننا أيضًا أخذ عينات من نسيج ضوضاء تم إنشاؤه مسبقًا. ميزة استخدام الملمس هي أنه أبسط وأسرع بكثير من حساب ضجيج بيرلين متعدد الترددات. عيبه هو أن الملمس يشغل مساحة أكبر من الذاكرة ويغطي مساحة صغيرة فقط من الضوضاء. لذلك ، يجب أن يكون متصلاً بسلاسة وكبيرًا بما يكفي بحيث لا يكون التكرار مدهشًا.
نسيج الضوضاء
سنستخدم الملمس ، لذلك فإن البرنامج التعليمي
للضوضاء اختياري. لذلك نحن بحاجة إلى نسيج. ها هي:
ربط نسيج الضوضاء بيرلين بسلاسة.يحتوي الملمس الموضح أعلاه على ضوضاء بيرلين المقترنة بسلاسة متعددة الترددات. هذه صورة رمادية. متوسط قيمتها 0.5 ، وتميل القيم القصوى إلى 0 و 1.
ولكن انتظر ، هناك قيمة واحدة فقط لكل نقطة. إذا كنا بحاجة إلى تشويه ثلاثي الأبعاد ، فإننا نحتاج على الأقل إلى ثلاث عينات عشوائية زائفة! لذلك ، نحتاج إلى نسيجين آخرين بضجيج مختلف.
يمكننا إنشاؤها أو تخزين قيم ضوضاء مختلفة في كل من قنوات الألوان. سيسمح لنا هذا بتخزين ما يصل إلى أربعة أنماط ضوضاء في نسيج واحد. هنا هذا الملمس.
أربعة في واحد.كيفية إنشاء مثل هذا الملمس؟لقد استخدمت
NumberFlow . هذا هو محرر النسيج الإجرائي الذي أنشأته لـ Unity.
قم بتنزيل هذا الملمس واستيراده في مشروع الوحدة الخاص بك. نظرًا لأننا سنقوم بتجربة المادة من خلال التعليمات البرمجية ، فيجب أن تكون قابلة للقراءة. قم
بتبديل نوع مادة إلى
متقدمة وتمكين تمكين
القراءة / الكتابة . سيحفظ هذا بيانات النسيج في الذاكرة ويمكن الوصول إليه من كود C #. اضبط
التنسيق على
Truecolor التلقائي ، وإلا لن يعمل أي شيء. لا نريد أن يؤدي ضغط الملمس إلى تدمير نمط الضجيج لدينا.
يمكنك تعطيل
إنشاء خرائط Mip ، لأننا لسنا بحاجة إليها. قم أيضًا بتمكين
Bypass sRGB Sampling . لن نحتاج هذا ، لكنه سيكون كذلك. تشير هذه المعلمة إلى أن النسيج لا يحتوي على بيانات اللون في مساحة جاما.
نسيج ضجيج مستورد.
متى يكون أخذ عينات sRGB مهمًا؟إذا أردنا استخدام نسيج في تظليل ، فإن ذلك سيحدث فرقًا. عند استخدام وضع التقديم الخطي ، يؤدي أخذ عينات النسيج تلقائيًا إلى تحويل بيانات اللون من النطاق إلى مساحة لون خطية. في حالة نسيج الضجيج لدينا ، سيؤدي هذا إلى نتائج غير صحيحة ، لذلك لا نحتاج إلى ذلك.
لماذا تبدو إعدادات استيراد النسيج مختلفة؟تم تغييرها بعد كتابة هذا البرنامج التعليمي. تحتاج إلى استخدام إعدادات نسيج ثنائي الأبعاد الافتراضية ، ويجب تعطيل sRGB (Color Texture) ، ويجب ضبط الضغط على None .
أخذ عينات الضوضاء
دعنا نضيف وظيفة أخذ عينات الضوضاء إلى
HexMetrics
بحيث يمكنك استخدامها في أي مكان. هذا يعني أن
HexMetrics
يجب أن يحتوي على إشارة إلى نسيج الضوضاء.
public static Texture2D noiseSource;
نظرًا لأن هذا ليس مكونًا ، لا يمكننا تعيينه من خلال المحرر. لذلك ، كوسيط ، نستخدم
HexGrid
. نظرًا لأن
HexGrid
ستعمل أولاً ، فسيكون من الجيد إذا مررنا النسيج في بداية طريقة
Awake
.
public Texture2D noiseSource; void Awake () { HexMetrics.noiseSource = noiseSource; … }
ومع ذلك ، لن ينجو هذا النهج من إعادة الترجمة في وضع التشغيل. لا يقوم محرك الوحدة بتسلسل المتغيرات الثابتة. لحل هذه المشكلة ، قم بإعادة تعيين الملمس في طريقة حدث
OnEnable
. سيتم استدعاء هذه الطريقة بعد إعادة الترجمة.
void OnEnable () { HexMetrics.noiseSource = noiseSource; }
تعيين نسيج الضجيج.الآن بعد أن
HexMetrics
لديه حق الوصول إلى الملمس ، دعنا نضيف طريقة ملائمة لعينات الضوضاء إليها. تأخذ هذه الطريقة موقعًا في العالم وتنشئ متجهًا رباعي الأبعاد يحتوي على أربع عينات ضوضاء.
public static Vector4 SampleNoise (Vector3 position) { }
تم إنشاء عينات عن طريق أخذ عينات من النسيج باستخدام التصفية الثنائية ، حيث تم استخدام إحداثيات العالم X و Z كم إحداثيات للأشعة فوق البنفسجية.بما أن مصدر الضوضاء لدينا ثنائي الأبعاد ، فإننا نتجاهل الإحداثيات الثالثة في العالم. إذا كان مصدر الضوضاء ثلاثي الأبعاد ، فسنستخدم أيضًا إحداثيات ص.
ونتيجة لذلك ، نحصل على لون يمكن تحويله إلى ناقل رباعي الأبعاد. يمكن أن يكون هذا التخفيض غير مباشر ، أي يمكننا إرجاع اللون مباشرة ، وليس بما في ذلك صراحة
(Vector4)
.
public static Vector4 SampleNoise (Vector3 position) { return noiseSource.GetPixelBilinear(position.x, position.z); }
حزمة الوحدةحركة قمة الرأس
سوف نقوم بتشويه شبكتنا الناعمة من أقراص العسل ، مع تحريك كل رأس من القمم بشكل فردي. للقيام بذلك ، دعنا نضيف أسلوب
Perturb
إلى
Perturb
. يأخذ نقطة غير متحرك ويعيد النقطة المنقولة. للقيام بذلك ، يستخدم نقطة غير متغيرة عند أخذ عينات الضوضاء.
Vector3 Perturb (Vector3 position) { Vector4 sample = HexMetrics.SampleNoise(position); }
دعنا فقط نضيف عينات الضجيج X و Y و Z مباشرة إلى إحداثيات النقطة المقابلة ونستخدمها نتيجة لذلك.
Vector3 Perturb (Vector3 position) { Vector4 sample = HexMetrics.SampleNoise(position); position.x += sample.x; position.y += sample.y; position.z += sample.z; return position; }
كيف نغير
HexMesh
بسرعة لتحريك جميع القمم؟ بتغيير كل قمة عند إضافة رؤوس إلى القائمة في
AddTriangle
و
AddQuad
. فلنفعل ذلك.
void AddTriangle (Vector3 v1, Vector3 v2, Vector3 v3) { int vertexIndex = vertices.Count; vertices.Add(Perturb(v1)); vertices.Add(Perturb(v2)); vertices.Add(Perturb(v3)); … } void AddQuad (Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4) { int vertexIndex = vertices.Count; vertices.Add(Perturb(v1)); vertices.Add(Perturb(v2)); vertices.Add(Perturb(v3)); vertices.Add(Perturb(v4)); … }
هل ستبقى الزوايا الرباعية مسطحة بعد تحريك رؤوسها؟على الأرجح لا. تتكون من مثلثين لن يكونا بعد ذلك في نفس المستوى. ومع ذلك ، نظرًا لأن هذه المثلثات لها رأسان مشتركان ، فسيتم تنعيم القواعد العادية لهذه القمم. هذا يعني أنه لن يكون لدينا انتقالات حادة بين مثلثين. إذا لم يكن التشويه كبيرًا جدًا ، فسوف نظل ننظر إلى الزوايا الرباعية على أنها مسطحة.
يتم تحريك القمم أم لا.في حين أن التغييرات ليست ملحوظة للغاية ، اختفت فقط تسميات الخلايا. حدث هذا لأننا أضفنا عينات ضوضاء إلى النقاط ، وهي دائمًا إيجابية. لذلك ، نتيجة لذلك ، ارتفعت جميع المثلثات فوق علاماتها ، مما أدى إلى إغلاقها. يجب أن نركز التغييرات حتى تحدث في كلا الاتجاهين. قم بتغيير الفاصل الزمني لعينة الضوضاء من 0-1 إلى -1-1.
Vector3 Perturb (Vector3 position) { Vector4 sample = HexMetrics.SampleNoise(position); position.x += sample.x * 2f - 1f; position.y += sample.y * 2f - 1f; position.z += sample.z * 2f - 1f; return position; }
نزوح مركزي.حجم (قوة) النزوح
الآن من الواضح أننا قمنا بتشويه الشبكة ، ولكن التأثير بالكاد ملحوظ. التغيير في كل بُعد لا يزيد عن وحدة واحدة. أي أن الإزاحة القصوى النظرية هي √3 ≈ 1.73 وحدة ، وهو ما يحدث نادرًا للغاية ، على الإطلاق. بما أن نصف القطر الخارجي للخلايا هو 10 وحدات ، فإن الإزاحة صغيرة نسبيًا.
الحل هو إضافة معلمة
HexMetrics
إلى
HexMetrics
بحيث يمكنك تحجيم الحركات. دعنا نحاول استخدام القوة 5. في هذه الحالة ، سيكون الإزاحة القصوى النظرية √75 ≈ 8.66 وحدة ، وهو أكثر وضوحًا.
public const float cellPerturbStrength = 5f;
نطبق القوة بضربها بعينات في
HexMesh.Perturb
.
Vector3 Perturb (Vector3 position) { Vector4 sample = HexMetrics.SampleNoise(position); position.x += (sample.x * 2f - 1f) * HexMetrics.cellPerturbStrength; position.y += (sample.y * 2f - 1f) * HexMetrics.cellPerturbStrength; position.z += (sample.z * 2f - 1f) * HexMetrics.cellPerturbStrength; return position; }
زيادة القوة.مقياس الضوضاء
على الرغم من أن الشبكة تبدو جيدة قبل التغيير ، إلا أن كل شيء قد يسوء بعد ظهور الحواف. يمكن أن تشوه قممهم في اتجاهات مختلفة لا يمكن التنبؤ بها ، وخلق الفوضى. عند استخدام ضوضاء Perlin ، لا ينبغي أن يحدث هذا.
تبرز المشكلة لأننا نستخدم إحداثيات العالم بشكل مباشر لأخذ الضجيج. وبسبب هذا ، يتم إخفاء النسيج من خلال كل وحدة ، والخلايا أكبر بكثير من هذه القيمة. في الواقع ، يتم أخذ عينات النسيج في نقاط عشوائية ، مما يؤدي إلى تدمير سلامته الحالية.
صفوف من 10 إلى 10 خلايا متداخلة في الشبكة.سيتعين علينا توسيع نطاق أخذ عينات الضوضاء بحيث يغطي النسيج مساحة أكبر بكثير. دعنا نضيف هذا المقياس إلى
HexMetrics
قيمة 0.003 ، ثم
HexMetrics
إحداثيات العينات بهذا العامل.
public const float noiseScale = 0.003f; public static Vector4 SampleNoise (Vector3 position) { return noiseSource.GetPixelBilinear( position.x * noiseScale, position.z * noiseScale ); }
اتضح فجأة أن نسيجنا يغطي 333 & frac13؛ الوحدات المربعة ، وتصبح سلامتها المحلية واضحة.
ضوضاء متدرجة.بالإضافة إلى ذلك ، مقياس جديد يزيد المسافة بين مفاصل الضوضاء. في الواقع ، نظرًا لأن الخلايا يبلغ قطرها الداخلي 10√3 وحدة ، فلن يتم تجانبها تمامًا في البعد X. ومع ذلك ، نظرًا للسلامة المحلية للضوضاء ، على نطاق أوسع ، سنظل قادرين على التعرف على أنماط التكرار ، كل 20 خلية تقريبًا ، حتى لو لم تتطابق التفاصيل. لكنها ستكون واضحة فقط على الخريطة بدون ميزات مميزة أخرى.
حزمة الوحدةمحاذاة مراكز الخلايا
تحريك جميع القمم يعطي الخريطة مظهرًا أكثر طبيعية ، ولكن هناك العديد من المشاكل. بما أن الخلايا متعرجة الآن ، تتقاطع تسمياتها مع الشبكة. وفي مفاصل الحواف بالمنحدرات ، تنشأ شقوق. سنترك الشقوق في وقت لاحق ، لكننا سنركز الآن على أسطح الخلايا.
أصبحت الخريطة أقل صرامة ، ولكن ظهرت المزيد من المشاكل.أسهل طريقة لحل مشكلة التقاطع هي جعل مراكز الخلايا مسطحة. دعنا لا نغير إحداثيات Y في
HexMesh.Perturb
.
Vector3 Perturb (Vector3 position) { Vector4 sample = HexMetrics.SampleNoise(position); position.x += (sample.x * 2f - 1f) * HexMetrics.cellPerturbStrength;
محاذاة الخلايا.مع هذا التغيير ، ستبقى جميع المواضع الرأسية دون تغيير ، سواء في مراكز الخلايا أو على درجات الحواف. وتجدر الإشارة إلى أن هذا يقلل من الإزاحة القصوى إلى √50 ≈ 7.07 فقط في المستوى XZ.
هذا تغيير جيد ، لأنه يبسط تحديد الخلايا الفردية ولا يسمح للحواف بأن تصبح فوضوية للغاية. ولكن سيكون من الجميل إضافة حركة رأسية صغيرة.
تحريك ارتفاع الخلية
بدلاً من تطبيق الحركة الرأسية على كل قمة ، يمكننا تطبيقها على خلية. في هذه الحالة ، ستظل كل خلية مسطحة ، ولكن سيظل التباين بين الخلايا. سيكون من المنطقي أيضًا استخدام مقياس مختلف لتحريك الارتفاع ، لذا قم بإضافته إلى
HexMetrics
. تخلق قوة 1.5 وحدة اختلافًا طفيفًا ، يساوي تقريبًا ارتفاع خطوة واحدة من الحافة.
public const float elevationPerturbStrength = 1.5f;
تغيير خاصية
HexCell.Elevation
بحيث يتم
HexCell.Elevation
هذا الانتقال إلى الموضع العمودي للخلية.
public int Elevation { get { return elevation; } set { elevation = value; Vector3 position = transform.localPosition; position.y = value * HexMetrics.elevationStep; position.y += (HexMetrics.SampleNoise(position).y * 2f - 1f) * HexMetrics.elevationPerturbStrength; transform.localPosition = position; Vector3 uiPosition = uiRect.localPosition; uiPosition.z = -position.y; uiRect.localPosition = uiPosition; } }
من أجل تطبيق النقل على الفور ، نحتاج إلى ضبط ارتفاع كل خلية بشكل
HexGrid.CreateCell
في
HexGrid.CreateCell
. خلاف ذلك ، ستكون الشبكة في البداية مسطحة. لنقم بذلك في النهاية ، بعد إنشاء واجهة المستخدم.
void CreateCell (int x, int z, int i) { … cell.Elevation = 0; }
ارتفاعات النازحين مع الشقوق.باستخدام نفس الارتفاعات
ظهرت العديد من الشقوق في الشبكة ، لأنه عندما نقوم بتثليث الشبكة ، فإننا لا نستخدم نفس ارتفاعات الخلية. دعنا نضيف خاصية إلى
HexCell
للحصول على موقعها بحيث يمكنك استخدامه في أي مكان.
public Vector3 Position { get { return transform.localPosition; } }
الآن يمكننا استخدام هذه الخاصية في
HexMesh.Triangulate
لتحديد مركز الخلية.
void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.Position; … }
ويمكننا استخدامه في
TriangulateConnection
عند تحديد المواضع الرأسية للخلايا المجاورة.
void TriangulateConnection ( HexDirection direction, HexCell cell, Vector3 v1, Vector3 v2 ) { … Vector3 bridge = HexMetrics.GetBridge(direction); Vector3 v3 = v1 + bridge; Vector3 v4 = v2 + bridge; v3.y = v4.y = neighbor.Position.y; … HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (direction <= HexDirection.E && nextNeighbor != null) { Vector3 v5 = v2 + HexMetrics.GetBridge(direction.Next()); v5.y = nextNeighbor.Position.y; … } }
الاستخدام المتسق لارتفاع الخلية.حزمة الوحدةوحدة حافة الخلية
على الرغم من أن الخلايا لديها اختلاف جميل ، إلا أنها لا تزال تبدو وكأنها سداسيات واضحة. هذا في حد ذاته ليس مشكلة ، ولكن يمكننا تحسين مظهرهم.
خلايا سداسية واضحة للعيان.إذا كان لدينا المزيد من القمم ، فسيكون هناك قدر أكبر من التباين المحلي. لذا دعونا نقسم كل حافة من الخلية إلى جزأين عن طريق إضافة الجزء العلوي من الحافة في المنتصف بين كل زوج من الزوايا. هذا يعني أن
HexMesh.Triangulate
يجب ألا تضيف
HexMesh.Triangulate
واحدًا ، بل مثلثين.
void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.Position; Vector3 v1 = center + HexMetrics.GetFirstSolidCorner(direction); Vector3 v2 = center + HexMetrics.GetSecondSolidCorner(direction); Vector3 e1 = Vector3.Lerp(v1, v2, 0.5f); AddTriangle(center, v1, e1); AddTriangleColor(cell.color); AddTriangle(center, e1, v2); AddTriangleColor(cell.color); if (direction <= HexDirection.SE) { TriangulateConnection(direction, cell, v1, v2); } }
اثنا عشر جانبًا بدلاً من ستة.يضيف تضاعف القمم والمثلثات المزيد من التباين إلى حواف الخلية. دعونا نجعلها أكثر تفاوتًا من خلال زيادة عدد القمم بثلاثة أضعاف.
Vector3 e1 = Vector3.Lerp(v1, v2, 1f / 3f); Vector3 e2 = Vector3.Lerp(v1, v2, 2f / 3f); AddTriangle(center, v1, e1); AddTriangleColor(cell.color); AddTriangle(center, e1, e2); AddTriangleColor(cell.color); AddTriangle(center, e2, v2); AddTriangleColor(cell.color);
18 جانب.قسم الضلع المشترك
بالطبع ، نحتاج أيضًا إلى تقسيم مفاصل الحافة. لذلك ، سنمرر حواف القمة الجديدة إلى
TriangulateConnection
.
if (direction <= HexDirection.SE) { TriangulateConnection(direction, cell, v1, e1, e2, v2); }
أضف المعلمات المناسبة إلى
TriangulateConnection
حتى يمكنها العمل مع القمم الإضافية.
void TriangulateConnection ( HexDirection direction, HexCell cell, Vector3 v1, Vector3 e1, Vector3 e2, Vector3 v2 ) { … }
نحتاج أيضًا إلى حساب الحواف الإضافية للحواف للخلايا المجاورة. يمكننا حسابها بعد توصيل الجسر بالجانب الآخر.
Vector3 bridge = HexMetrics.GetBridge(direction); Vector3 v3 = v1 + bridge; Vector3 v4 = v2 + bridge; v3.y = v4.y = neighbor.Position.y; Vector3 e3 = Vector3.Lerp(v3, v4, 1f / 3f); Vector3 e4 = Vector3.Lerp(v3, v4, 2f / 3f);
بعد ذلك نحتاج إلى تغيير تثليث الضلع. حتى نتجاهل المنحدرات بالحواف ، أضف ثلاثة فقط بدلاً من رباعية واحدة.
if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(v1, v2, cell, v3, v4, neighbor); } else { AddQuad(v1, e1, v3, e3); AddQuadColor(cell.color, neighbor.color); AddQuad(e1, e2, e3, e4); AddQuadColor(cell.color, neighbor.color); AddQuad(e2, v2, e4, v4); AddQuadColor(cell.color, neighbor.color); }
اتصالات مقسمة.اتحاد حواف الحواف
نظرًا لوصف الحواف ، نحتاج الآن إلى أربعة رؤوس ، سيكون من المنطقي دمجها في مجموعة. هذا أكثر ملاءمة من العمل مع أربعة رؤوس مستقلة. قم
EdgeVertices
بنية
EdgeVertices
بسيطة لهذا. يجب أن يحتوي على أربعة رؤوس تتحرك باتجاه عقارب الساعة على طول حافة الخلية.
using UnityEngine; public struct EdgeVertices { public Vector3 v1, v2, v3, v4; }
ألا يجب أن تكون قابلة للتسلسل؟سنستخدم هذا الهيكل فقط للتثليث. في هذه المرحلة ، لا نحتاج إلى تخزين رؤوس الحواف ، لذلك ليس من الضروري أن تكون قابلة للتسلسل.
أضف طريقة بناء مناسبة لها ، والتي ستتعامل مع حساب النقاط الوسيطة للحافة.
public EdgeVertices (Vector3 corner1, Vector3 corner2) { v1 = corner1; v2 = Vector3.Lerp(corner1, corner2, 1f / 3f); v3 = Vector3.Lerp(corner1, corner2, 2f / 3f); v4 = corner2; }
يمكننا الآن إضافة طريقة تثليث منفصلة إلى
HexMesh
لإنشاء مروحة من المثلثات بين مركز الخلية وأحد حوافها.
void TriangulateEdgeFan (Vector3 center, EdgeVertices edge, Color color) { AddTriangle(center, edge.v1, edge.v2); AddTriangleColor(color); AddTriangle(center, edge.v2, edge.v3); AddTriangleColor(color); AddTriangle(center, edge.v3, edge.v4); AddTriangleColor(color); }
وطريقة لتثليث شريط رباعي الزوايا بين حافتين.
void TriangulateEdgeStrip ( EdgeVertices e1, Color c1, EdgeVertices e2, Color c2 ) { AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); AddQuadColor(c1, c2); AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); AddQuadColor(c1, c2); AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); AddQuadColor(c1, c2); }
هذا سيسمح لنا بتبسيط طريقة
Triangulate
.
void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.Position; EdgeVertices e = new EdgeVertices( center + HexMetrics.GetFirstSolidCorner(direction), center + HexMetrics.GetSecondSolidCorner(direction) ); TriangulateEdgeFan(center, e, cell.color); if (direction <= HexDirection.SE) { TriangulateConnection(direction, cell, e); } }
دعنا ننتقل إلى
TriangulateConnection
. الآن يمكننا استخدام
TriangulateEdgeStrip
، ولكن يجب إجراء بدائل أخرى. حيث استخدمنا
v1
، نحتاج إلى استخدام
e1.v1
. وبالمثل ، يصبح
v2
e1.v4
، و
v3
يصبح
e2.v1
، و
v4
يصبح
e2.v4
.
void TriangulateConnection ( HexDirection direction, HexCell cell, EdgeVertices e1 ) { HexCell neighbor = cell.GetNeighbor(direction); if (neighbor == null) { return; } Vector3 bridge = HexMetrics.GetBridge(direction); bridge.y = neighbor.Position.y - cell.Position.y; EdgeVertices e2 = new EdgeVertices( e1.v1 + bridge, e1.v4 + bridge ); if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(e1.v1, e1.v4, cell, e2.v1, e2.v4, neighbor); } else { TriangulateEdgeStrip(e1, cell.color, e2, neighbor.color); } HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (direction <= HexDirection.E && nextNeighbor != null) { Vector3 v5 = e1.v4 + HexMetrics.GetBridge(direction.Next()); v5.y = nextNeighbor.Position.y; if (cell.Elevation <= neighbor.Elevation) { if (cell.Elevation <= nextNeighbor.Elevation) { TriangulateCorner( e1.v4, cell, e2.v4, neighbor, v5, nextNeighbor ); } else { TriangulateCorner( v5, nextNeighbor, e1.v4, cell, e2.v4, neighbor ); } } else if (neighbor.Elevation <= nextNeighbor.Elevation) { TriangulateCorner( e2.v4, neighbor, v5, nextNeighbor, e1.v4, cell ); } else { TriangulateCorner( v5, nextNeighbor, e1.v4, cell, e2.v4, neighbor ); } }
تقسيم الحافة
نحن بحاجة لتقسيم الحواف. لذلك ، نمر الحواف إلى
TriangulateEdgeTerraces
.
if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(e1, cell, e2, neighbor); }
الآن نحن بحاجة إلى تعديل
TriangulateEdgeTerraces
بحيث يتم الاستيفاء بين الحواف وليس بين أزواج القمم. لنفترض أن
EdgeVertices
لديها طريقة ثابتة مناسبة للقيام بذلك. سيسمح لنا هذا بتبسيط
TriangulateEdgeTerraces
بدلاً من تعقيدها.
void TriangulateEdgeTerraces ( EdgeVertices begin, HexCell beginCell, EdgeVertices end, HexCell endCell ) { EdgeVertices e2 = EdgeVertices.TerraceLerp(begin, end, 1); Color c2 = HexMetrics.TerraceLerp(beginCell.color, endCell.color, 1); TriangulateEdgeStrip(begin, beginCell.color, e2, c2); for (int i = 2; i < HexMetrics.terraceSteps; i++) { EdgeVertices e1 = e2; Color c1 = c2; e2 = EdgeVertices.TerraceLerp(begin, end, i); c2 = HexMetrics.TerraceLerp(beginCell.color, endCell.color, i); TriangulateEdgeStrip(e1, c1, e2, c2); } TriangulateEdgeStrip(e2, c2, end, endCell.color); }
تقوم طريقة
EdgeVertices.TerraceLerp
ببساطة باستيفاء الحواف بين الأزواج الأربعة للرؤوس من
EdgeVertices.TerraceLerp
.
public static EdgeVertices TerraceLerp ( EdgeVertices a, EdgeVertices b, int step) { EdgeVertices result; result.v1 = HexMetrics.TerraceLerp(a.v1, b.v1, step); result.v2 = HexMetrics.TerraceLerp(a.v2, b.v2, step); result.v3 = HexMetrics.TerraceLerp(a.v3, b.v3, step); result.v4 = HexMetrics.TerraceLerp(a.v4, b.v4, step); return result; }
حواف مقسمة.حزمة الوحدةإعادة ربط المنحدرات والحواف
حتى الآن ، تجاهلنا الشقوق في تقاطع المنحدرات والحواف. حان الوقت لحل هذه المشكلة. دعونا أولاً نلقي نظرة على حالات المنحدر المنحدر المنحدر (OSS) والمنحدر المنحدر المنحدر (SOS).
ثقوب شبكة.تنشأ المشكلة لأن قمم الحدود قد تحركت. هذا يعني أنهم الآن لا يستلقون بالضبط على جانب الجرف ، مما يؤدي إلى تصدع. في بعض الأحيان تكون هذه الثقوب غير مرئية وأحيانًا لافتة للنظر.
الحل هو عدم تحريك الجزء العلوي من الحدود. هذا يعني أننا بحاجة للسيطرة على ما إذا كانت النقطة سيتم نقلها. أسهل طريقة هي إنشاء بديل
AddTriangle
لا يحرك القمم على الإطلاق.
void AddTriangleUnperturbed (Vector3 v1, Vector3 v2, Vector3 v3) { int vertexIndex = vertices.Count; vertices.Add(v1); vertices.Add(v2); vertices.Add(v3); triangles.Add(vertexIndex); triangles.Add(vertexIndex + 1); triangles.Add(vertexIndex + 2); }
قم بتغيير
TriangulateBoundaryTriangle
بحيث يستخدم هذا الأسلوب. هذا يعني أنه سيتعين عليه تحريك جميع القمم صراحة ، باستثناء الحدود.
void TriangulateBoundaryTriangle ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 boundary, Color boundaryColor ) { Vector3 v2 = HexMetrics.TerraceLerp(begin, left, 1); Color c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, 1); AddTriangleUnperturbed(Perturb(begin), Perturb(v2), boundary); AddTriangleColor(beginCell.color, c2, boundaryColor); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v2; Color c1 = c2; v2 = HexMetrics.TerraceLerp(begin, left, i); c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, i); AddTriangleUnperturbed(Perturb(v1), Perturb(v2), boundary); AddTriangleColor(c1, c2, boundaryColor); } AddTriangleUnperturbed(Perturb(v2), Perturb(left), boundary); AddTriangleColor(c2, leftCell.color, boundaryColor); }
تجدر الإشارة إلى ما يلي: نظرًا لأننا لا نستخدم
v2
للحصول على نقطة أخرى ، يمكننا نقلها على الفور. هذا تحسين بسيط ويقلل من كمية الكود ، لذلك دعونا نقدمه.
void TriangulateBoundaryTriangle ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 boundary, Color boundaryColor ) { Vector3 v2 = Perturb(HexMetrics.TerraceLerp(begin, left, 1)); Color c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, 1); AddTriangleUnperturbed(Perturb(begin), v2, boundary); AddTriangleColor(beginCell.color, c2, boundaryColor); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v2; Color c1 = c2; v2 = Perturb(HexMetrics.TerraceLerp(begin, left, i)); c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, i); AddTriangleUnperturbed(v1, v2, boundary); AddTriangleColor(c1, c2, boundaryColor); } AddTriangleUnperturbed(v2, Perturb(left), boundary); AddTriangleColor(c2, leftCell.color, boundaryColor); }
حدود غير منقولة.يبدو أفضل ، لكننا لم ننتهي بعد. داخل طريقة
TriangulateCornerTerracesCliff
، يتم استيفاء نقطة الحدود بين النقطتين اليمنى واليسرى. ومع ذلك ، لم يتم نقل هذه النقاط حتى الآن. لكي تتوافق نقطة الحدود مع الجرف الناتج ، نحتاج إلى الاستيفاء بين النقاط المنقولة.
Vector3 boundary = Vector3.Lerp(Perturb(begin), Perturb(right), b);
وينطبق الشيء نفسه على طريقة
TriangulateCornerCliffTerraces
.
Vector3 boundary = Vector3.Lerp(Perturb(begin), Perturb(left), b);
ذهبت الثقوب.منحدرات مزدوجة ومنحدر
في جميع الحالات الإشكالية المتبقية ، يوجد جرفان ومنحدر واحد.
حفرة كبيرة بسبب مثلث واحد.يتم حل هذه المشكلة عن طريق تحريك مثلث واحد يدويًا في الكتلة
else
في نهاية
TriangulateCornerTerracesCliff
.
else { AddTriangleUnperturbed(Perturb(left), Perturb(right), boundary); AddTriangleColor(leftCell.color, rightCell.color, boundaryColor); }
وينطبق الشيء نفسه على
TriangulateCornerCliffTerraces
.
else { AddTriangleUnperturbed(Perturb(left), Perturb(right), boundary); AddTriangleColor(leftCell.color, rightCell.color, boundaryColor); }
تخلص من أحدث الشقوق.حزمة الوحدةإتمام
الآن لدينا شبكة مشوهة صحيحة تمامًا. يعتمد مظهره على الضجيج المحدد وحجمه وقوى التشويه. في حالتنا ، قد يبدو التشويه قويًا جدًا. على الرغم من أن هذا التفاوت يبدو جميلًا ، إلا أننا لا نريد أن تنحرف الخلايا كثيرًا عن الشبكة الزوجية. في النهاية ، ما زلنا نستخدمها لتحديد الخلية التي سيتم تغيير حجمها. وإذا كان حجم الخلايا يختلف كثيرًا ، فسيكون من الصعب علينا وضع المحتويات فيها.شبكات غير مشوهة ومشوهة.يبدو أن القوة 5 لتشويه الخلايا كبيرة جدًا.تشويه الخلايا من 0 إلى 5.دعنا نخفضها إلى 4 لزيادة راحة الشبكة ، دون جعلها صحيحة للغاية. هذا يضمن أن أقصى إزاحة XZ ستكون √32 ≈ 5.66 وحدة. public const float cellPerturbStrength = 4f;
قوة تشويه الخلية 4.قيمة أخرى يمكن تغييرها هي معامل التكامل. إذا زدناها ، فستصبح المراكز المسطحة للخلايا أكبر ، أي أنه سيكون هناك مساحة أكبر للمحتوى المستقبلي. وبطبيعة الحال ، سيصبحون بذلك سداسية.معامل النزاهة من 0.75 إلى 0.95.زيادة طفيفة في معامل النزاهة إلى 0.8 ستبسط حياتنا قليلاً في المستقبل. public const float solidFactor = 0.8f;
معامل النزاهة 0.8.أخيرًا ، قد تلاحظ أن الاختلافات بين مستويات الارتفاع شديدة للغاية. هذا مناسب عندما تحتاج إلى التأكد من إنشاء الشبكة بشكل صحيح ، ولكننا قد انتهينا بالفعل من ذلك. دعنا نخفضها إلى وحدة واحدة لكل خطوة ، أي 3. public const float elevationStep = 3f;
يتم تقليل درجة الصوت إلى 3.يمكننا أيضًا تغيير قوة تشويه الملعب. ولكن الآن تبلغ قيمتها 1.5 ، وهو ما يعادل نصف خطوة في الارتفاع ، وهو ما يناسبنا.تسمح الدرجات الصغيرة من المرتفعات باستخدام أكثر منطقية لجميع مستويات الارتفاع السبعة. هذا يزيد من تنوع الخريطة.نستخدم سبعة مستويات من المرتفعات.حزمة الوحدةالجزء 5: بطاقات أكبر
- نقسم الشبكة إلى أجزاء.
- نحن نتحكم في الكاميرا.
- تلوين الألوان والارتفاعات بشكل منفصل.
- استخدم الفرشاة المكبرة للخلايا.
حتى الآن نحن نعمل مع خريطة صغيرة جدًا. حان الوقت لزيادته.حان الوقت للتكبير.شظايا شبكة
لا يمكننا جعل الشبكة كبيرة جدًا ، لأننا نواجه حدود ما يمكن احتواؤه في شبكة واحدة. كيف تحل هذه المشكلة؟ استخدم شبكات متعددة. للقيام بذلك ، نحتاج إلى تقسيم شبكتنا إلى عدة أجزاء. نستخدم شظايا مستطيلة ذات حجم ثابت.تقسيم الشبكة إلى أجزاء 3 × 3.دعنا نستخدم 5 × 5 كتل ، أي 25 خلية لكل جزء. حددها HexMetrics
. public const int chunkSizeX = 5, chunkSizeZ = 5;
ما حجم الجزء الذي يمكن اعتباره مناسبًا؟. , . . , (frustum culling), . .
الآن لا يمكننا استخدام أي حجم للشبكة ؛ يجب أن يكون مضاعفًا لحجم الجزء. لذلك ، دعنا نغيرها HexGrid
بحيث لا تحدد حجمها ليس في خلايا منفصلة ، ولكن في أجزاء. قم بتعيين الحجم الافتراضي على 4 × 3 أجزاء ، أي 12 جزءًا فقط أو 300 خلية. لذلك نحصل على بطاقة اختبار مريحة. public int chunkCountX = 4, chunkCountZ = 3;
فإننا نواصل استخدام width
و height
، ولكنهم الآن أصبحوا الخاص. وإعادة تسميتها بـ cellCountX
و cellCountZ
. استخدم المحرر لإعادة تسمية كل تكرارات هذه المتغيرات دفعة واحدة. الآن سيكون من الواضح عندما نتعامل مع عدد الأجزاء أو الخلايا.
حدد الحجم في الأجزاء.التغيير Awake
بحيث يتم حساب عدد الخلايا ، إذا لزم الأمر ، من عدد الأجزاء. نسلط الضوء على إنشاء الخلايا بطريقة منفصلة ، حتى لا تسد Awake
. void Awake () { HexMetrics.noiseSource = noiseSource; gridCanvas = GetComponentInChildren<Canvas>(); hexMesh = GetComponentInChildren<HexMesh>(); cellCountX = chunkCountX * HexMetrics.chunkSizeX; cellCountZ = chunkCountZ * HexMetrics.chunkSizeZ; CreateCells(); } void CreateCells () { cells = new HexCell[cellCountZ * cellCountX]; for (int z = 0, i = 0; z < cellCountZ; z++) { for (int x = 0; x < cellCountX; x++) { CreateCell(x, z, i++); } } }
شظايا الجاهزة
لوصف أجزاء الشبكة ، نحتاج إلى نوع جديد من المكونات. using UnityEngine; using UnityEngine.UI; public class HexGridChunk : MonoBehaviour { }
بعد ذلك سنقوم بإنشاء جزء الجاهزة. سنفعل ذلك بتكرار كائن Hex Grid وإعادة تسميته إلى Hex Grid Chunk . قم بإزالة المكون الخاص به HexGrid
وإضافة مكون بدلاً من ذلك HexGridChunk
. ثم قم بتحويله إلى سابق الإعداد وإزالة الكائن من المشهد.قطعة جاهزة مع قماش خاص بها وشبكة.نظرًا لأنه سيقوم بإنشاء حالات من هذه الأجزاء HexGrid
، فسوف نعطيه رابطًا إلى الهيكل الجاهز للجزء. public HexGridChunk chunkPrefab;
الآن مع شظايا.إنشاء مثيلات للشظايا يشبه إلى حد كبير إنشاء مثيلات للخلايا. سنتتبعهم بمساعدة مجموعة ، ونستخدم حلقة مزدوجة لملئه. HexGridChunk[] chunks; void Awake () { … CreateChunks(); CreateCells(); } void CreateChunks () { chunks = new HexGridChunk[chunkCountX * chunkCountZ]; for (int z = 0, i = 0; z < chunkCountZ; z++) { for (int x = 0; x < chunkCountX; x++) { HexGridChunk chunk = chunks[i++] = Instantiate(chunkPrefab); chunk.transform.SetParent(transform); } } }
يشبه تهيئة جزء ما كيفية تهيئة شبكة من السداسيات. إنها تضع كل شيء في Awake
الداخل وتقوم بإجراء التثليث Start
. يتطلب إشارة إلى قماشها وشبكتها ، بالإضافة إلى صفيف للخلايا. ومع ذلك ، فإن الجزء لن يخلق هذه الخلايا. ستستمر الشبكة في القيام بذلك. public class HexGridChunk : MonoBehaviour { HexCell[] cells; HexMesh hexMesh; Canvas gridCanvas; void Awake () { gridCanvas = GetComponentInChildren<Canvas>(); hexMesh = GetComponentInChildren<HexMesh>(); cells = new HexCell[HexMetrics.chunkSizeX * HexMetrics.chunkSizeZ]; } void Start () { hexMesh.Triangulate(cells); } }
تعيين الخلايا إلى أجزاء
HexGrid
لا يزال يخلق جميع الخلايا. هذا أمر طبيعي ، لكننا نحتاج الآن إلى إضافة كل خلية إلى جزء مناسب ، وعدم تعيينها باستخدام شبكتنا ولوحة القماش الخاصة بنا. void CreateCell (int x, int z, int i) { … HexCell cell = cells[i] = Instantiate<HexCell>(cellPrefab);
يمكننا العثور على جزء الصحيح باستخدام عدد صحيح تقسيم x
و z
على حجم شظية. void AddCellToChunk (int x, int z, HexCell cell) { int chunkX = x / HexMetrics.chunkSizeX; int chunkZ = z / HexMetrics.chunkSizeZ; HexGridChunk chunk = chunks[chunkX + chunkZ * chunkCountX]; }
باستخدام النتائج الوسيطة ، يمكننا أيضًا تحديد الفهرس المحلي للخلية في هذا الجزء. بعد ذلك ، يمكنك إضافة خلية إلى الجزء. void AddCellToChunk (int x, int z, HexCell cell) { int chunkX = x / HexMetrics.chunkSizeX; int chunkZ = z / HexMetrics.chunkSizeZ; HexGridChunk chunk = chunks[chunkX + chunkZ * chunkCountX]; int localX = x - chunkX * HexMetrics.chunkSizeX; int localZ = z - chunkZ * HexMetrics.chunkSizeZ; chunk.AddCell(localX + localZ * HexMetrics.chunkSizeX, cell); }
ثم HexGridChunk.AddCell
تضع الخلية في الصفيف الخاص بها ، ثم تقوم بتعيين العناصر الرئيسية للخلية وواجهة المستخدم الخاصة بها. public void AddCell (int index, HexCell cell) { cells[index] = cell; cell.transform.SetParent(transform, false); cell.uiRect.SetParent(gridCanvas.transform, false); }
اكتساح
عند هذه النقطة ، HexGrid
يمكنه التخلص من قماش أطفاله وشبكة سداسية ، بالإضافة إلى التعليمات البرمجية.
بما أننا تخلصنا Refresh
، HexMapEditor
لم يعد علينا استخدامه. void EditCell (HexCell cell) { cell.color = activeColor; cell.Elevation = activeElevation;
شبكة تنظيف السداسي.بعد بدء وضع التشغيل ، لا تزال البطاقة كما هي. لكن التسلسل الهرمي للأشياء سيكون مختلفًا. تُنشئ Hex Grid الآن كائنات فرعية مجزأة تحتوي على خلايا ، بالإضافة إلى الشبكة واللوحة.شظايا الأطفال في وضع التشغيل.ربما لدينا بعض المشاكل مع ملصقات الخلية. في البداية ، قمنا بتعيين عرض الملصق على 5. كان هذا كافيًا لعرض الحرفين اللذين كانا كافيين بالنسبة لنا على خريطة صغيرة. ولكن الآن يمكن أن يكون لدينا إحداثيات مثل −10 ، حيث يوجد ثلاثة أحرف. لن تكون مناسبة وسيتم قطعها. لإصلاح ذلك ، قم بزيادة عرض تسمية الخلية إلى 10 ، أو حتى أكثر.تسميات الخلايا الموسعة.الآن يمكننا إنشاء خرائط أكبر بكثير! نظرًا لأننا ننشئ الشبكة بالكامل عند بدء التشغيل ، فقد يستغرق إنشاء خرائط كبيرة وقتًا طويلاً. ولكن بعد الانتهاء ، سيكون لدينا مساحة كبيرة للتجريب.إصلاح تحرير الخلية
لا يبدو أن التحرير يعمل في المرحلة الحالية ، لأننا لم نعد نقوم بتحديث الشبكة. نحتاج إلى تحديث الأجزاء الفردية ، لذا أضف طريقة Refresh
إلى HexGridChunk
. public void Refresh () { hexMesh.Triangulate(cells); }
متى يجب أن نسمي هذه الطريقة؟ قمنا بتحديث الشبكة بأكملها في كل مرة لأنه كان لدينا شبكة واحدة فقط. ولكن الآن لدينا العديد من الشظايا. بدلاً من تحديثها كل مرة ، سيكون تحديث الأجزاء المتغيرة أكثر كفاءة. خلاف ذلك ، سوف يصبح تغيير البطاقات الكبيرة عملية بطيئة للغاية.ولكن كيف نعرف أي جزء يجب تحديثه؟ أسهل طريقة هي جعل كل خلية تعرف أي جزء تنتمي إليه. ثم ستتمكن الخلية من تحديث جزأها عند تغيير هذه الخلية. لذا دعونا نعطي HexCell
رابطًا لجزءه. public HexGridChunk chunk;
HexGridChunk
يمكن أن تضيف نفسها إلى الخلية عند الإضافة. public void AddCell (int index, HexCell cell) { cells[index] = cell; cell.chunk = this; cell.transform.SetParent(transform, false); cell.uiRect.SetParent(gridCanvas.transform, false); }
من خلال ربطها ، نضيف إلى HexCell
الطريقة Refresh
. في كل مرة يتم فيها تحديث خلية ، ستقوم ببساطة بتحديث جزءها. void Refresh () { chunk.Refresh(); }
لسنا بحاجة إلى جعلها HexCell.Refresh
شائعة ، لأن الخلية نفسها تعرف أفضل عندما يتم تغييرها. على سبيل المثال ، بعد تغيير ارتفاعه. public int Elevation { get { return elevation; } set { … Refresh(); } }
في الواقع ، نحتاج إلى تحديثه فقط عندما يتغير ارتفاعه إلى قيمة مختلفة. إنها لا تحتاج حتى إلى إعادة حساب أي شيء إذا قمنا بتعيينها بنفس الارتفاع كما كان من قبل. لذلك ، يمكننا الخروج من بداية المحدد. public int Elevation { get { return elevation; } set { if (elevation == value) { return; } … } }
ومع ذلك ، سنقوم أيضًا بتخطي الحسابات لأول مرة عندما يتم تعيين الارتفاع إلى 0 ، لأن هذه هي القيمة الافتراضية لارتفاع الشبكة. لتجنب ذلك ، سنجعل القيمة الأولية مثل عدم استخدامنا مطلقًا. int elevation = int.MinValue;
ما هو int.MinValue؟, integer. C# integer —
32- , 2 32 integer, , . .
— −2 31 = −2 147 483 648. !
2 31 − 1 = 2 147 483 647. 2 31 - .
للتعرف على تغير لون الخلية ، نحتاج أيضًا إلى تحويلها إلى خاصية. إعادة تسميته Color
بأحرف كبيرة ، ثم تحويله إلى خاصية ذات متغير خاص color
. ستكون قيمة اللون الافتراضية سوداء شفافة ، وهو ما يناسبنا. public Color Color { get { return color; } set { if (color == value) { return; } color = value; Refresh(); } } Color color;
الآن عندما نبدأ وضع التشغيل ، نحصل على استثناءات خالية من المرجع. يحدث هذا لأننا قمنا بتعيين اللون والارتفاع لقيمها الافتراضية قبل تعيين خلية إلى جزءها. من الطبيعي أننا لا نقوم بتحديث الأجزاء في هذه المرحلة ، لأننا نقوم بتثليثها بعد اكتمال جميع التهيئة. بمعنى آخر ، نقوم بتحديث جزء فقط إذا تم تعيينه. void Refresh () { if (chunk) { chunk.Refresh(); } }
يمكننا أخيرا تغيير الخلايا مرة أخرى! ومع ذلك ، تنشأ مشكلة. عند الرسم على طول حدود الشظايا ، تظهر اللحامات.أخطاء في حدود الشظايا.هذا أمر منطقي ، لأنه عندما تتغير خلية واحدة ، تتغير جميع الاتصالات مع جيرانها أيضًا. وقد يكون هؤلاء الجيران في أجزاء أخرى. الحل الأبسط هو تحديث جميع الخلايا المجاورة إذا كانت مختلفة. void Refresh () { if (chunk) { chunk.Refresh(); for (int i = 0; i < neighbors.Length; i++) { HexCell neighbor = neighbors[i]; if (neighbor != null && neighbor.chunk != chunk) { neighbor.chunk.Refresh(); } } } }
على الرغم من أن هذا يعمل ، فقد يتبين أننا نقوم بتحديث جزء واحد عدة مرات. وعندما نبدأ في تلوين عدة خلايا في وقت واحد ، سيزداد كل شيء سوءًا.لكننا لسنا مطالبين بالتثليث فورًا بعد تحديث الجزء. بدلاً من ذلك ، نكتب ببساطة أن التحديث مطلوب وثلاثي بعد اكتمال التغيير.نظرًا لأنه HexGridChunk
لا يفعل أي شيء آخر ، يمكننا استخدام حالته الممكنة للإشارة إلى الحاجة إلى التحديثات. عند تحديثه ، نقوم بتضمين المكون. لن يؤدي تشغيله عدة مرات إلى تغيير أي شيء. تم تحديث المكون لاحقًا. سنقوم بالتثليث عند هذه النقطة ونعطل المكون مرة أخرى.نستخدم LateUpdate
بدلاً من ذلكUpdate
لضمان حدوث التثليث بعد اكتمال التغيير للإطار الحالي. public void Refresh () {
ما الفرق بين التحديث والتحديث المتأخر؟Update
- . LateUpdate
. , .
نظرًا لأن المكون ممكّن افتراضيًا ، فإننا لم نعد بحاجة إلى التثليث بشكل صريح Start
. لذلك ، يمكن إزالة هذه الطريقة.
شظايا 20 × 20 تحتوي على 10000 خلية.القوائم المعممة
على الرغم من أننا قد قمنا بتغيير طريقة الشبكة بشكل كبير ، HexMesh
إلا أنها تظل كما هي. كل ما يحتاجه للعمل هو مجموعة من الخلايا. لا يهتم إذا كانت هناك شبكة واحدة من السداسيات ، أو العديد منها. لكننا لم نفكر بعد في استخدام شبكات متعددة. ربما يمكن تحسين شيء هنا؟ القوائمالمستخدمة HexMesh
هي في الأساس مخازن مؤقتة. يتم استخدامها فقط للتثليث. ويتم تثليث الشظايا في وقت واحد. لذلك ، في الواقع ، نحن بحاجة إلى مجموعة واحدة فقط من القوائم ، وليس مجموعة واحدة لكل كائن شبكة سداسي. يمكن تحقيق ذلك بجعل القوائم ثابتة. static List<Vector3> vertices = new List<Vector3>(); static List<Color> colors = new List<Color>(); static List<int> triangles = new List<int>(); void Awake () { GetComponent<MeshFilter>().mesh = hexMesh = new Mesh(); meshCollider = gameObject.AddComponent<MeshCollider>(); hexMesh.name = "Hex Mesh";
هل القوائم الثابتة مهمة حقًا؟. , , .
, . 20 20 100.
حزمة الوحدةالتحكم بالكاميرا
الكاميرا الكبيرة رائعة ، لكنها غير مجدية إذا لم نتمكن من رؤيتها. لفحص الخريطة بأكملها ، نحتاج إلى تحريك الكاميرا. التكبير مفيد أيضًا. لذلك ، دعنا ننشئ كاميرا لأداء هذه الإجراءات.قم بإنشاء كائن وهمي واسميها Hex Map Camera . إسقاط مكون التحويل الخاص به بحيث ينتقل إلى الأصل دون تغيير دورانه ومقياسه. إضافة طفل إليها يدعى Swivel ، وإضافة طفل إليها Stick . اجعل الكاميرا الرئيسية تابعة للعصا ، وأعد ضبط مكون التحويل الخاص بها.التسلسل الهرمي للكاميرا.الهدف من مفصل الكاميرا (Swivel) هو التحكم في الزاوية التي تنظر بها الكاميرا إلى الخريطة. فلنقم بدورها (45 ، 0 ، 0). يتحكم المقبض (Stick) في المسافة التي تقع بها الكاميرات. لنضعها في مركز (0 ، 0 ، -45).الآن نحن بحاجة إلى عنصر للتحكم في هذا النظام. قم بتعيين هذا المكون إلى جذر التسلسل الهرمي للكاميرا. أعطه رابطًا للمفصلة والمقبض ، وأدخلها Awake
. using UnityEngine; public class HexMapCamera : MonoBehaviour { Transform swivel, stick; void Awake () { swivel = transform.GetChild(0); stick = swivel.GetChild(0); } }
كاميرا خريطة سداسية.تكبير / تصغير
الوظيفة الأولى التي سننشئها هي التكبير (التكبير). يمكننا التحكم في مستوى الزوم الحالي باستخدام المتغير العائم. تعني القيمة 0 أننا بعيدون تمامًا ، وتعني القيمة 1 أننا قريبون تمامًا. لنبدأ بالتكبير الأقصى. float zoom = 1f;
يتم إجراء الزوم عادةً باستخدام عجلة الماوس أو التحكم التناظري. يمكننا تنفيذه باستخدام محور إدخال Mouse ScrollWheel الافتراضي. أضف طريقة Update
تتحقق من وجود دلتا الإدخال ، وإذا كان هناك واحدة ، فإنها تستدعي طريقة تغيير التكبير. void Update () { float zoomDelta = Input.GetAxis("Mouse ScrollWheel"); if (zoomDelta != 0f) { AdjustZoom(zoomDelta); } } void AdjustZoom (float delta) { }
لتغيير مستوى التكبير / التصغير ، نضيف ببساطة دلتا إليه ثم نحد من القيمة (المشبك) للبقاء في النطاق من 0 إلى 1. void AdjustZoom (float delta) { zoom = Mathf.Clamp01(zoom + delta); }
عند التكبير والتصغير ، يجب أن تتغير المسافة إلى الكاميرا وفقًا لذلك. يمكن القيام بذلك عن طريق تغيير موضع المقبض في Z. أضف متغيرين عائمين مشتركين لضبط موضع المقبض عند الحد الأدنى والحد الأقصى من الزوم. نظرًا لأننا نقوم بتطوير خريطة صغيرة نسبيًا ، قم بتعيين القيم على -250 و -45. public float stickMinZoom, stickMaxZoom;
بعد تغيير الزوم ، نقوم بإجراء استيفاء خطي بين هاتين القيمتين بناءً على قيمة الزوم الجديدة. ثم قم بتحديث موضع المقبض. void AdjustZoom (float delta) { zoom = Mathf.Clamp01(zoom + delta); float distance = Mathf.Lerp(stickMinZoom, stickMaxZoom, zoom); stick.localPosition = new Vector3(0f, 0f, distance); }
قيم الحد الأدنى والحد الأقصى للعصا.الآن يعمل التكبير ، ولكن حتى الآن ليست مفيدة للغاية. عادة ، عندما يكون الزووم بعيدًا ، تنتقل الكاميرا إلى منظر علوي. يمكننا أن ندرك ذلك عن طريق تدوير المفصلة. لذلك ، نضيف المتغيرات min و max للمفصلة. دعنا نحدد لهم القيم 90 و 45. public float swivelMinZoom, swivelMaxZoom;
كما هو الحال مع موضع المقبض ، فإننا نستكمل لإيجاد زاوية تقريب مناسبة. ثم نقوم بتعيين دوران المفصلة. void AdjustZoom (float delta) { zoom = Mathf.Clamp01(zoom + delta); float distance = Mathf.Lerp(stickMinZoom, stickMaxZoom, zoom); stick.localPosition = new Vector3(0f, 0f, distance); float angle = Mathf.Lerp(swivelMinZoom, swivelMaxZoom, zoom); swivel.localRotation = Quaternion.Euler(angle, 0f, 0f); }
الحد الأدنى والأقصى لقيمة Swivel.يمكن تعديل معدل تغيير الزوم عن طريق تغيير حساسية معلمات الإدخال لعجلة الماوس. يمكن العثور عليها في تحرير / إعدادات المشروع / الإدخال . على سبيل المثال ، عند تغييرها من 0.1 إلى 0.025 ، نحصل على تغيير أبطأ وأكثر سلاسة في التكبير.خيارات إدخال عجلة الماوس.تتحرك
الآن دعنا ننتقل إلى تحريك الكاميرا. الحركة في اتجاه X و Z يجب أن ننفذها Update
، كما في حالة الزوم. يمكننا استخدام هذه المدخلات محور أفقي و رأسي . هذا سيسمح لنا بتحريك الكاميرا مع الأسهم ومفاتيح WASD. void Update () { float zoomDelta = Input.GetAxis("Mouse ScrollWheel"); if (zoomDelta != 0f) { AdjustZoom(zoomDelta); } float xDelta = Input.GetAxis("Horizontal"); float zDelta = Input.GetAxis("Vertical"); if (xDelta != 0f || zDelta != 0f) { AdjustPosition(xDelta, zDelta); } } void AdjustPosition (float xDelta, float zDelta) { }
إن أبسط طريقة هي الحصول على الموضع الحالي لنظام الكاميرا ، وإضافة deltas X و Z إليه ، وتعيين النتيجة لموضع النظام. void AdjustPosition (float xDelta, float zDelta) { Vector3 position = transform.localPosition; position += new Vector3(xDelta, 0f, zDelta); transform.localPosition = position; }
ونتيجة لذلك ، ستتحرك الكاميرا أثناء حمل الأسهم أو WASD ، ولكن ليس بسرعة ثابتة. سيعتمد على معدل الإطار. لتحديد المسافة التي تحتاج إلى تحريكها ، نستخدم دلتا الوقت ، بالإضافة إلى السرعة المطلوبة. لذلك ، نضيف متغيرًا مشتركًا ونضبطه moveSpeed
على 100 ، ثم نضربه في دلتا الوقت للحصول على موضع دلتا. public float moveSpeed; void AdjustPosition (float xDelta, float zDelta) { float distance = moveSpeed * Time.deltaTime; Vector3 position = transform.localPosition; position += new Vector3(xDelta, 0f, zDelta) * distance; transform.localPosition = position; }
سرعة التحرك.يمكننا الآن التحرك بسرعة ثابتة على طول المحور X أو Z. ولكن عند التحرك على طول المحورين في نفس الوقت (قطريًا) ستكون الحركة أسرع. لإصلاح ذلك ، نحتاج إلى تطبيع متجه دلتا. سيسمح لك ذلك باستخدامه كوجهة. void AdjustPosition (float xDelta, float zDelta) { Vector3 direction = new Vector3(xDelta, 0f, zDelta).normalized; float distance = moveSpeed * Time.deltaTime; Vector3 position = transform.localPosition; position += direction * distance; transform.localPosition = position; }
يتم تنفيذ الحركة القطرية بشكل صحيح الآن ، ولكن فجأة اتضح أن الكاميرا تستمر في التحرك لفترة طويلة إلى حد ما حتى بعد تحرير جميع المفاتيح. يحدث هذا لأن محاور الإدخال لا تقفز على الفور إلى القيم الحدية فورًا بعد الضغط على المفاتيح. يحتاجون بعض الوقت لهذا. وينطبق الشيء نفسه على تحرير المفاتيح. يستغرق الأمر وقتًا للعودة إلى قيم المحور صفر. ومع ذلك ، نظرًا لأننا قمنا بتطبيع قيم الإدخال ، يتم الحفاظ على السرعة القصوى باستمرار.يمكننا ضبط معلمات الإدخال للتخلص من التأخيرات ، ولكنها تعطي شعورًا بالنعومة التي تستحق التوفير. يمكننا تطبيق أقصى قيمة للمحاور كمعامل التخميد للحركة. Vector3 direction = new Vector3(xDelta, 0f, zDelta).normalized; float damping = Mathf.Max(Mathf.Abs(xDelta), Mathf.Abs(zDelta)); float distance = moveSpeed * damping * Time.deltaTime;
الحركة مع التوهين.تعمل الحركة الآن بشكل جيد ، على الأقل مع زيادة التكبير. لكن على مسافة اتضح أنها بطيئة للغاية. مع تقليل التكبير ، نحتاج إلى تسريع. يمكن القيام بذلك عن طريق استبدال متغير واحد بمتغيرين moveSpeed
من أجل التكبير الأدنى والأقصى ، ثم الاستيفاء. قم بتعيين قيم 400 و 100 لهم.
تختلف سرعة الحركة باختلاف مستوى التكبير.الآن يمكننا التحرك بسرعة حول الخريطة! في الواقع ، يمكننا أن نتجاوز الخريطة ، لكن هذا غير مرغوب فيه. يجب أن تبقى الكاميرا داخل الخريطة. لضمان ذلك ، نحتاج إلى معرفة حدود الخريطة ، لذا يلزم وجود رابط بالشبكة. أضفه وربطه. public HexGrid grid;
تحتاج إلى طلب حجم الشبكة.بعد الانتقال إلى مركز جديد ، سنقوم بتحديده باستخدام الطريقة الجديدة. void AdjustPosition (float xDelta, float zDelta) { … transform.localPosition = ClampPosition(position); } Vector3 ClampPosition (Vector3 position) { return position; }
لا تقل قيمة الموضع X عن 0 ، ويتم تحديد الحد الأقصى من خلال حجم الخريطة. Vector3 ClampPosition (Vector3 position) { float xMax = grid.chunkCountX * HexMetrics.chunkSizeX * (2f * HexMetrics.innerRadius); position.x = Mathf.Clamp(position.x, 0f, xMax); return position; }
الأمر نفسه ينطبق على الموضع Z. Vector3 ClampPosition (Vector3 position) { float xMax = grid.chunkCountX * HexMetrics.chunkSizeX * (2f * HexMetrics.innerRadius); position.x = Mathf.Clamp(position.x, 0f, xMax); float zMax = grid.chunkCountZ * HexMetrics.chunkSizeZ * (1.5f * HexMetrics.outerRadius); position.z = Mathf.Clamp(position.z, 0f, zMax); return position; }
في الواقع ، هذا غير دقيق بعض الشيء. نقطة البداية في وسط الخلية ، وليس على اليسار. لذلك ، نريد أن تتوقف الكاميرا في وسط الخلايا الموجودة في أقصى اليمين. للقيام بذلك ، اطرح نصف الخلية من الحد الأقصى لـ X. float xMax = (grid.chunkCountX * HexMetrics.chunkSizeX - 0.5f) * (2f * HexMetrics.innerRadius); position.x = Mathf.Clamp(position.x, 0f, xMax);
للسبب نفسه ، نحتاج إلى تقليل الحد الأقصى Z. نظرًا لأن المقاييس مختلفة قليلاً ، نحتاج إلى طرح الخلية الكاملة. float zMax = (grid.chunkCountZ * HexMetrics.chunkSizeZ - 1) * (1.5f * HexMetrics.outerRadius); position.z = Mathf.Clamp(position.z, 0f, zMax);
مع الحركة التي قمنا بها ، تبقى تفاصيل صغيرة فقط. في بعض الأحيان ، تتفاعل واجهة المستخدم مع مفاتيح الأسهم ، وهذا يؤدي إلى حقيقة أنه عند تحريك الكاميرا ، يتحرك شريط التمرير. يحدث هذا عندما تعتبر واجهة المستخدم نفسها نشطة ، بعد النقر فوقها ويظل المؤشر فوقها.يمكنك منع واجهة المستخدم من الاستماع إلى إدخال لوحة المفاتيح. يمكن القيام بذلك عن طريق إرشاد كائن EventSystem بعدم تنفيذ إرسال أحداث التنقل .لا مزيد من الأحداث الملاحة.إستدر
تريد أن ترى ما وراء الجرف؟ سيكون من الملائم أن تكون قادرًا على تدوير الكاميرا! دعنا نضيف هذه الميزة.مستوى التكبير ليس مهمًا للدوران ، فقط السرعة كافية. أضف متغيرًا مشتركًا واضبطه rotationSpeed
على 180 درجة. تحقق من دلتا الدوران Update
عن طريق أخذ عينات من محور الدوران وتغيير الدوران إذا لزم الأمر. public float rotationSpeed; void Update () { float zoomDelta = Input.GetAxis("Mouse ScrollWheel"); if (zoomDelta != 0f) { AdjustZoom(zoomDelta); } float rotationDelta = Input.GetAxis("Rotation"); if (rotationDelta != 0f) { AdjustRotation(rotationDelta); } float xDelta = Input.GetAxis("Horizontal"); float zDelta = Input.GetAxis("Vertical"); if (xDelta != 0f || zDelta != 0f) { AdjustPosition(xDelta, zDelta); } } void AdjustRotation (float delta) { }
سرعة الدوران.في الواقع ، محور التدوير ليس بشكل افتراضي. سيتعين علينا أن نخلقها بأنفسنا. انتقل إلى معلمات الإدخال وكرر أعلى إدخال عمودي . قم بتغيير اسم التكرار إلى Rotation وقم بتغيير المفاتيح إلى QE وفاصلة (،) بنقطة (.).بدوره محور الإدخال.لقد قمت بتنزيل حزمة الحزم ، لماذا لا أملك هذا الإدخال؟. Unity. , . , , .
زاوية الدوران سنتتبعها ونتغير بها AdjustRotation
. بعد ذلك سنقوم بتدوير نظام الكاميرا بالكامل. float rotationAngle; void AdjustRotation (float delta) { rotationAngle += delta * rotationSpeed * Time.deltaTime; transform.localRotation = Quaternion.Euler(0f, rotationAngle, 0f); }
نظرًا لأن الدائرة الكاملة تبلغ 360 درجة ، فإننا نلف زاوية التدوير بحيث تكون في النطاق من 0 إلى 360. void AdjustRotation (float delta) { rotationAngle += delta * rotationSpeed * Time.deltaTime; if (rotationAngle < 0f) { rotationAngle += 360f; } else if (rotationAngle >= 360f) { rotationAngle -= 360f; } transform.localRotation = Quaternion.Euler(0f, rotationAngle, 0f); }
بدوره في العمل.الآن الدوران يعمل. إذا قمت بفحصها ، يمكنك أن ترى أن الحركة مطلقة. لذلك ، بعد أن تحولت بمقدار 180 درجة ، ستكون الحركة عكس ما كان متوقعًا. سيكون أكثر ملاءمة للمستخدم أن يتم تنفيذ الحركة بالنسبة لزاوية عرض الكاميرا. يمكننا القيام بذلك عن طريق ضرب الدوران الحالي في اتجاه الحركة. void AdjustPosition (float xDelta, float zDelta) { Vector3 direction = transform.localRotation * new Vector3(xDelta, 0f, zDelta).normalized; … }
النزوح النسبي.حزمة الوحدةالتحرير المتقدم
الآن بعد أن أصبح لدينا خريطة أكبر ، يمكنك تحسين أدوات تعديل الخريطة. تغيير خلية واحدة في كل مرة طويل جدًا ، لذا سيكون من الرائع إنشاء فرشاة أكبر. سيكون من الملائم أيضًا أن تختار الطلاء أو تغيير الارتفاع ، تاركًا كل شيء آخر دون تغيير.اللون والارتفاع الاختياري
يمكننا جعل الألوان اختيارية عن طريق إضافة خيار تحديد فارغ إلى مجموعة التبديل. كرر أحد محولات الألوان واستبدل الملصق الخاص به بـ --- أو شيء مشابه للإشارة إلى أنه ليس لونًا. ثم قم بتغيير وسيطة حدث On Value Changed إلى −1.مؤشر لون غير صالح.بالطبع ، هذا الفهرس غير صالح لمجموعة من الألوان. يمكننا استخدامه لتحديد ما إذا كان يجب تطبيق اللون على الخلايا. bool applyColor; public void SelectColor (int index) { applyColor = index >= 0; if (applyColor) { activeColor = colors[index]; } } void EditCell (HexCell cell) { if (applyColor) { cell.Color = activeColor; } cell.Elevation = activeElevation; }
يتم التحكم في الارتفاع بواسطة شريط تمرير ، لذلك لا يمكننا إضافة مفتاح تبديل إليه. بدلاً من ذلك ، يمكننا استخدام مفتاح منفصل لتمكين تعديل الارتفاع أو تعطيله. بشكل افتراضي ، سيتم تمكينه. bool applyElevation = true; void EditCell (HexCell cell) { if (applyColor) { cell.Color = activeColor; } if (applyElevation) { cell.Elevation = activeElevation; } }
أضف مفتاح ارتفاع جديد إلى واجهة المستخدم. سأضع أيضًا كل شيء على لوحة جديدة ، وأجعل شريط تمرير الارتفاع أفقيًا حتى تكون واجهة المستخدم أكثر جمالًا.اللون والارتفاع الاختياري.لتمكين الارتفاع ، نحتاج إلى طريقة جديدة سنتصل بها بواجهة المستخدم. public void SetApplyElevation (bool toggle) { applyElevation = toggle; }
من خلال توصيله بمفتاح الارتفاع ، تأكد من استخدام طريقة bool الديناميكية في الجزء العلوي من قائمة الطرق. لا تعرض الإصدارات الصحيحة علامة اختيار في المفتش.نرسل حالة مفتاح الارتفاع.الآن يمكننا اختيار التلوين فقط بالورود أو الارتفاع فقط. أو كلاهما ، كالعادة. يمكننا حتى أن نختار عدم تغيير أحدهما أو الآخر ، ولكن حتى الآن ليس مفيدًا لنا بشكل خاص.التبديل بين اللون والارتفاع.لماذا ينطفئ الارتفاع عند اختيار اللون؟, toggle group. , , toggle group.
حجم الفرشاة
لدعم حجم الفرشاة القابل لتغيير الحجم ، أضف متغيرًا صحيحًا brushSize
وطريقة لتعيينه من خلال واجهة المستخدم. سنستخدم شريط التمرير ، لذلك مرة أخرى سيتعين علينا تحويل القيمة من تعويم إلى int. int brushSize; public void SetBrushSize (float size) { brushSize = (int)size; }
منزلق حجم الفرشاة.يمكنك إنشاء شريط تمرير جديد من خلال تكرار شريط تمرير الارتفاع. قم بتغيير قيمته القصوى إلى 4 وأرفقها بالطريقة المقابلة. أضفت أيضًا علامة له.إعدادات شريط تمرير حجم الفرشاة.الآن بعد أن أصبح بإمكاننا تحرير عدة خلايا في نفس الوقت ، نحتاج إلى استخدام الطريقة EditCells
. ستدعو هذه الطريقة EditCell
لجميع الخلايا المعنية. سيتم اعتبار الخلية المحددة مبدئيًا مركز الفرشاة. void HandleInput () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { EditCells(hexGrid.GetCell(hit.point)); } } void EditCells (HexCell center) { } void EditCell (HexCell cell) { … }
يحدد حجم الفرشاة نصف قطر التحرير. مع نصف قطر 0 ، ستكون هذه خلية مركزية واحدة فقط. مع نصف قطر 1 ، سيكون هذا المركز وجيرانه. عند نصف قطر 2 ، يتم تشغيل جيران المركز وجيرانهم المباشرين. وهكذا دواليك.
حتى الشعاع 3.لتحرير الخلايا ، تحتاج إلى الالتفاف عليها في حلقة. نحتاج أولاً إلى إحداثيات X و Z للمركز. void EditCells (HexCell center) { int centerX = center.coordinates.X; int centerZ = center.coordinates.Z; }
نجد الحد الأدنى للإحداثيات Z بطرح نصف القطر. لذلك نحدد خط الصفر. بدءًا من هذا الخط ، ننتقل خلاله حتى نغطي الخط في المنتصف. void EditCells (HexCell center) { int centerX = center.coordinates.X; int centerZ = center.coordinates.Z; for (int r = 0, z = centerZ - brushSize; z <= centerZ; z++, r++) { } }
تحتوي الخلية الأولى في الصف السفلي على نفس إحداثيات X مثل الخلية المركزية. هذا التنسيق ينخفض بزيادة رقم السطر.تحتوي الخلية الأخيرة دائمًا على إحداثي س يساوي إحداثيات المركز بالإضافة إلى نصف القطر.الآن يمكننا التكرار حول كل صف والحصول على الخلايا حسب إحداثياتها. for (int r = 0, z = centerZ - brushSize; z <= centerZ; z++, r++) { for (int x = centerX - r; x <= centerX + brushSize; x++) { EditCell(hexGrid.GetCell(new HexCoordinates(x, z))); } }
ليس لدينا حتى الآن طريقة HexGrid.GetCell
بمعلمة إحداثيات ، لذا قم بإنشائها. تحويل إلى إحداثيات النزوح والحصول على الخلية. public HexCell GetCell (HexCoordinates coordinates) { int z = coordinates.Z; int x = coordinates.X + z / 2; return cells[x + z * cellCountX]; }
الجزء السفلي من الفرشاة ، الحجم 2.نحن نغطي بقية الفرشاة ، ونقوم بدورة من الأعلى إلى الأسفل إلى المركز. في هذه الحالة ، يتم عكس المنطق ويجب استبعاد الصف المركزي. void EditCells (HexCell center) { int centerX = center.coordinates.X; int centerZ = center.coordinates.Z; for (int r = 0, z = centerZ - brushSize; z <= centerZ; z++, r++) { for (int x = centerX - r; x <= centerX + brushSize; x++) { EditCell(hexGrid.GetCell(new HexCoordinates(x, z))); } } for (int r = 0, z = centerZ + brushSize; z > centerZ; z--, r++) { for (int x = centerX - brushSize; x <= centerX + r; x++) { EditCell(hexGrid.GetCell(new HexCoordinates(x, z))); } } }
الفرشاة كاملة ، الحجم 2.هذا يعمل ، ما لم تتجاوز الفرشاة حدود الشبكة. عندما يحدث هذا ، نحصل على استثناء خارج النطاق. لتجنب ذلك، تحقق من الحدود HexGrid.GetCell
واسترداد null
يطلب خلية غير موجود. public HexCell GetCell (HexCoordinates coordinates) { int z = coordinates.Z; if (z < 0 || z >= cellCountZ) { return null; } int x = coordinates.X + z / 2; if (x < 0 || x >= cellCountX) { return null; } return cells[x + z * cellCountX]; }
لتجنب استثناء مرجع فارغ ، HexMapEditor
يجب التحقق قبل التحرير ما إذا كانت الخلية موجودة بالفعل. void EditCell (HexCell cell) { if (cell) { if (applyColor) { cell.Color = activeColor; } if (applyElevation) { cell.Elevation = activeElevation; } } }
باستخدام أحجام فرشاة متعددة.تبديل رؤية تسمية الخلية
في أغلب الأحيان ، لا نحتاج إلى رؤية ملصقات الخلايا. لذلك دعونا نجعلها اختيارية. نظرًا لأن كل جزء يتحكم في لوحة الرسم الخاصة به ، فأضف طريقة ShowUI
إلى HexGridChunk
. عندما تكون واجهة المستخدم مرئية ، نقوم بتنشيط اللوحة. خلاف ذلك ، قم بإلغاء تنشيطه. public void ShowUI (bool visible) { gridCanvas.gameObject.SetActive(visible); }
دعنا نخفي واجهة المستخدم بشكل افتراضي. void Awake () { gridCanvas = GetComponentInChildren<Canvas>(); hexMesh = GetComponentInChildren<HexMesh>(); cells = new HexCell[HexMetrics.chunkSizeX * HexMetrics.chunkSizeZ]; ShowUI(false); }
نظرًا لأن رؤية واجهة المستخدم يتم تبديلها للخريطة بأكملها ، فإننا نضيف الطريقة ShowUI
إلى HexGrid
. يمرر الطلب فقط إلى شظاياه. public void ShowUI (bool visible) { for (int i = 0; i < chunks.Length; i++) { chunks[i].ShowUI(visible); } }
HexMapEditor
يحصل على نفس الطريقة ، ويمرر الطلب إلى الشبكة. public void ShowUI (bool visible) { hexGrid.ShowUI(visible); }
أخيرًا ، يمكننا إضافة مفتاح إلى واجهة المستخدم وتوصيله.تبديل رؤية العلامة.حزمة الوحدةالجزء السادس: الأنهار
- إضافة الأنهار إلى الخلايا.
- سحب وإسقاط الدعم لرسم الأنهار.
- إنشاء مجاري الأنهار.
- استخدام شبكات متعددة لكل جزء.
- إنشاء تجمع قائمة مشترك.
- تثليث وتحريك المياه المتدفقة.
تحدثنا في الجزء السابق عن دعم الخرائط الكبيرة. الآن يمكننا الانتقال إلى عناصر الإغاثة الأكبر. هذه المرة سنتحدث عن الأنهار.تتدفق الأنهار من الجبال.خلايا النهر
هناك ثلاث طرق لإضافة الأنهار إلى شبكة من السداسيات. الطريقة الأولى هي السماح لهم بالتدفق من خلية إلى أخرى. هذه هي الطريقة التي يتم تنفيذها في Endless Legend. الطريقة الثانية هي السماح لهم بالتدفق بين الخلايا ، من الحافة إلى الحافة. لذلك يتم تنفيذه في الحضارة 5. الطريقة الثالثة ليست إنشاء هياكل نهارية خاصة على الإطلاق ، ولكن استخدام خلايا المياه لاقتراحها. لذلك يتم تنفيذ الأنهار في Age of Wonders 3.في حالتنا ، حواف الخلايا مشغولة بالفعل بالمنحدرات والمنحدرات. هذا يترك مساحة صغيرة للأنهار. لذلك ، سنجعلها تتدفق من خلية إلى أخرى. هذا يعني أنه في كل خلية لن يكون هناك نهر ، أو سيتدفق نهر على طوله ، أو سيكون هناك بداية أو نهاية للنهر فيها. في تلك الخلايا التي يتدفق على طولها النهر ، يمكن أن يتدفق مباشرة ، ثم ينعطف خطوة واحدة أو خطوتين.خمسة تشكيلات نهرية ممكنة.لن ندعم التفرع أو دمج الأنهار. سيؤدي هذا إلى تعقيد الأمور أكثر ، وخاصة تدفق المياه. كما أننا لن نربك كميات كبيرة من المياه. سننظر فيها في برنامج تعليمي آخر.تتبع النهر
يمكن اعتبار الخلية التي يتدفق معها النهر في نفس الوقت على أنها نهر وارد وصادر. إذا كان يحتوي على بداية نهر ، فلن يكون له سوى نهر صادر. وإذا كان يحتوي على نهاية النهر ، فلن يكون له سوى نهر وارد. يمكننا تخزين هذه المعلومات HexCell
باستخدام قيمتين منطقية. bool hasIncomingRiver, hasOutgoingRiver;
لكن هذا لا يكفي. نحتاج أيضًا إلى معرفة اتجاه هذه الأنهار. في حالة نهر صادر ، فإنه يشير إلى أين يتحرك. في حالة وجود نهر وارد ، فإنه يشير إلى مصدره. bool hasIncomingRiver, hasOutgoingRiver; HexDirection incomingRiver, outgoingRiver;
سنحتاج إلى هذه المعلومات عند تثليث الخلايا ، لذلك سنضيف خصائص للوصول إليها. لن ندعم تعيينهم مباشرةً. للقيام بذلك ، سنضيف أيضًا طريقة منفصلة. public bool HasIncomingRiver { get { return hasIncomingRiver; } } public bool HasOutgoingRiver { get { return hasOutgoingRiver; } } public HexDirection IncomingRiver { get { return incomingRiver; } } public HexDirection OutgoingRiver { get { return outgoingRiver; } }
السؤال المهم هو ما إذا كان هناك نهر في الخلية ، بغض النظر عن التفاصيل. لذلك ، دعنا نضيف خاصية لهذا أيضًا. public bool HasRiver { get { return hasIncomingRiver || hasOutgoingRiver; } }
سؤال منطقي آخر: بداية أو نهاية النهر في الخلية. إذا كانت حالة النهر الوارد والصادر مختلفة ، فهذا هو الحال. لذلك ، سنجعل هذا خاصية أخرى. public bool HasRiverBeginOrEnd { get { return hasIncomingRiver != hasOutgoingRiver; } }
وأخيرًا ، سيكون من المفيد معرفة ما إذا كان النهر يتدفق عبر سلسلة معينة ، سواء كانت واردة أو صادرة. public bool HasRiverThroughEdge (HexDirection direction) { return hasIncomingRiver && incomingRiver == direction || hasOutgoingRiver && outgoingRiver == direction; }
إزالة النهر
قبل أن نبدأ بإضافة نهر إلى خلية ، دعنا ننفذ أولاً دعم إزالة النهر. بادئ ذي بدء ، سنكتب طريقة لإزالة الجزء الصادر فقط من النهر.إذا لم يكن هناك نهر صادر في الزنزانة ، فلا داعي للقيام بأي شيء. وإلا ، قم بإيقاف تشغيله وقم بإجراء التحديث. public void RemoveOutgoingRiver () { if (!hasOutgoingRiver) { return; } hasOutgoingRiver = false; Refresh(); }
لكن هذا ليس كل شيء يجب أن يتحرك النهر الصادر في مكان ما. لذلك ، يجب أن يكون هناك جار مع النهر القادم. نحن بحاجة للتخلص منها أيضًا. public void RemoveOutgoingRiver () { if (!hasOutgoingRiver) { return; } hasOutgoingRiver = false; Refresh(); HexCell neighbor = GetNeighbor(outgoingRiver); neighbor.hasIncomingRiver = false; neighbor.Refresh(); }
لا يمكن أن يتدفق نهر من الخريطة؟, . , .
إزالة نهر من خلية يغير مظهر تلك الخلية فقط. بخلاف تحرير الارتفاع أو اللون ، فإنه لا يؤثر على الجيران. لذلك ، نحتاج إلى تحديث الخلية نفسها فقط ، وليس تحديث جيرانها. public void RemoveOutgoingRiver () { if (!hasOutgoingRiver) { return; } hasOutgoingRiver = false; RefreshSelfOnly(); HexCell neighbor = GetNeighbor(outgoingRiver); neighbor.hasIncomingRiver = false; neighbor.RefreshSelfOnly(); }
تقوم هذه الطريقة RefreshSelfOnly
ببساطة بتحديث الجزء الذي تنتمي إليه الخلية. نظرًا لأننا لا نغير النهر أثناء تهيئة الشبكة ، فلا داعي للقلق إذا تم تعيين جزء بالفعل. void RefreshSelfOnly () { chunk.Refresh(); }
تعمل إزالة الأنهار الواردة بنفس الطريقة. public void RemoveIncomingRiver () { if (!hasIncomingRiver) { return; } hasIncomingRiver = false; RefreshSelfOnly(); HexCell neighbor = GetNeighbor(incomingRiver); neighbor.hasOutgoingRiver = false; neighbor.RefreshSelfOnly(); }
وإزالة النهر بالكامل يعني ببساطة إزالة الأجزاء الواردة والصادرة من النهر. public void RemoveRiver () { RemoveOutgoingRiver(); RemoveIncomingRiver(); }
مضيفا الأنهار
لدعم إنشاء الأنهار ، نحتاج إلى طريقة لتحديد النهر الخارج للخلية. يجب عليه إعادة تعريف جميع الأنهار السابقة السابقة وتعيين النهر الوارد المقابل.بادئ ذي بدء ، لسنا بحاجة إلى القيام بأي شيء إذا كان النهر موجودًا بالفعل. public void SetOutgoingRiver (HexDirection direction) { if (hasOutgoingRiver && outgoingRiver == direction) { return; } }
بعد ذلك ، نحتاج إلى التأكد من وجود جار في الاتجاه الصحيح. بالإضافة إلى ذلك ، لا يمكن للأنهار أن تتدفق. لذلك ، يجب أن نكمل العملية إذا كان الجار أعلى. HexCell neighbor = GetNeighbor(direction); if (!neighbor || elevation < neighbor.elevation) { return; }
بعد ذلك نحتاج إلى مسح النهر السابق السابق. ونحتاج أيضًا إلى إزالة النهر القادم ، إذا كان متراكبًا على نهر صادر جديد. RemoveOutgoingRiver(); if (hasIncomingRiver && incomingRiver == direction) { RemoveIncomingRiver(); }
الآن يمكننا أن ننتقل إلى إنشاء النهر الصادر. hasOutgoingRiver = true; outgoingRiver = direction; RefreshSelfOnly();
ولا تنس تعيين النهر الوارد لخلية أخرى بعد إزالة النهر الوارد الحالي ، إذا كان موجودًا. neighbor.RemoveIncomingRiver(); neighbor.hasIncomingRiver = true; neighbor.incomingRiver = direction.Opposite(); neighbor.RefreshSelfOnly();
التخلص من الأنهار المتدفقة
الآن بعد أن جعلنا من الممكن إضافة الأنهار الصحيحة فقط ، لا يزال بإمكان الإجراءات الأخرى إنشاء الأنهار الخاطئة. عندما نغير ارتفاع الخلية ، يجب علينا مرة أخرى أن نتأكد بقوة من أنه لا يمكن أن تتدفق الأنهار فقط. يجب إزالة جميع الأنهار غير المنتظمة. public int Elevation { get { return elevation; } set { … if ( hasOutgoingRiver && elevation < GetNeighbor(outgoingRiver).elevation ) { RemoveOutgoingRiver(); } if ( hasIncomingRiver && elevation > GetNeighbor(incomingRiver).elevation ) { RemoveIncomingRiver(); } Refresh(); } }
حزمة الوحدةتغيير الأنهار
لدعم تحرير النهر ، نحتاج إلى إضافة تبديل نهر إلى واجهة المستخدم. في الحقيقة. نحن بحاجة إلى دعم لثلاثة أوضاع تحرير. نحتاج إلى تجاهل الأنهار ، أو إضافتها ، أو حذفها. يمكننا استخدام تعداد بسيط للمفاتيح لتتبع الحالة. نظرًا لأننا سنستخدمه فقط داخل المحرر ، يمكننا تحديده داخل الفصل HexMapEditor
، جنبًا إلى جنب مع حقل وضع النهر. enum OptionalToggle { Ignore, Yes, No } OptionalToggle riverMode;
ونحن بحاجة إلى طريقة لتغيير نظام النهر من خلال واجهة المستخدم. public void SetRiverMode (int mode) { riverMode = (OptionalToggle)mode; }
للتحكم في نظام النهر ، أضف ثلاثة مفاتيح إلى واجهة المستخدم وربطها بمجموعة التبديل الجديدة ، كما فعلنا مع الألوان. لقد هيأت مفاتيح التبديل بحيث تكون تسمياتها تحت مربعات الاختيار. ونتيجة لذلك ، ستظل رقيقة بما يكفي لتناسب جميع الخيارات الثلاثة في سطر واحد.الأنهار واجهة المستخدملماذا لا تستخدم قائمة منسدلة؟, . dropdown list Unity Play. , .
سحب وإسقاط الاعتراف
لإنشاء نهر ، نحتاج إلى خلية واتجاه. في الوقت الحالي ، HexMapEditor
لا تزودنا بهذه المعلومات. لذلك ، نحتاج إلى إضافة دعم السحب والإفلات من خلية إلى أخرى.نحتاج إلى معرفة ما إذا كان هذا السحب سيكون صحيحًا ، وكذلك تحديد اتجاهه. وللتعرف على السحب والإفلات ، نحتاج إلى تذكر الخلية السابقة. bool isDrag; HexDirection dragDirection; HexCell previousCell;
في البداية ، عندما لا يتم إجراء السحب ، لا يتم تنفيذ الخلية السابقة. أي عندما لا يكون هناك إدخال أو لا نتفاعل مع البطاقة ، فأنت بحاجة إلى تعيين قيمة لها null
. void Update () { if ( Input.GetMouseButton(0) && !EventSystem.current.IsPointerOverGameObject() ) { HandleInput(); } else { previousCell = null; } } void HandleInput () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { EditCells(hexGrid.GetCell(hit.point)); } else { previousCell = null; } }
الخلية الحالية هي الخلية التي وجدناها بعبور الشعاع بالشبكة. بعد تحرير الخلايا ، يتم تحديثها وتصبح الخلية السابقة لتحديث جديد. void HandleInput () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { HexCell currentCell = hexGrid.GetCell(hit.point); EditCells(currentCell); previousCell = currentCell; } else { previousCell = null; } }
بعد تحديد الخلية الحالية ، يمكننا مقارنتها بالخلية السابقة ، إن وجدت. إذا حصلنا على خليتين مختلفتين ، فقد يكون لدينا السحب والإفلات الصحيح ونحتاج إلى التحقق من ذلك. خلاف ذلك ، هذا بالتأكيد ليس السحب والإفلات. if (Physics.Raycast(inputRay, out hit)) { HexCell currentCell = hexGrid.GetCell(hit.point); if (previousCell && previousCell != currentCell) { ValidateDrag(currentCell); } else { isDrag = false; } EditCells(currentCell); previousCell = currentCell; isDrag = true; }
كيف نتحقق من السحب والإسقاط؟ التحقق مما إذا كانت الخلية الحالية هي جوار الخلية السابقة. نتحقق من ذلك من خلال التحايل على جيرانها في دورة. إذا وجدنا تطابقًا ، نتعرف أيضًا على الفور على اتجاه السحب. void ValidateDrag (HexCell currentCell) { for ( dragDirection = HexDirection.NE; dragDirection <= HexDirection.NW; dragDirection++ ) { if (previousCell.GetNeighbor(dragDirection) == currentCell) { isDrag = true; return; } } isDrag = false; }
هل سنقوم بعمل جر متشنج؟, . «» , .
, . .
تغيير الخلايا
الآن يمكننا التعرف على السحب والإفلات ، يمكننا تحديد الأنهار الصادرة. يمكننا أيضًا إزالة الأنهار ؛ لذلك ، لا يلزم دعم السحب والإفلات. void EditCell (HexCell cell) { if (cell) { if (applyColor) { cell.Color = activeColor; } if (applyElevation) { cell.Elevation = activeElevation; } if (riverMode == OptionalToggle.No) { cell.RemoveRiver(); } else if (isDrag && riverMode == OptionalToggle.Yes) { previousCell.SetOutgoingRiver(dragDirection); } } }
سيؤدي هذا الرمز إلى رسم النهر من الخلية السابقة إلى التيار. لكنه يتجاهل حجم الفرشاة. هذا أمر منطقي تمامًا ، ولكن دعنا نرسم الأنهار لجميع الخلايا المغلقة بواسطة الفرشاة. يمكن القيام بذلك عن طريق إجراء العمليات على الخلية المحررة. في حالتنا ، نحتاج إلى التأكد من وجود خلية أخرى بالفعل. else if (isDrag && riverMode == OptionalToggle.Yes) { HexCell otherCell = cell.GetNeighbor(dragDirection.Opposite()); if (otherCell) { otherCell.SetOutgoingRiver(dragDirection); } }
الآن يمكننا تحرير الأنهار ، لكن لا نراها بعد. يمكننا التحقق من أن هذا يعمل من خلال فحص الخلايا المعدلة في مفتش التصحيح.خلية بها نهر في مفتش التصحيح.ما هو مفتش التصحيح؟. . , .
حزمة الوحدةمجاري النهر بين الخلايا
عند تثليث نهر ، نحتاج إلى النظر في جزأين: موقع قاع النهر والمياه التي تتدفق من خلاله. أولاً ، سننشئ قناة ونترك المياه لوقت لاحق.أبسط جزء من النهر هو حيث يتدفق في التقاطعات بين الخلايا. بينما نقوم بتثليث هذه المنطقة بشريط من ثلاثة أرباع. يمكننا إضافة قاع نهر إليها عن طريق خفض الرباعي الأوسط وإضافة جدارين للقناة.إضافة نهر إلى شريط ضلع.لهذا ، في حالة النهر ، ستكون هناك حاجة إلى كوادتين إضافيتين وسيتم إنشاء قناة بجدارين رأسيين. نهج بديل لاستخدام أربعة أرباع. ثم نخفض القمة الوسطى لإنشاء سرير بجدران مائلة.دائما أربعة أرباع.الاستخدام المستمر لنفس العدد من الزوايا الرباعية مناسب ، لذلك دعنا نختار هذا الخيار.مضيفا قمم الحافة
يتطلب الانتقال من ثلاثة إلى أربعة لكل حافة إنشاء قمة إضافية للحافة. إعادة كتابة EdgeVertices
، وإعادة تسمية لأول مرة v4
في v5
، ثم أعيدت تسميته v3
ل v4
. تضمن الإجراءات في هذا الترتيب أن جميع الشفرات تستمر في الرجوع إلى القمم الصحيحة. استخدم خيار إعادة التسمية أو إعادة البناء في المحرر الخاص بك لإجراء التغييرات في كل مكان. خلاف ذلك ، سيكون عليك فحص الكود بأكمله يدويًا وإجراء التغييرات. public Vector3 v1, v2, v4, v5;
بعد إعادة تسمية كل شيء ، أضف واحدة جديدة v3
. public Vector3 v1, v2, v3, v4, v5;
أضف قمة جديدة للمنشئ. وهي تقع في الوسط بين قمم الزاوية. بالإضافة إلى ذلك ، يجب أن تكون القمم الأخرى في ½ و ¾ ، وليس في & frac13 ؛ و & frac23 ؛. public EdgeVertices (Vector3 corner1, Vector3 corner2) { v1 = corner1; v2 = Vector3.Lerp(corner1, corner2, 0.25f); v3 = Vector3.Lerp(corner1, corner2, 0.5f); v4 = Vector3.Lerp(corner1, corner2, 0.75f); v5 = corner2; }
إضافة v3
في TerraceLerp
. public static EdgeVertices TerraceLerp ( EdgeVertices a, EdgeVertices b, int step) { EdgeVertices result; result.v1 = HexMetrics.TerraceLerp(a.v1, b.v1, step); result.v2 = HexMetrics.TerraceLerp(a.v2, b.v2, step); result.v3 = HexMetrics.TerraceLerp(a.v3, b.v3, step); result.v4 = HexMetrics.TerraceLerp(a.v4, b.v4, step); result.v5 = HexMetrics.TerraceLerp(a.v5, b.v5, step); return result; }
الآن لا HexMesh
بد لي من تضمين قمة إضافية في مراوح مثلثات الضلع. void TriangulateEdgeFan (Vector3 center, EdgeVertices edge, Color color) { AddTriangle(center, edge.v1, edge.v2); AddTriangleColor(color); AddTriangle(center, edge.v2, edge.v3); AddTriangleColor(color); AddTriangle(center, edge.v3, edge.v4); AddTriangleColor(color); AddTriangle(center, edge.v4, edge.v5); AddTriangleColor(color); }
وأيضا في خطوط الزوايا. void TriangulateEdgeStrip ( EdgeVertices e1, Color c1, EdgeVertices e2, Color c2 ) { AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); AddQuadColor(c1, c2); AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); AddQuadColor(c1, c2); AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); AddQuadColor(c1, c2); AddQuad(e1.v4, e1.v5, e2.v4, e2.v5); AddQuadColor(c1, c2); }
مقارنة بين أربعة وخمس رؤوس في كل حافة.ارتفاع قاع النهر
أنشأنا القناة بخفض الجزء العلوي السفلي من الضلع. يحدد الوضع الرأسي لقاع النهر. على الرغم من أن الوضع الرأسي الدقيق لكل خلية مشوه ، يجب أن نحافظ على نفس ارتفاع قاع النهر في الخلايا بنفس الارتفاع. بفضل هذه المياه ، لا يجب أن تتدفق إلى أعلى النهر. بالإضافة إلى ذلك ، يجب أن يكون السرير منخفضًا بما يكفي للبقاء بالأسفل ، حتى في حالة الخلايا الأكثر انحرافًا عموديًا ، بينما يترك في نفس الوقت مساحة كافية للماء.لنقم بتعيين هذا الإزاحة إلى HexMetrics
والتعبير عنه على أنه ارتفاع. تعويضات مستوى واحد ستكون كافية. public const float streamBedElevationOffset = -1f;
يمكننا استخدام هذا المقياس لإضافة خصائص HexCell
للحصول على الوضع الرأسي لقاع نهر الخلية. public float StreamBedY { get { return (elevation + HexMetrics.streamBedElevationOffset) * HexMetrics.elevationStep; } }
إنشاء قناة
عندما HexMesh
يتم تثليث أحد الأجزاء الثلاثة المثلثة للخلية ، يمكننا تحديد ما إذا كان النهر يتدفق على طول حافته. إذا كان الأمر كذلك ، فيمكننا خفض القمة الوسطى للضلع إلى ارتفاع قاع النهر. void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.Position; EdgeVertices e = new EdgeVertices( center + HexMetrics.GetFirstSolidCorner(direction), center + HexMetrics.GetSecondSolidCorner(direction) ); if (cell.HasRiverThroughEdge(direction)) { e.v3.y = cell.StreamBedY; } TriangulateEdgeFan(center, e, cell.Color); if (direction <= HexDirection.SE) { TriangulateConnection(direction, cell, e); } }
قم بتغيير قمة منتصف الضلع.يمكننا أن نرى كيف تظهر العلامات الأولى للنهر ، ولكن تظهر ثقوب في الراحة. لإغلاقها ، نحتاج إلى تغيير حافة أخرى ثم تثليث الاتصال. void TriangulateConnection ( HexDirection direction, HexCell cell, EdgeVertices e1 ) { HexCell neighbor = cell.GetNeighbor(direction); if (neighbor == null) { return; } Vector3 bridge = HexMetrics.GetBridge(direction); bridge.y = neighbor.Position.y - cell.Position.y; EdgeVertices e2 = new EdgeVertices( e1.v1 + bridge, e1.v5 + bridge ); if (cell.HasRiverThroughEdge(direction)) { e2.v3.y = neighbor.StreamBedY; } … }
القنوات المكتملة للمفاصل الضلعية.حزمة الوحدةمجاري الأنهار تمر عبر زنزانة
الآن لدينا مجاري الأنهار الصحيحة بين الخلايا. ولكن عندما يتدفق النهر عبر الخلية ، تنتهي القنوات دائمًا في مركزها. لحل هذه المشكلة يجب أن تعمل. لنبدأ بالحالة عندما يتدفق نهر عبر خلية مباشرة ، من حافة إلى العكس.إذا لم يكن هناك نهر ، فيمكن أن يكون كل جزء من الخلية مروحة بسيطة للمثلثات. ولكن عندما يتدفق النهر مباشرة ، من الضروري إدخال قناة. في الواقع ، نحتاج إلى تمديد الرأس المركزي إلى خط ، وبالتالي تحويل المثلثين الأوسطين إلى أربعة زوايا. ثم تتحول مروحة المثلثات إلى شبه منحرف.نقوم بإدخال القناة في المثلث.ستكون هذه القنوات أطول بكثير من تلك التي تمر عبر اتصال الخلايا. يصبح هذا واضحًا عندما يتم تشويه مواضع القمة. لذلك ، دعنا نقسم شبه المنحرف إلى قسمين عن طريق إدراج مجموعة أخرى من حواف القمة في الوسط بين المركز والحافة.تثليث القناة.بما أن التثليث مع النهر سيكون مختلفًا تمامًا عن التثليث بدون نهر ، فلنقم بإنشاء طريقة منفصلة لذلك. إذا كان لدينا نهر ، فإننا نستخدم هذه الطريقة ، وإلا سنترك مروحة من المثلثات. void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.Position; EdgeVertices e = new EdgeVertices( center + HexMetrics.GetFirstSolidCorner(direction), center + HexMetrics.GetSecondSolidCorner(direction) ); if (cell.HasRiver) { if (cell.HasRiverThroughEdge(direction)) { e.v3.y = cell.StreamBedY; TriangulateWithRiver(direction, cell, center, e); } } else { TriangulateEdgeFan(center, e, cell.Color); } if (direction <= HexDirection.SE) { TriangulateConnection(direction, cell, e); } } void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { }
ثقوب يجب أن يكون فيها أنهار.لرؤية ما يحدث بشكل أفضل ، قم بتعطيل تشويه الخلية مؤقتًا. public const float cellPerturbStrength = 0f;
قمم غير مشوهة.التثليث مباشرة من خلال الخلية
لإنشاء قناة مباشرة من خلال جزء من الخلية ، نحتاج إلى تمديد المركز إلى خط. يجب أن يكون لهذا الخط نفس عرض القناة. يمكننا إيجاد الرأس الأيسر بنقل ¼ المسافة من المركز إلى الزاوية الأولى من الجزء السابق. void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { Vector3 centerL = center + HexMetrics.GetFirstSolidCorner(direction.Previous()) * 0.25f; }
وبالمثل بالنسبة للرأس الأيمن. في هذه الحالة ، نحتاج إلى الزاوية الثانية من الجزء التالي. Vector3 centerL = center + HexMetrics.GetFirstSolidCorner(direction.Previous()) * 0.25f; Vector3 centerR = center + HexMetrics.GetSecondSolidCorner(direction.Next()) * 0.25f;
يمكن العثور على الخط الأوسط من خلال إنشاء حواف قمة بين الوسط والحافة. EdgeVertices m = new EdgeVertices( Vector3.Lerp(centerL, e.v1, 0.5f), Vector3.Lerp(centerR, e.v5, 0.5f) );
بعد ذلك ، قم بتغيير الرأس الأوسط للضلع الأوسط ، وكذلك المركز ، لأنها ستصبح النقاط السفلية للقناة. m.v3.y = center.y = e.v3.y;
الآن يمكننا استخدام TriangulateEdgeStrip
لملء الفراغ بين الخط الأوسط وخط الحافة. TriangulateEdgeStrip(m, cell.Color, e, cell.Color);
القنوات المضغوطة.لسوء الحظ ، تبدو القنوات مضغوطة. يحدث هذا لأن القمم الوسطى من الضلع قريبة جدًا من بعضها البعض. لماذا حدث هذا؟إذا افترضنا أن طول الحافة الخارجية هو 1 ، فسيكون طول الخط المركزي ½. بما أن الحافة الوسطى في المنتصف بينهما ، يجب أن يكون طولها يساوي ¾.عرض القناة ½ ويجب أن يظل ثابتًا. بما أن طول الحافة الوسطى هو ¾ ، يبقى فقط only ، وفقًا لـ & frac18 ؛ على جانبي القناة.الأطوال النسبية.بما أن طول الحافة الوسطى هو ¾ ، فإن & frac18 ؛ يصبح نسبة إلى طول الضلع الأوسط يساوي & frac16 ؛. هذا يعني أنه يجب أن تكون ذروته الثانية والرابعة محققة بالسداس وليس الأرباع.يمكننا تقديم الدعم لمثل هذا الاستيفاء البديل عن طريق الإضافة إلى EdgeVertices
مُنشئ آخر. بدلا من الاستيفاء الثابتة v2
و v4
دعونا نستخدم هذا الخيار. public EdgeVertices (Vector3 corner1, Vector3 corner2, float outerStep) { v1 = corner1; v2 = Vector3.Lerp(corner1, corner2, outerStep); v3 = Vector3.Lerp(corner1, corner2, 0.5f); v4 = Vector3.Lerp(corner1, corner2, 1f - outerStep); v5 = corner2; }
الآن يمكننا استخدامه مع & frac16 ؛ ج HexMesh.TriangulateWithRiver
. EdgeVertices m = new EdgeVertices( Vector3.Lerp(centerL, e.v1, 0.5f), Vector3.Lerp(centerR, e.v5, 0.5f), 1f / 6f );
القنوات المباشرة.بعد أن جعلنا القناة مستقيمة ، يمكننا الذهاب إلى الجزء الثاني من شبه المنحرف. في هذه الحالة ، لا يمكننا استخدام شريط الضلع ، لذلك يجب علينا القيام بذلك يدويًا. لنقم أولاً بإنشاء مثلثات على الجانبين. AddTriangle(centerL, m.v1, m.v2); AddTriangleColor(cell.Color); AddTriangle(centerR, m.v4, m.v5); AddTriangleColor(cell.Color);
مثلثات جانبية.يبدو الأمر جيدًا ، لذا دعنا نملأ المساحة المتبقية بأربعة زوايا ، لإنشاء الجزء الأخير من القناة. AddTriangle(centerL, m.v1, m.v2); AddTriangleColor(cell.Color); AddQuad(centerL, center, m.v2, m.v3); AddQuadColor(cell.Color); AddQuad(center, centerR, m.v3, m.v4); AddQuadColor(cell.Color); AddTriangle(centerR, m.v4, m.v5); AddTriangleColor(cell.Color);
في الواقع ، ليس لدينا بديل AddQuadColor
يتطلب معلمة واحدة فقط. بينما لم نكن بحاجة إليها. لذا دعونا ننشئها. void AddQuadColor (Color color) { colors.Add(color); colors.Add(color); colors.Add(color); colors.Add(color); }
القنوات المستقيمة المكتملة.بداية ونهاية التثليث
يختلف التثليث للجزء الذي له بداية أو نهاية نهر فقط ، وبالتالي يتطلب أسلوبه الخاص. لذلك ، سوف نتحقق من ذلك Triangulate
ونستدعي الطريقة المناسبة. if (cell.HasRiver) { if (cell.HasRiverThroughEdge(direction)) { e.v3.y = cell.StreamBedY; if (cell.HasRiverBeginOrEnd) { TriangulateWithRiverBeginOrEnd(direction, cell, center, e); } else { TriangulateWithRiver(direction, cell, center, e); } } }
في هذه الحالة ، نريد استكمال القناة في المركز ، لكننا ما زلنا نستخدم مرحلتين لهذا. لذلك ، سنقوم مرة أخرى بإنشاء الحافة الوسطى بين المركز أو الحافة. نظرًا لأننا نريد إكمال القناة ، يسعدنا تمامًا أنه سيتم ضغطها. void TriangulateWithRiverBeginOrEnd ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { EdgeVertices m = new EdgeVertices( Vector3.Lerp(center, e.v1, 0.5f), Vector3.Lerp(center, e.v5, 0.5f) ); }
حتى لا تصبح القناة ضحلة بسرعة كبيرة ، سنقوم بتعيين ارتفاع قاع النهر إلى القمة الوسطى. لكن المركز لا يحتاج إلى تغيير. m.v3.y = e.v3.y;
يمكننا التثليث بشريط ضلع ومروحة واحدة. TriangulateEdgeStrip(m, cell.Color, e, cell.Color); TriangulateEdgeFan(center, m, cell.Color);
نقاط البداية والنهاية.يتحول خطوة واحدة
بعد ذلك ، ضع في اعتبارك المنعطفات الحادة التي تتعرج بين الخلايا المجاورة. سنتعامل معهم أيضا TriangulateWithRiver
. لذلك ، نحتاج إلى تحديد نوع النهر الذي نعمل معه.نهر متعرج.إذا كان للخلية نهر يتدفق في الاتجاه المعاكس ، وكذلك في الاتجاه الذي نعمل فيه ، فيجب أن يكون هذا نهرًا مستقيمًا. في هذه الحالة ، يمكننا حفظ خط الوسط الذي قمنا بحسابه بالفعل. خلاف ذلك ، فإنه يعود إلى نقطة واحدة ، مع طي خط الوسط. Vector3 centerL, centerR; if (cell.HasRiverThroughEdge(direction.Opposite())) { centerL = center + HexMetrics.GetFirstSolidCorner(direction.Previous()) * 0.25f; centerR = center + HexMetrics.GetSecondSolidCorner(direction.Next()) * 0.25f; } else { centerL = centerR = center; }
متعرج متعرج.يمكننا التعرف على المنعطفات الحادة من خلال التحقق مما إذا كانت الخلية تحتوي على نهر يمر عبر الجزء التالي أو السابق من الخلية. إذا كان هناك ، فنحن بحاجة إلى محاذاة خط الوسط مع الحافة بين هذا والجزء المجاور. يمكننا القيام بذلك عن طريق وضع الجانب المقابل من الخط في المنتصف بين المركز والزاوية المشتركة. ثم يصبح الجانب الآخر من الخط هو المركز. if (cell.HasRiverThroughEdge(direction.Opposite())) { centerL = center + HexMetrics.GetFirstSolidCorner(direction.Previous()) * 0.25f; centerR = center + HexMetrics.GetSecondSolidCorner(direction.Next()) * 0.25f; } else if (cell.HasRiverThroughEdge(direction.Next())) { centerL = center; centerR = Vector3.Lerp(center, e.v5, 0.5f); } else if (cell.HasRiverThroughEdge(direction.Previous())) { centerL = Vector3.Lerp(center, e.v1, 0.5f); centerR = center; } else { centerL = centerR = center; }
بعد تحديد مكان النقطتين اليمنى واليسرى ، يمكننا تحديد المركز الناتج عن طريق حساب متوسطها. if (cell.HasRiverThroughEdge(direction.Opposite())) { … } center = Vector3.Lerp(centerL, centerR, 0.5f);
تعويض الضلع المركزي.على الرغم من أن القناة لها نفس العرض على كلا الجانبين ، إلا أنها تبدو مضغوطة تمامًا. هذا ناتج عن تحويل خط الوسط 60 درجة. يمكنك تنعيم هذا التأثير عن طريق زيادة عرض الخط المركزي قليلاً. بدلاً من الاستيفاء ½ ، نستخدم & frac23 ؛. else if (cell.HasRiverThroughEdge(direction.Next())) { centerL = center; centerR = Vector3.Lerp(center, e.v5, 2f / 3f); } else if (cell.HasRiverThroughEdge(direction.Previous())) { centerL = Vector3.Lerp(center, e.v1, 2f / 3f); centerR = center; }
متعرج بدون ضغط.يتحول مرحلتين
الحالات المتبقية بين التعرجات والأنهار المستقيمة. هذه هي المنعطفات على مرحلتين التي تخلق أنهارًا منحنية بلطف.النهر المتعرج.للتمييز بين اتجاهين محتملين ، نحتاج إلى استخدام direction.Next().Next()
. ولكن دعونا جعله أكثر ملاءمة بإضافة HexDirection
طرق الإرشاد Next2
و Previous2
. public static HexDirection Previous2 (this HexDirection direction) { direction -= 2; return direction >= HexDirection.NE ? direction : (direction + 6); } public static HexDirection Next2 (this HexDirection direction) { direction += 2; return direction <= HexDirection.NW ? direction : (direction - 6); }
العودة الى HexMesh.TriangulateWithRiver
. الآن يمكننا التعرف على اتجاه نهرنا المتعرج direction.Next2()
. if (cell.HasRiverThroughEdge(direction.Opposite())) { centerL = center + HexMetrics.GetFirstSolidCorner(direction.Previous()) * 0.25f; centerR = center + HexMetrics.GetSecondSolidCorner(direction.Next()) * 0.25f; } else if (cell.HasRiverThroughEdge(direction.Next())) { centerL = center; centerR = Vector3.Lerp(center, e.v5, 2f / 3f); } else if (cell.HasRiverThroughEdge(direction.Previous())) { centerL = Vector3.Lerp(center, e.v1, 2f / 3f); centerR = center; } else if (cell.HasRiverThroughEdge(direction.Next2())) { centerL = centerR = center; } else { centerL = centerR = center; }
في هاتين الحالتين الأخيرتين ، نحتاج إلى تحويل خط المنتصف إلى جزء الخلية الموجود داخل المنحنى. إذا كان لدينا متجه إلى منتصف حافة صلبة ، فيمكننا استخدامه لتحديد نقطة النهاية. دعونا نتخيل أن لدينا طريقة لذلك. else if (cell.HasRiverThroughEdge(direction.Next2())) { centerL = center; centerR = center + HexMetrics.GetSolidEdgeMiddle(direction.Next()) * 0.5f; } else { centerL = center + HexMetrics.GetSolidEdgeMiddle(direction.Previous()) * 0.5f; centerR = center; }
بالطبع ، نحتاج الآن إلى إضافة مثل هذه الطريقة HexMetrics
. لديه فقط متوسط متجهين من الزوايا المجاورة وتطبيق معامل النزاهة. public static Vector3 GetSolidEdgeMiddle (HexDirection direction) { return (corners[(int)direction] + corners[(int)direction + 1]) * (0.5f * solidFactor); }
منحنيات مضغوطة قليلاً.يتم تدوير خطوط المركز الآن بشكل صحيح 30 درجة. لكنها ليست طويلة بما فيه الكفاية ، ولهذا السبب يتم ضغط القنوات قليلاً. يحدث هذا لأن نقطة منتصف الضلع أقرب إلى المركز من زاوية الضلع. مسافته تساوي نصف القطر الداخلي ، وليس المسافة الخارجية. أي أننا نعمل على نطاق خاطئ.نقوم بالفعل بالتحويل من نصف قطر خارجي إلى داخلي في HexMetrics
. نحن بحاجة إلى إجراء العملية العكسية. لذلك دعونا نجعل كل من عوامل التحويل متاحة من خلال HexMetrics
. public const float outerToInner = 0.866025404f; public const float innerToOuter = 1f / outerToInner; public const float outerRadius = 10f; public const float innerRadius = outerRadius * outerToInner;
الآن يمكننا أن ننتقل إلى المقياس الصحيح HexMesh.TriangulateWithRiver
. ستبقى القنوات مضغوطة قليلاً بسبب دورها ، ولكن هذا أقل وضوحًا بكثير من حالة التعرجات. لذلك ، لسنا بحاجة للتعويض عن ذلك. else if (cell.HasRiverThroughEdge(direction.Next2())) { centerL = center; centerR = center + HexMetrics.GetSolidEdgeMiddle(direction.Next()) * (0.5f * HexMetrics.innerToOuter); } else { centerL = center + HexMetrics.GetSolidEdgeMiddle(direction.Previous()) * (0.5f * HexMetrics.innerToOuter); centerR = center; }
منحنيات ناعمة.حزمة الوحدةالتثليث بالقرب من الأنهار
أنهارنا جاهزة. لكننا لم نقم بعد بتثليث أجزاء أخرى من الخلايا التي تحتوي على الأنهار. الآن سنغلق هذه الثقوب.ثقوب بالقرب من القنوات.إذا كانت الخلية تحتوي على نهر ، لكنها لا تتدفق في الاتجاه الحالي ، Triangulate
فسنستدعي طريقة جديدة. if (cell.HasRiver) { if (cell.HasRiverThroughEdge(direction)) { e.v3.y = cell.StreamBedY; if (cell.HasRiverBeginOrEnd) { TriangulateWithRiverBeginOrEnd(direction, cell, center, e); } else { TriangulateWithRiver(direction, cell, center, e); } } else { TriangulateAdjacentToRiver(direction, cell, center, e); } } else { TriangulateEdgeFan(center, e, cell.Color); }
في هذه الطريقة ، نملأ مثلث الخلية بشريط ومروحة. مجرد مروحة لن تكون كافية بالنسبة لنا ، لأن القمم يجب أن تتوافق مع الحافة الوسطى للأجزاء التي تحتوي على النهر. void TriangulateAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { EdgeVertices m = new EdgeVertices( Vector3.Lerp(center, e.v1, 0.5f), Vector3.Lerp(center, e.v5, 0.5f) ); TriangulateEdgeStrip(m, cell.Color, e, cell.Color); TriangulateEdgeFan(center, m, cell.Color); }
تراكب في المنحنيات والأنهار المستقيمة.تطابق القناة
بالطبع نحن بحاجة لجعل المركز الذي نستخدمه يطابق الجزء المركزي الذي تستخدمه أجزاء النهر. مع التعرجات ، كل شيء في محله ، وتتطلب المنحنيات والأنهار المستقيمة الانتباه. لذلك ، نحتاج إلى تحديد نوع النهر واتجاهه النسبي.لنبدأ بالتحقق مما إذا كنا داخل المنحنى. في هذه الحالة ، يحتوي كلا الاتجاهين السابق والتالي على النهر. إذا كان الأمر كذلك ، فنحن بحاجة إلى تحريك المركز إلى الحافة. if (cell.HasRiverThroughEdge(direction.Next())) { if (cell.HasRiverThroughEdge(direction.Previous())) { center += HexMetrics.GetSolidEdgeMiddle(direction) * (HexMetrics.innerToOuter * 0.5f); } } EdgeVertices m = new EdgeVertices( Vector3.Lerp(center, e.v1, 0.5f), Vector3.Lerp(center, e.v5, 0.5f) );
تم إصلاح حالة تدفق النهر من كلا الجانبين.إذا كان لدينا نهر في اتجاه مختلف ، ولكن ليس في الاتجاه السابق ، فإننا نتحقق لنرى ما إذا كان مستقيماً. إذا كان الأمر كذلك ، فانتقل المركز إلى الزاوية الأولى. if (cell.HasRiverThroughEdge(direction.Next())) { if (cell.HasRiverThroughEdge(direction.Previous())) { center += HexMetrics.GetSolidEdgeMiddle(direction) * (HexMetrics.innerToOuter * 0.5f); } else if ( cell.HasRiverThroughEdge(direction.Previous2()) ) { center += HexMetrics.GetFirstSolidCorner(direction) * 0.25f; } }
نصف تراكب ثابت بنهر مستقيم.لذلك قمنا بحل المشكلة مع نصف الأجزاء المجاورة للأنهار المستقيمة. الحالة الأخيرة - لدينا نهر في الاتجاه السابق ، وهو مستقيم. في هذه الحالة ، تحتاج إلى تحريك المركز إلى الزاوية التالية. if (cell.HasRiverThroughEdge(direction.Next())) { if (cell.HasRiverThroughEdge(direction.Previous())) { center += HexMetrics.GetSolidEdgeMiddle(direction) * (HexMetrics.innerToOuter * 0.5f); } else if ( cell.HasRiverThroughEdge(direction.Previous2()) ) { center += HexMetrics.GetFirstSolidCorner(direction) * 0.25f; } } else if ( cell.HasRiverThroughEdge(direction.Previous()) && cell.HasRiverThroughEdge(direction.Next2()) ) { center += HexMetrics.GetSecondSolidCorner(direction) * 0.25f; }
لا مزيد من التراكبات.حزمة الوحدةتعميم HexMesh
لقد أكملنا تثليث القنوات. الآن يمكننا ملئها بالماء. نظرًا لأن الماء يختلف عن الأرض ، فسنحتاج إلى استخدام شبكة مختلفة مع بيانات قمة مختلفة ومواد مختلفة. سيكون من الملائم للغاية أن نستخدم HexMesh
السوشي والماء. لذا دعنا نعمم HexMesh
بتحويلها إلى فئة تتعامل مع هذه الشبكات ، بغض النظر عن الغرض منها. سننقل مهمة تثليث خلاياه HexGridChunk
.تحريك طريقة بيرتورب
نظرًا لأن الطريقة Perturb
معممة تمامًا وسيتم استخدامها في أماكن مختلفة ، فلننتقل إليها HexMetrics
. أولاً ، أعد تسميته إلى HexMetrics.Perturb
. هذا اسم أسلوب غير صحيح ، لكنه يعيد صياغة كل الكود لاستخدامه الصحيح. إذا كان محرر التعليمات البرمجية الخاص بك يحتوي على وظائف خاصة لطرق النقل ، فاستخدمها.بتحريك الطريقة للداخل HexMetrics
، اجعلها عامة وثابتة ، ثم قم بتصحيح اسمها. public static Vector3 Perturb (Vector3 position) { Vector4 sample = SampleNoise(position); position.x += (sample.x * 2f - 1f) * cellPerturbStrength; position.z += (sample.z * 2f - 1f) * cellPerturbStrength; return position; }
تحريك طرق التثليث
في HexGridChunk
استبدال المتغير بمتغير hexMesh
مشترك terrain
. public HexMesh terrain;
بعد ذلك ، نقوم بإعادة بناء جميع الطرق Add…
من HexMesh
c terrain.Add…
. ثم انقل كل الطرق Triangulate…
إلى HexGridChunk
. يمكنك ثم تصحيح أسماء الطرق Add…
في HexMesh
وجعلها مشتركة. ونتيجة لذلك ، سيتم العثور على جميع طرق التثليث المعقدة ، وستبقى HexGridChunk
الطرق البسيطة لإضافة البيانات إلى الشبكة HexMesh
.لم ننتهي بعد. الآن HexGridChunk.LateUpdate
يجب أن يطلق طريقته الخاصة Triangulate
. بالإضافة إلى ذلك ، لا ينبغي أن يمرر الخلايا كحجة. لذلك ، Triangulate
قد تفقد المعلمة الخاصة به. ويجب عليه تفويض تنظيف وتطبيق بيانات الشبكة HexMesh
. void LateUpdate () { Triangulate();
إضافة الأساليب المطلوبة Clear
و Apply
في HexMesh
. public void Clear () { hexMesh.Clear(); vertices.Clear(); colors.Clear(); triangles.Clear(); } public void Apply () { hexMesh.SetVertices(vertices); hexMesh.SetColors(colors); hexMesh.SetTriangles(triangles, 0); hexMesh.RecalculateNormals(); meshCollider.sharedMesh = hexMesh; }
ماذا عن SetVertices و SetColors و SetTriangles؟Mesh
. . , .
SetTriangles
integer, . , .
أخيرًا ، قم بتوصيل تابع الشبكة يدويًا بالجزء الجاهز. لم يعد بإمكاننا فعل ذلك تلقائيًا ، لأننا سنضيف طفلًا ثانيًا إلى الشبكة قريبًا. إعادة تسميته إلى Terrain للإشارة إلى الغرض منه.تعيين الإغاثة.إعادة تسمية طفل جاهز لا يعمل؟. , . , Apply , . .
إنشاء تجمعات القوائم
على الرغم من أننا قمنا بنقل القليل من التعليمات البرمجية ، إلا أن خريطتنا يجب أن تعمل كما كانت من قبل. لن تؤدي إضافة شبكة أخرى إلى الجزء إلى تغيير ذلك. ولكن إذا فعلنا ذلك مع الحاضر HexMesh
، فقد تنشأ أخطاء.المشكلة هي أننا افترضنا أننا سنعمل فقط مع شبكة واحدة في كل مرة. سمح لنا هذا باستخدام القوائم الثابتة لتخزين بيانات الشبكة المؤقتة. ولكن بعد إضافة الماء ، سنعمل في الوقت نفسه مع شبكتين ، لذلك لم يعد بإمكاننا استخدام القوائم الثابتة.ومع ذلك ، لن نعود إلى مجموعات القوائم لكل مثيل HexMesh
. بدلاً من ذلك ، نستخدم تجمع قائمة ثابت. بشكل افتراضي ، هذا التجمع غير موجود ، لذلك دعونا نبدأ بإنشاء فئة تجمع قائمة مشتركة بأنفسنا. public static class ListPool<T> { }
كيف يعمل ListPool <T>؟, List<int>
. <T>
ListPool
, , . , T
( template).
لتخزين مجموعة من القوائم في التجمع ، يمكننا استخدام المكدس. عادةً لا أستخدم القوائم لأن Unity لا تقوم بتسلسلها ، ولكن في هذه الحالة لا يهم. using System.Collections.Generic; public static class ListPool<T> { static Stack<List<T>> stack = new Stack<List<T>>(); }
ماذا يعني المكدس <list <t>>؟. , . .
أضف طريقة ثابتة شائعة للحصول على القائمة من التجمع. إذا لم يكن المكدس فارغًا ، فسنقوم باستخراج القائمة العلوية وإرجاع هذه القائمة. خلاف ذلك ، سوف نقوم بإنشاء قائمة جديدة في المكان. public static List<T> Get () { if (stack.Count > 0) { return stack.Pop(); } return new List<T>(); }
لإعادة استخدام القوائم ، تحتاج إلى إضافتها إلى التجمع بعد الانتهاء من العمل معهم. ListPool
سوف يمسح القائمة ويدفعها إلى المكدس. public static void Add (List<T> list) { list.Clear(); stack.Push(list); }
الآن يمكننا استخدام حمامات السباحة HexMesh
. استبدل القوائم الثابتة بروابط خاصة غير ثابتة. فلنضع عليها علامة NonSerialized
حتى لا تحافظ عليها الوحدة أثناء إعادة الترجمة. أو اكتب System.NonSerialized
أو أضف using System;
في بداية النص. [NonSerialized] List<Vector3> vertices; [NonSerialized] List<Color> colors; [NonSerialized] List<int> triangles;
نظرًا لأن الشبكة يتم تنظيفها مباشرة قبل إضافة بيانات جديدة إليها ، فمن هنا تحتاج إلى الحصول على قوائم من التجمعات. public void Clear () { hexMesh.Clear(); vertices = ListPool<Vector3>.Get(); colors = ListPool<Color>.Get(); triangles = ListPool<int>.Get(); }
بعد تطبيق هذه الشبكات ، لم نعد بحاجة إليها ، لذا يمكننا هنا إضافتها إلى التجمعات. public void Apply () { hexMesh.SetVertices(vertices); ListPool<Vector3>.Add(vertices); hexMesh.SetColors(colors); ListPool<Color>.Add(colors); hexMesh.SetTriangles(triangles, 0); ListPool<int>.Add(triangles); hexMesh.RecalculateNormals(); meshCollider.sharedMesh = hexMesh; }
لذلك قمنا بتنفيذ استخدامات متعددة للقوائم ، بغض النظر عن عدد الشبكات التي نملأها في نفس الوقت.مصادم اختياري
على الرغم من أن تضاريسنا تحتاج إلى مصادم ، إلا أنها ليست ضرورية حقًا للأنهار. سوف تمر الأشعة ببساطة عبر الماء وتتقاطع مع القناة تحتها. دعونا نجعلها حتى نتمكن من تكوين وجود مصادم ل HexMesh
. نحن ندرك ذلك بإضافة حقل مشترك bool useCollider
. للتضاريس ، نشغلها. public bool useCollider;
باستخدام مصادم شبكة.نحتاج إلى إنشاء المصادم وتعيينه فقط عند تشغيله. void Awake () { GetComponent<MeshFilter>().mesh = hexMesh = new Mesh(); if (useCollider) { meshCollider = gameObject.AddComponent<MeshCollider>(); } hexMesh.name = "Hex Mesh"; } public void Apply () { … if (useCollider) { meshCollider.sharedMesh = hexMesh; } … }
ألوان اختيارية
قد تكون ألوان Vertex اختيارية أيضًا. نحتاجهم لإظهار أنواع مختلفة من الإغاثة ، لكن الماء لا يغير اللون. يمكننا جعلها اختيارية تمامًا كما جعلنا المصادم اختياريًا. public bool useCollider, useColors; public void Clear () { hexMesh.Clear(); vertices = ListPool<Vector3>.Get(); if (useColors) { colors = ListPool<Color>.Get(); } triangles = ListPool<int>.Get(); } public void Apply () { hexMesh.SetVertices(vertices); ListPool<Vector3>.Add(vertices); if (useColors) { hexMesh.SetColors(colors); ListPool<Color>.Add(colors); } … }
بالطبع ، يجب أن تستخدم التضاريس ألوان القمم ، لذا قم بتشغيلها.استخدام الألوان.الأشعة فوق البنفسجية اختياري
أثناء قيامنا بذلك ، يمكننا أيضًا إضافة دعم لإحداثيات الأشعة فوق البنفسجية الاختيارية. على الرغم من أن الإغاثة لا تستخدمها ، سنحتاج إليها للحصول على الماء. public bool useCollider, useColors, useUVCoordinates; [NonSerialized] List<Vector2> uvs; public void Clear () { hexMesh.Clear(); vertices = ListPool<Vector3>.Get(); if (useColors) { colors = ListPool<Color>.Get(); } if (useUVCoordinates) { uvs = ListPool<Vector2>.Get(); } triangles = ListPool<int>.Get(); } public void Apply () { hexMesh.SetVertices(vertices); ListPool<Vector3>.Add(vertices); if (useColors) { hexMesh.SetColors(colors); ListPool<Color>.Add(colors); } if (useUVCoordinates) { hexMesh.SetUVs(0, uvs); ListPool<Vector2>.Add(uvs); } … }
نحن لا نستخدم إحداثيات الأشعة فوق البنفسجية.لاستخدام هذه الوظيفة ، قم بإنشاء طرق لإضافة إحداثيات الأشعة فوق البنفسجية إلى المثلثات والرباعية. public void AddTriangleUV (Vector2 uv1, Vector2 uv2, Vector3 uv3) { uvs.Add(uv1); uvs.Add(uv2); uvs.Add(uv3); } public void AddQuadUV (Vector2 uv1, Vector2 uv2, Vector3 uv3, Vector3 uv4) { uvs.Add(uv1); uvs.Add(uv2); uvs.Add(uv3); uvs.Add(uv4); }
دعونا نضيف طريقة إضافية AddQuadUV
لإضافة منطقة UV مستطيلة بشكل ملائم. هذه هي الحالة القياسية عندما تكون الرباعية وملمسها متماثلان ، سنستخدمها لمياه النهر. public void AddQuadUV (float uMin, float uMax, float vMin, float vMax) { uvs.Add(new Vector2(uMin, vMin)); uvs.Add(new Vector2(uMax, vMin)); uvs.Add(new Vector2(uMin, vMax)); uvs.Add(new Vector2(uMax, vMax)); }
حزمة الوحدةالأنهار الحالية
أخيرا حان الوقت لخلق الماء! سنفعل ذلك مع رباعي ، والذي سيشير إلى سطح الماء. وبما أننا نعمل مع الأنهار ، يجب أن يتدفق الماء. للقيام بذلك ، نستخدم إحداثيات الأشعة فوق البنفسجية التي تشير إلى اتجاه النهر. لتصور هذا ، نحن بحاجة إلى تظليل جديد. لذلك ، قم بإنشاء تظليل قياسي جديد وتسميته نهر . قم بتغييره بحيث يتم تسجيل إحداثيات الأشعة فوق البنفسجية في قنوات البياض الخضراء والحمراء. Shader "Custom/River" { … void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color; o.Albedo = c.rgb * IN.color; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; o.Albedo.rg = IN.uv_MainTex; } ENDCG } FallBack "Diffuse" }
أضف إلى HexGridChunk
الحقل العام HexMesh rivers
. نقوم بتنظيفه وتطبيقه بنفس الطريقة كما في حالة الإغاثة. public HexMesh terrain, rivers; public void Triangulate () { terrain.Clear(); rivers.Clear(); for (int i = 0; i < cells.Length; i++) { Triangulate(cells[i]); } terrain.Apply(); rivers.Apply(); }
هل سيكون لدينا مكالمات سحب إضافية ، حتى لو لم يكن لدينا أنهار؟Unity , . , - .
قم بتغيير الإعداد المسبق (من خلال المثال) ، وتكرار كائن التضاريس ، وإعادة تسميته إلى الأنهار وربطه .جزء الجاهزة مع الأنهار.إنشاء المادي نهر ، وذلك باستخدام لدينا تظليل الجديد، وسوف نتأكد من أن يستخدم ما لديه من وجوه الأنهار . نقوم أيضًا بإعداد مكون الشبكة السداسية للكائن بحيث يستخدم إحداثيات الأشعة فوق البنفسجية ، لكن لا يستخدم ألوان الرأس أو المصادم.الأنهار الفرعية.تثليث المياه
قبل أن نتمكن من تثليث الماء ، نحتاج إلى تحديد مستوى سطحه. دعونا نجعلها تحولا في الارتفاع HexMetrics
، كما فعلنا مع قاع النهر. بما أن التشويه الرأسي للخلية يساوي نصف إزاحة الارتفاع ، فلنستخدمه لتغيير سطح النهر. لذا فنحن نضمن أن الماء لن يكون أبداً فوق تضاريس الخلية. public const float riverSurfaceElevationOffset = -0.5f;
لماذا لا تجعلها أقل قليلاً؟, . , .
أضف HexCell
خاصية للحصول على الموضع العمودي لسطح النهر. public float RiverSurfaceY { get { return (elevation + HexMetrics.riverSurfaceElevationOffset) * HexMetrics.elevationStep; } }
الآن يمكننا العمل HexGridChunk
! نظرًا لأننا سننشئ العديد من الزوايا الرباعية للأنهار ، فلنضيف طريقة منفصلة لذلك. دعونا نعطيها أربعة قمم وارتفاع كمعلمات. سيتيح لنا ذلك ضبط الوضع الرأسي لجميع القمم الأربعة بشكل متزامن قبل إضافة الرباعي. void TriangulateRiverQuad ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y ) { v1.y = v2.y = v3.y = v4.y = y; rivers.AddQuad(v1, v2, v3, v4); }
سنضيف هنا إحداثيات الأشعة فوق البنفسجية للرباعي. فقط انتقل من اليسار إلى اليمين ومن الأسفل إلى الأعلى. rivers.AddQuad(v1, v2, v3, v4); rivers.AddQuadUV(0f, 1f, 0f, 1f);
TriangulateWithRiver
- هذه هي الطريقة الأولى التي سنضيف إليها الزوايا الرباعية للأنهار. الرباعي الأول يقع بين الوسط والوسط. والثاني بين الوسط والأضلاع. نحن فقط نستخدم القمم التي لدينا بالفعل. نظرًا لأن هذه القمم سيتم التقليل من شأنها ، فستكون المياه نتيجة لذلك جزئيًا تحت الجدران المائلة للقناة. لذلك ، لا داعي للقلق بشأن الموضع الدقيق لحافة الماء. void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … TriangulateRiverQuad(centerL, centerR, m.v2, m.v4, cell.RiverSurfaceY); TriangulateRiverQuad(m.v2, m.v4, e.v2, e.v4, cell.RiverSurfaceY); }
أولى علامات الماء.لماذا يتغير عرض الماء؟, , — . . .
تتحرك مع التدفق
في الوقت الحالي ، لا تتوافق إحداثيات الأشعة فوق البنفسجية مع اتجاه النهر. نحن بحاجة للحفاظ على الاتساق هنا. افترض أن إحداثيات U هي 0 على الجانب الأيسر من النهر ، و 1 على اليمين ، عند النظر في اتجاه المصب. ويجب أن تختلف إحداثيات V من 0 إلى 1 في اتجاه النهر.باستخدام هذه المواصفات ، ستكون الأشعة فوق البنفسجية صحيحة عندما يتم تثليث النهر الصادر ، ولكنها ستصبح غير صحيحة وستحتاج إلى قلبها عند تثليث النهر الوارد. لتبسيط العمل ، أضف إلى TriangulateRiverQuad
المعلمة bool reversed
. استخدمه لقلب الأشعة فوق البنفسجية إذا لزم الأمر. void TriangulateRiverQuad ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y, bool reversed ) { v1.y = v2.y = v3.y = v4.y = y; rivers.AddQuad(v1, v2, v3, v4); if (reversed) { rivers.AddQuadUV(1f, 0f, 1f, 0f); } else { rivers.AddQuadUV(0f, 1f, 0f, 1f); } }
كما TriangulateWithRiver
أننا نعرف أننا في حاجة لتحويل الاتجاه، عند التعامل مع النهر واردة. bool reversed = cell.IncomingRiver == direction; TriangulateRiverQuad( centerL, centerR, m.v2, m.v4, cell.RiverSurfaceY, reversed ); TriangulateRiverQuad( m.v2, m.v4, e.v2, e.v4, cell.RiverSurfaceY, reversed );
الاتجاه المتفق عليه للأنهار.بداية ونهاية النهر
في الداخل ، TriangulateWithRiverBeginOrEnd
نحتاج فقط للتحقق مما إذا كان لدينا نهر وارد لتحديد اتجاه التدفق. ثم يمكننا إدخال نهر رباعي آخر بين الوسط والضلع. void TriangulateWithRiverBeginOrEnd ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … bool reversed = cell.HasIncomingRiver; TriangulateRiverQuad( m.v2, m.v4, e.v2, e.v4, cell.RiverSurfaceY, reversed ); }
الجزء بين المركز والوسط هو مثلث ، لذلك لا يمكننا استخدامه TriangulateRiverQuad
. الاختلاف الوحيد المهم هنا هو أن القمة المركزية تقع في منتصف النهر. لذلك ، فإن إحداثياته U تساوي دائمًا ½. center.y = m.v2.y = m.v4.y = cell.RiverSurfaceY; rivers.AddTriangle(center, m.v2, m.v4); if (reversed) { rivers.AddTriangleUV( new Vector2(0.5f, 1f), new Vector2(1f, 0f), new Vector2(0f, 0f) ); } else { rivers.AddTriangleUV( new Vector2(0.5f, 0f), new Vector2(0f, 1f), new Vector2(1f, 1f) ); }
الماء في البداية والنهاية.هل هناك أجزاء مفقودة من الماء في النهايات؟, quad , . . .
, . , . .
التدفق بين الخلايا
عند إضافة الماء بين الخلايا ، يجب أن نكون حذرين بشأن الاختلاف في الارتفاع. لكي يتدفق الماء إلى أسفل المنحدرات والمنحدرات ، TriangulateRiverQuad
يجب أن يدعم معلمتين للارتفاع. لذا دعنا نضيف واحدة أخرى. void TriangulateRiverQuad ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y1, float y2, bool reversed ) { v1.y = v2.y = y1; v3.y = v4.y = y2; rivers.AddQuad(v1, v2, v3, v4); if (reversed) { rivers.AddQuadUV(1f, 0f, 1f, 0f); } else { rivers.AddQuadUV(0f, 1f, 0f, 1f); } }
أيضًا ، من أجل الراحة ، دعنا نضيف خيارًا سيحصل على ارتفاع واحد. ستستدعي فقط طريقة أخرى. void TriangulateRiverQuad ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y, bool reversed ) { TriangulateRiverQuad(v1, v2, v3, v4, y, y, reversed); }
الآن يمكننا إضافة نهر رباعي وداخل TriangulateConnection
. كوننا بين الخلايا ، لا يمكننا معرفة نوع النهر الذي نتعامل معه على الفور. لتحديد ما إذا كان التحول ضروريًا ، يجب علينا التحقق مما إذا كان لدينا نهر وارد وما إذا كان يتحرك في اتجاهنا. if (cell.HasRiverThroughEdge(direction)) { e2.v3.y = neighbor.StreamBedY; TriangulateRiverQuad( e1.v2, e1.v4, e2.v2, e2.v4, cell.RiverSurfaceY, neighbor.RiverSurfaceY, cell.HasIncomingRiver && cell.IncomingRiver == direction ); }
النهر المكتمل.تنسيق V تمتد
حتى الآن ، في كل جزء من النهر ، لدينا إحداثيات V تتراوح من 0 إلى 1. أي أن هناك أربعة فقط على الخلية. خمسة إذا أضفنا أيضًا روابط بين الخلايا. أيا كان ما نستخدمه لنسيج النهر ، يجب أن يتكرر عدة مرات.يمكننا تقليل عدد التكرار عن طريق تمديد إحداثيات V بحيث تنتقل من 0 إلى 1 في جميع أنحاء الخلية بالإضافة إلى اتصال واحد. يمكن القيام بذلك عن طريق زيادة إحداثيات V في كل مقطع بمقدار 0.2. إذا وضعنا 0.4 في المنتصف ، فسيصبح في المنتصف 0.6 ، وعلى الحافة سيصل إلى 0.8. ثم في اتصال الخلية ، ستكون القيمة 1.إذا كان النهر يتدفق في الاتجاه المعاكس ، فلا يزال بإمكاننا وضع 0.4 في المنتصف ، ولكن في المنتصف يصبح 0.2 ، وعلى الحافة - 0. إذا واصلنا هذا حتى تنضم الخلية ، ستكون النتيجة -0.2. هذا أمر طبيعي لأنه يشبه 0.8 لمادة ذات وضع تصفية متكرر ، تمامًا مثل 0 يعادل 1.تغيير الإحداثيات V.لإنشاء دعم لهذا ، نحتاج إلى إضافة TriangulateRiverQuad
معلمة أخرى. void TriangulateRiverQuad ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y, float v, bool reversed ) { TriangulateRiverQuad(v1, v2, v3, v4, y, y, v, reversed); } void TriangulateRiverQuad ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y1, float y2, float v, bool reversed ) { … }
عندما لا يتم عكس الاتجاه ، نستخدم ببساطة الإحداثيات المرسلة في الجزء السفلي من الزوايا الرباعية ونضيف 0.2 في الأعلى. else { rivers.AddQuadUV(0f, 1f, v, v + 0.2f); }
يمكننا العمل مع الاتجاه المقلوب بطرح الإحداثيات من 0.8 و 0.6. if (reversed) { rivers.AddQuadUV(1f, 0f, 0.8f - v, 0.6f - v); }
الآن يجب أن ننقل الإحداثيات الصحيحة ، كما لو كنا نتعامل مع نهر صادر. لنبدأ TriangulateWithRiver
. TriangulateRiverQuad( centerL, centerR, m.v2, m.v4, cell.RiverSurfaceY, 0.4f, reversed ); TriangulateRiverQuad( m.v2, m.v4, e.v2, e.v4, cell.RiverSurfaceY, 0.6f, reversed );
ثم قم TriangulateConnection
بتغيير ما يلي. TriangulateRiverQuad( e1.v2, e1.v4, e2.v2, e2.v4, cell.RiverSurfaceY, neighbor.RiverSurfaceY, 0.8f, cell.HasIncomingRiver && cell.IncomingRiver == direction );
وأخيرًا TriangulateWithRiverBeginOrEnd
. TriangulateRiverQuad( m.v2, m.v4, e.v2, e.v4, cell.RiverSurfaceY, 0.6f, reversed ); center.y = m.v2.y = m.v4.y = cell.RiverSurfaceY; rivers.AddTriangle(center, m.v2, m.v4); if (reversed) { rivers.AddTriangleUV( new Vector2(0.5f, 0.4f), new Vector2(1f, 0.2f), new Vector2(0f, 0.2f) ); } else { rivers.AddTriangleUV( new Vector2(0.5f, 0.4f), new Vector2(0f, 0.6f), new Vector2(1f, 0.6f) ); }
إحداثيات V الممتدة.لعرض طي إحداثيات V بشكل صحيح ، تأكد من أنها لا تزال إيجابية في تظليل النهر. if (IN.uv_MainTex.y < 0) { IN.uv_MainTex.y += 1; } o.Albedo.rg = IN.uv_MainTex;
الإحداثياتالمنهارة V.packpackageالرسوم المتحركة النهر
بعد الانتهاء من إحداثيات الأشعة فوق البنفسجية ، يمكننا الانتقال إلى تحريك الأنهار. سيفعل شادر النهر هذا حتى لا نضطر إلى تحديث الشبكة باستمرار.لن نقوم بإنشاء تظليل نهر معقد في هذا البرنامج التعليمي ، ولكننا سنفعل ذلك لاحقًا. في الوقت الحالي ، سننشئ تأثيرًا بسيطًا يوفر فهمًا لكيفية عمل الرسوم المتحركة.يتم إنشاء الرسوم المتحركة عن طريق تحويل إحداثيات V بناءً على وقت اللعبة. الوحدة تسمح لك بالحصول على قيمته باستخدام متغير _Time
. يحتوي المكون Y على الوقت بدون تغيير ، والذي نستخدمه. تحتوي المكونات الأخرى على مقاييس زمنية مختلفة.سنتخلص من الطي على طول V ، لأننا لم نعد بحاجة إليه. بدلاً من ذلك ، نطرح الوقت الحالي من إحداثيات V. وهذا يغير الإحداثيات لأسفل ، مما يخلق الوهم بتدفق التيار المتدفق في مجرى النهر. // if (IN.uv_MainTex.y < 0) { // IN.uv_MainTex.y += 1; // } IN.uv_MainTex.y -= _Time.y; o.Albedo.rg = IN.uv_MainTex;
في ثانية واحدة ، ستصبح إحداثيات V في جميع النقاط أقل من الصفر ، لذلك لن نرى الفرق بعد الآن. مرة أخرى ، هذا أمر طبيعي عند استخدام التصفية في وضع تكرار النسيج. ولكن لمعرفة ما يحدث ، يمكننا أن نأخذ الجزء الكسري من إحداثيات V. IN.uv_MainTex.y -= _Time.y; IN.uv_MainTex.y = frac(IN.uv_MainTex.y); o.Albedo.rg = IN.uv_MainTex;
إحداثيات V المتحركة.استخدام الضوضاء
الآن أصبح نهرنا متحركًا ، ولكن في الاتجاه والسرعة هناك انتقالات حادة. نمط الأشعة فوق البنفسجية لدينا يجعلها واضحة جدًا ، ولكن سيكون من الصعب التعرف على ما إذا كنت تستخدم نمطًا شبيهًا بالماء. لذا ، بدلاً من عرض الأشعة فوق البنفسجية الخام ، لنأخذ عينة من النسيج. يمكننا استخدام نسيج الضجيج الموجود لدينا. نأخذ عينات منه ونضرب لون المادة في قناة الضجيج الأولى. void surf (Input IN, inout SurfaceOutputStandard o) { float2 uv = IN.uv_MainTex; uv.y -= _Time.y; float4 noise = tex2D(_MainTex, uv); fixed4 c = _Color * noise.r; o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; }
عيّن مادة الضجيج إلى مادة النهر وتأكد من أنها بيضاء.باستخدام نسيج الضجيج.نظرًا لأن إحداثيات V ممتدة للغاية ، فإن نسيج الضجيج يمتد أيضًا على طول النهر. لسوء الحظ ، الدورة ليست جميلة للغاية. دعنا نحاول تمديدها بطريقة أخرى - تقليل حجم إحداثيات U. إلى حد كبير سيكون كافياً. وهذا يعني أننا لن نأخذ سوى عينة من نطاق ضيق من نسيج الضجيج. float2 uv = IN.uv_MainTex; uv.x *= 0.0625; uv.y -= _Time.y;
تمدد إحداثيات U.دعنا نبطئ أيضًا إلى ربع في الثانية حتى يستغرق إكمال دورة النسيج أربع ثوانٍ. uv.y -= _Time.y * 0.25;
الضجيج الحالي.خلط الضوضاء
كل شيء يبدو بالفعل أفضل بكثير ، لكن النمط يبقى دائمًا على حاله. الماء لا يتصرف هكذا.نظرًا لأننا نستخدم فقط نطاقًا صغيرًا من الضوضاء ، يمكننا تغيير النمط عن طريق تحريك هذا النطاق على طول النسيج. يتم ذلك عن طريق إضافة الوقت إلى إحداثيات U. يجب أن نفعل ذلك ببطء ، وإلا سيبدو النهر متدفقًا بشكل جانبي. لنجرب معامل 0.005. وهذا يعني أن إكمال النمط يستغرق 200 ثانية. uv.x = uv.x * 0.0625 + _Time.y * 0.005;
تحريك الضوضاء.لسوء الحظ ، لا يبدو هذا جميلًا جدًا. لا يزال الماء يبدو ثابتًا ومن الواضح أن التحول ملحوظ ، على الرغم من أنه بطيء جدًا. يمكننا إخفاء التحول بدمج عينتي ضوضاء وتحويلهما في اتجاهين متعاكسين. وإذا استخدمنا قيمًا مختلفة قليلاً لتحريك العينة الثانية ، فسننشئ رسمًا متحركًا خفيفًا للتغيير.ونتيجة لذلك ، لا نتداخل أبدًا مع نفس نمط الضجيج ، نستخدم قناة مختلفة للعينة الثانية. float2 uv = IN.uv_MainTex; uv.x = uv.x * 0.0625 + _Time.y * 0.005; uv.y -= _Time.y * 0.25; float4 noise = tex2D(_MainTex, uv); float2 uv2 = IN.uv_MainTex; uv2.x = uv2.x * 0.0625 - _Time.y * 0.0052; uv2.y -= _Time.y * 0.23; float4 noise2 = tex2D(_MainTex, uv2); fixed4 c = _Color * (noise.r * noise2.a);
مزيج من نمطي ضوضاء متغيرين.ماء شفاف
يبدو نمطنا ديناميكيًا تمامًا. الخطوة التالية هي جعلها شفافة.أولاً ، تأكد من أن الماء لا يلقي بالظلال. يمكنك تعطيلها من خلال مكون العارض للكائن Rivers في الإعداد المسبق.تم تعطيل صب الظل.الآن قم بتبديل التظليل إلى الوضع الشفاف. للإشارة إلى ذلك ، استخدم علامات تظليل. ثم أضف #pragma surface
الكلمة الأساسية إلى السطر alpha
. أثناء وجودنا هنا ، يمكنك إزالة الكلمة الرئيسية fullforwardshadows
، لأننا ما زلنا لا نلقي الظلال. Tags { "RenderType"="Transparent" "Queue"="Transparent" } LOD 200 CGPROGRAM #pragma surface surf Standard alpha // fullforwardshadows #pragma target 3.0
الآن سنقوم بتغيير الطريقة التي نحدد بها لون النهر. بدلاً من ضرب الضجيج حسب اللون ، سنضيف ضوضاء إليه. ثم نستخدم الوظيفة saturate
لتحديد النتيجة بحيث لا تتجاوز 1. fixed4 c = saturate(_Color + noise.r * noise2.a);
سيسمح لنا ذلك باستخدام لون المادة كلون أساسي. الضوضاء ستزيد من سطوعها وتعتيمها. دعونا نحاول استخدام لون أزرق مع عتامة منخفضة إلى حد ما. ونتيجة لذلك ، نحصل على ماء شفاف أزرق مع بقع بيضاء.الماء الشفاف الملون.حزمة الوحدةإتمام
الآن بعد أن بدا أن كل شيء يعمل ، حان الوقت لتشويه القمم مرة أخرى. بالإضافة إلى تشويه حواف الخلايا ، سيجعل هذا أنهارنا غير متساوية. public const float cellPerturbStrength = 4f;
قمم مشوهة ومشوهة.دعنا نفحص التضاريس بحثًا عن المشاكل التي نشأت بسبب التشويه. يبدو أنهم! دعونا تحقق من الشلالات الطويلة.المياه المقطوعة بواسطة المنحدرات.يختفي الماء المتساقط من شلال مرتفع خلف جرف. عندما يحدث هذا ، يكون ملحوظًا جدًا ، لذلك نحتاج إلى القيام بشيء حيال ذلك.أقل وضوحا بكثير هو أن الشلالات يمكن أن تنحدر ، بدلا من النزول مباشرة إلى أسفل. على الرغم من أن المياه في الواقع لا تتدفق على هذا النحو ، إلا أنها ليست ملحوظة بشكل خاص. سوف يفسرها دماغنا بطريقة تبدو طبيعية بالنسبة لنا. لذا فقط تجاهلها.أسهل طريقة لتجنب فقدان المياه هي تعميق مجاري الأنهار. لذا سنخلق مساحة أكبر بين سطح الماء وقاع النهر. كما أنه سيجعل جدران القناة أكثر عمودية ، لذلك لا تتعمق أكثر من اللازم. دعنا نسألHexMetrics.streamBedElevationOffset
القيمة -1.75. سيؤدي ذلك إلى حل الجزء الأكبر من المشاكل ، ولن يصبح السرير عميقًا جدًا. سيظل جزء من الماء مقطوعًا ، ولكن ليس كل الشلالات. public const float streamBedElevationOffset = -1.75f;
قنوات متعمقة.حزمة الوحدةالجزء السابع: الطرق
- إضافة دعم الطريق.
- تثليث الطريق.
- نحن نجمع بين الطرق والأنهار.
- تحسين مظهر الطرق.
أولى علامات الحضارة.الخلايا مع الطرق
مثل الأنهار ، تنتقل الطرق من خلية إلى أخرى ، من خلال منتصف حواف الخلية. الفارق الكبير هو أنه لا توجد مياه تتدفق على الطرق ، لذلك فهي ثنائية الاتجاه. بالإضافة إلى ذلك ، يلزم وجود تقاطعات لشبكة طرق فعالة ، لذلك نحتاج إلى دعم أكثر من طريقين لكل خلية.إذا سمحت للطرق بالسير في جميع الاتجاهات الستة ، فيمكن أن تحتوي الخلية من صفر إلى ستة طرق. هذا ما مجموعه أربعة عشر تكوينًا ممكنًا للطريق. هذا أكثر بكثير من خمسة تكوينات نهرية ممكنة. للتعامل مع هذا ، نحتاج إلى استخدام نهج أكثر عمومية يمكنه التعامل مع جميع التكوينات.14 تكوين طريق ممكن.تتبع الطرق
إن أبسط طريقة لتتبع الطرق في الخلية هي استخدام مجموعة من القيم المنطقية. قم بإضافة الحقل الخاص للصفيف HexCell
وجعله قابلاً للتسلسل بحيث يمكنك رؤيته في المفتش. قم بتعيين حجم الصفيف من خلال الخلية الجاهزة بحيث يدعم ستة طرق. [SerializeField] bool[] roads;
خلية جاهزة مع ستة طرق.أضف طريقة للتحقق مما إذا كانت الخلية لها مسار في اتجاه معين. public bool HasRoadThroughEdge (HexDirection direction) { return roads[(int)direction]; }
سيكون من الملائم أيضًا معرفة ما إذا كان هناك طريق واحد على الأقل في الخلية ، لذلك سنضيف خاصية لذلك. ما عليك سوى التجول في الصفيف في الحلقة والعودة true
بمجرد العثور على الطريق. إذا لم تكن هناك طرق ، فارجع false
. public bool HasRoads { get { for (int i = 0; i < roads.Length; i++) { if (roads[i]) { return true; } } return false; } }
إزالة الطريق
كما هو الحال مع الأنهار ، سنضيف طريقة لإزالة جميع الطرق من الخلية. يمكن القيام بذلك من خلال حلقة تفصل بين كل طريق تم تمكينها مسبقًا. public void RemoveRoads () { for (int i = 0; i < neighbors.Length; i++) { if (roads[i]) { roads[i] = false; } } }
بالطبع ، نحتاج أيضًا إلى تعطيل الخلايا الباهظة الثمن المقابلة في الجيران. if (roads[i]) { roads[i] = false; neighbors[i].roads[(int)((HexDirection)i).Opposite()] = false; }
بعد ذلك ، نحتاج إلى تحديث كل خلية. نظرًا لأن الطرق محلية للخلايا ، فنحن بحاجة إلى تحديث الخلايا نفسها فقط دون جيرانها. if (roads[i]) { roads[i] = false; neighbors[i].roads[(int)((HexDirection)i).Opposite()] = false; neighbors[i].RefreshSelfOnly(); RefreshSelfOnly(); }
إضافة طرق
تشبه إضافة الطرق إزالة الطرق. والفرق الوحيد هو أننا نعين قيمة لـ Boolean true
، وليس false
. يمكننا إنشاء طريقة خاصة يمكنها تنفيذ كلتا العمليتين. ثم سيكون من الممكن استخدامه لإضافة وإزالة الطريق. public void AddRoad (HexDirection direction) { if (!roads[(int)direction]) { SetRoad((int)direction, true); } } public void RemoveRoads () { for (int i = 0; i < neighbors.Length; i++) { if (roads[i]) { SetRoad(i, false); } } } void SetRoad (int index, bool state) { roads[index] = state; neighbors[index].roads[(int)((HexDirection)index).Opposite()] = state; neighbors[index].RefreshSelfOnly(); RefreshSelfOnly(); }
لا يمكن أن يكون لدينا نهر وطريق يسيران في نفس الاتجاه في نفس الوقت. لذلك ، قبل إضافة الطريق ، سنتحقق مما إذا كان هناك مكان لها. public void AddRoad (HexDirection direction) { if (!roads[(int)direction] && !HasRiverThroughEdge(direction)) { SetRoad((int)direction, true); } }
بالإضافة إلى ذلك ، لا يمكن الجمع بين الطرق والمنحدرات لأنها حادة للغاية. أو ربما يجدر تمهيد الطريق من خلال جرف منخفض ، ولكن ليس من خلال ارتفاع؟ لتحديد ذلك ، نحتاج إلى إنشاء طريقة تخبرنا بفرق الارتفاع في اتجاه معين. public int GetElevationDifference (HexDirection direction) { int difference = elevation - GetNeighbor(direction).elevation; return difference >= 0 ? difference : -difference; }
الآن يمكننا أن نجعل الطرق تضيف بفارق ارتفاع صغير بما فيه الكفاية. سأقتصر على المنحدرات فقط ، أي بحد أقصى وحدة واحدة. public void AddRoad (HexDirection direction) { if ( !roads[(int)direction] && !HasRiverThroughEdge(direction) && GetElevationDifference(direction) <= 1 ) { SetRoad((int)direction, true); } }
إزالة الطرق الخاطئة
جعلنا الطرق تضيف فقط عندما يسمح. نحتاج الآن إلى التأكد من إزالتها إذا أصبحت غير صحيحة في وقت لاحق ، على سبيل المثال ، عند إضافة نهر. يمكننا حظر وضع الأنهار فوق الطرق ، ولكن الأنهار لا تقطعها الطرق. دعهم يغسلون الطريق بعيدًا عن الطريق.سيكون كافيا بالنسبة لنا أن نسأل عن الطريق false
، بغض النظر عما إذا كان الطريق. ودائما يتم تحديثه هناك كل من الخلايا، لذلك نحن لم تعد في حاجة إلى الدعوة بشكل صريح RefreshSelfOnly
في SetOutgoingRiver
. public void SetOutgoingRiver (HexDirection direction) { if (hasOutgoingRiver && outgoingRiver == direction) { return; } HexCell neighbor = GetNeighbor(direction); if (!neighbor || elevation < neighbor.elevation) { return; } RemoveOutgoingRiver(); if (hasIncomingRiver && incomingRiver == direction) { RemoveIncomingRiver(); } hasOutgoingRiver = true; outgoingRiver = direction;
عملية أخرى يمكن أن تجعل الطريق خاطئًا هي تغيير الارتفاع. في هذه الحالة ، سيتعين علينا التحقق من الطرق في جميع الاتجاهات. إذا كان فرق الارتفاع كبيرًا جدًا ، فيجب حذف الطريق الموجودة. public int Elevation { get { return elevation; } set { … for (int i = 0; i < roads.Length; i++) { if (roads[i] && GetElevationDifference((HexDirection)i) > 1) { SetRoad(i, false); } } Refresh(); } }
حزمة الوحدةتحرير الطريق
تعديل الطرق يعمل تمامًا مثل تحرير الأنهار. لذلك HexMapEditor
، يلزم تبديل واحد آخر ، بالإضافة إلى طريقة لتعيين حالته. OptionalToggle riverMode, roadMode; public void SetRiverMode (int mode) { riverMode = (OptionalToggle)mode; } public void SetRoadMode (int mode) { roadMode = (OptionalToggle)mode; }
EditCell
يجب أن تدعم الطريقة الآن الإزالة بإضافة طرق. هذا يعني أنه عند السحب والإفلات ، يمكنه تنفيذ أحد الإجراءين المحتملين. نحن بصدد إعادة هيكلة الكود قليلاً حتى عند إجراء السحب والإفلات الصحيحين ، يتم التحقق من حالة كلا المفتاحين. void EditCell (HexCell cell) { if (cell) { if (applyColor) { cell.Color = activeColor; } if (applyElevation) { cell.Elevation = activeElevation; } if (riverMode == OptionalToggle.No) { cell.RemoveRiver(); } if (roadMode == OptionalToggle.No) { cell.RemoveRoads(); } if (isDrag) { HexCell otherCell = cell.GetNeighbor(dragDirection.Opposite()); if (otherCell) { if (riverMode == OptionalToggle.Yes) { otherCell.SetOutgoingRiver(dragDirection); } if (roadMode == OptionalToggle.Yes) { otherCell.AddRoad(dragDirection); } } } } }
يمكننا بسرعة إضافة شريط طريق إلى واجهة المستخدم عن طريق نسخ شريط النهر وتغيير الطريقة التي تستدعيها المفاتيح.نتيجة لذلك ، نحصل على واجهة مستخدم عالية جدًا. لإصلاح ذلك ، قمت بتغيير تخطيط لوحة الألوان لتناسب لوحات الطرق والأنهار الأكثر إحكاما.واجهة مستخدم للطرق.نظرًا لأنني الآن أستخدم سطرين من ثلاثة خيارات للألوان ، فهناك مساحة للون آخر. لذا أضفت عنصرًا باللون البرتقالي.خمسة ألوان: الأصفر والأخضر والأزرق والبرتقالي والأبيض.يمكننا الآن تعديل الطرق ، لكنها غير مرئية حتى الآن. يمكنك استخدام المفتش للتأكد من عمل كل شيء.خلية بها طرق في المفتش.حزمة الوحدةتثليث الطريق
لعرض الطرق ، تحتاج إلى تثليثها. هذا مشابه لإنشاء شبكة للأنهار ، لن يظهر فقط في مجرى النهر.أولاً ، قم بإنشاء تظليل قياسي جديد يستخدم مرة أخرى إحداثيات الأشعة فوق البنفسجية لطلاء سطح الطريق. Shader "Custom/Road" { Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Albedo (RGB)", 2D) = "white" {} _Glossiness ("Smoothness", Range(0,1)) = 0.5 _Metallic ("Metallic", Range(0,1)) = 0.0 } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Standard fullforwardshadows #pragma target 3.0 sampler2D _MainTex; struct Input { float2 uv_MainTex; }; half _Glossiness; half _Metallic; fixed4 _Color; void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = fixed4(IN.uv_MainTex, 1, 1); o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; } ENDCG } FallBack "Diffuse" }
إنشاء مادة طريق باستخدام هذا التظليل.طريق مادي.اضبط الجاهزة للجزء بحيث يستقبل شبكة فرعية أخرى من السداسي للطرق. لا يجب أن تلقي هذه الشبكة الظلال ويجب أن تستخدم إحداثيات الأشعة فوق البنفسجية فقط. أسرع طريقة للقيام بذلك هي من خلال مثيل جاهز - تكرار كائن الأنهار واستبدال مادته.طرق الكائنات الفرعية.بعد ذلك ، أضف إلى HexGridChunk
الحقل العام HexMesh roads
وقم بإدراجه في Triangulate
. قم بتوصيله في المفتش بكائن الطرق . public HexMesh terrain, rivers, roads; public void Triangulate () { terrain.Clear(); rivers.Clear(); roads.Clear(); for (int i = 0; i < cells.Length; i++) { Triangulate(cells[i]); } terrain.Apply(); rivers.Apply(); roads.Apply(); }
كائن الطرق متصل.الطرق بين الخلايا
دعونا نلقي نظرة أولاً على أجزاء الطريق بين الخلايا. مثل الأنهار ، يتم إغلاق الطرق بواسطة اثنين متوسطة رباعية. نحن نغطي هذه الزوايا الأربعة بالكامل بأربعة زوايا للطرق بحيث يمكن استخدام مواضع القمم الستة نفسها. أضف لهذا HexGridChunk
الأسلوب TriangulateRoadSegment
. void TriangulateRoadSegment ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, Vector3 v5, Vector3 v6 ) { roads.AddQuad(v1, v2, v4, v5); roads.AddQuad(v2, v3, v5, v6); }
نظرًا لأننا لم نعد بحاجة للقلق بشأن تدفق المياه ، فإن إحداثيات V غير مطلوبة ، لذلك فإننا نعينها القيمة 0 في كل مكان. يمكننا استخدام إحداثيات U لتوضيح ما إذا كنا في منتصف الطريق أو على الجانب. دعها تساوي 1 في المنتصف ، وتساوي 0 في كلا الجانبين. void TriangulateRoadSegment ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, Vector3 v5, Vector3 v6 ) { roads.AddQuad(v1, v2, v4, v5); roads.AddQuad(v2, v3, v5, v6); roads.AddQuadUV(0f, 1f, 0f, 0f); roads.AddQuadUV(1f, 0f, 0f, 0f); }
جزء من الطريق بين الخلايا.سيكون من المنطقي استدعاء هذه الطريقة TriangulateEdgeStrip
، ولكن فقط إذا كان هناك طريق بالفعل. أضف معلمة منطقية إلى الطريقة لتمرير هذه المعلومات. void TriangulateEdgeStrip ( EdgeVertices e1, Color c1, EdgeVertices e2, Color c2, bool hasRoad ) { … }
بالطبع ، سنتلقى الآن أخطاء المترجم ، لأنه حتى الآن لم يتم إرسال هذه المعلومات حتى الآن. كحجة أخيرة في جميع الحالات ، TriangulateEdgeStrip
يمكن إضافة المكالمة false
. ومع ذلك ، يمكننا أيضًا أن نعلن أن القيمة الافتراضية لهذه المعلمة متساوية false
. ونتيجة لذلك ، ستصبح المعلمة اختيارية وستختفي أخطاء الترجمة. void TriangulateEdgeStrip ( EdgeVertices e1, Color c1, EdgeVertices e2, Color c2, bool hasRoad = false ) { … }
كيف تعمل المعلمات الاختيارية؟, . ,
int MyMethod (int x = 1, int y = 2) { return x + y; }
int MyMethod (int x, int y) { return x + y; } int MyMethod (int x) { return MyMethod(x, 2); } int MyMethod () { return MyMethod(1, 2}; }
. . . .
لتثليث الطريق ، ما عليك سوى الاتصال TriangulateRoadSegment
بالقمم الستة الوسطى ، إذا لزم الأمر. void TriangulateEdgeStrip ( EdgeVertices e1, Color c1, EdgeVertices e2, Color c2, bool hasRoad = false ) { terrain.AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); terrain.AddQuadColor(c1, c2); terrain.AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); terrain.AddQuadColor(c1, c2); terrain.AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); terrain.AddQuadColor(c1, c2); terrain.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5); terrain.AddQuadColor(c1, c2); if (hasRoad) { TriangulateRoadSegment(e1.v2, e1.v3, e1.v4, e2.v2, e2.v3, e2.v4); } }
هذه هي الطريقة التي نتعامل بها مع اتصالات الخلايا المسطحة. لدعم الطرق على الحواف ، نحتاج أيضًا إلى معرفة TriangulateEdgeTerraces
مكان إضافة الطريق. يمكنه ببساطة نقل هذه المعلومات TriangulateEdgeStrip
. void TriangulateEdgeTerraces ( EdgeVertices begin, HexCell beginCell, EdgeVertices end, HexCell endCell, bool hasRoad ) { EdgeVertices e2 = EdgeVertices.TerraceLerp(begin, end, 1); Color c2 = HexMetrics.TerraceLerp(beginCell.Color, endCell.Color, 1); TriangulateEdgeStrip(begin, beginCell.Color, e2, c2, hasRoad); for (int i = 2; i < HexMetrics.terraceSteps; i++) { EdgeVertices e1 = e2; Color c1 = c2; e2 = EdgeVertices.TerraceLerp(begin, end, i); c2 = HexMetrics.TerraceLerp(beginCell.Color, endCell.Color, i); TriangulateEdgeStrip(e1, c1, e2, c2, hasRoad); } TriangulateEdgeStrip(e2, c2, end, endCell.Color, hasRoad); }
TriangulateEdgeTerraces
دعا بالداخل TriangulateConnection
. يمكننا هنا تحديد ما إذا كان هناك بالفعل طريق يسير في الاتجاه الحالي ، سواء عند تثليث حافة ما أو عند تثليث الحواف. if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces( e1, cell, e2, neighbor, cell.HasRoadThroughEdge(direction) ); } else { TriangulateEdgeStrip( e1, cell.Color, e2, neighbor.Color, cell.HasRoadThroughEdge(direction) ); }
أجزاء الطريق بين الخلايا.الخلية على تقديم
عند رسم الطرق ، سترى أن أجزاء الطريق تظهر بين الخلايا. سيكون منتصف هذه الأجزاء باللون الأرجواني مع انتقال إلى اللون الأزرق عند الحواف.ومع ذلك ، عند تحريك الكاميرا ، قد تومض الأجزاء ، وأحيانًا تختفي تمامًا. وذلك لأن مثلثات الطرق تتداخل تمامًا مع مثلثات التضاريس. يتم تحديد مثلثات العرض بشكل عشوائي. يمكن إصلاح هذه المشكلة على مرحلتين.أولاً ، نريد رسم الطرق بعد رسم الإغاثة. يمكن تحقيق ذلك من خلال تقديمها بعد تقديم الهندسة المعتادة ، أي عن طريق وضعها في قائمة انتظار عرض لاحقة. Tags { "RenderType"="Opaque" "Queue" = "Geometry+1" }
ثانيًا ، نحتاج إلى التأكد من رسم الطرق فوق مثلثات التضاريس في نفس الموضع. يمكن القيام بذلك عن طريق إضافة إزاحة اختبار العمق. سيسمح لوحدة معالجة الرسومات بافتراض أن المثلثات أقرب إلى الكاميرا مما هي عليه بالفعل. Tags { "RenderType"="Opaque" "Queue" = "Geometry+1" } LOD 200 Offset -1, -1
الطرق عبر الخلايا
عند تثليث الأنهار ، كان علينا التعامل مع أكثر من اتجاهين للنهر لكل خلية. يمكننا تحديد خمسة خيارات ممكنة وتثليثها بشكل مختلف لإنشاء الأنهار المناسبة. ومع ذلك ، في حالة الطرق ، هناك أربعة عشر خيارًا ممكنًا. لن نستخدم مناهج منفصلة لكل من هذه الخيارات. بدلاً من ذلك ، سنقوم بمعالجة كل اتجاه من اتجاهات الخلايا الست بنفس الطريقة ، بغض النظر عن تكوين الطريق المحدد.عندما تمر طريق على طول جزء من الخلية ، سنرسمها مباشرة إلى مركز الخلية ، دون مغادرة منطقة المثلثات. سنرسم جزءًا من الطريق من الحافة إلى النصف في اتجاه المركز. ثم نستخدم مثلثين لإغلاق الباقي إلى المركز.تثليث جزء من الطريق.لتثليث هذا المخطط ، نحتاج إلى معرفة مركز الخلية والرؤوس الوسطى اليسرى واليمنى ورؤوس الحافة. أضف طريقة TriangulateRoad
مع المعلمات المناسبة. void TriangulateRoad ( Vector3 center, Vector3 mL, Vector3 mR, EdgeVertices e ) { }
لبناء جزء من الطريق ، نحتاج إلى ذروة إضافية. وهي تقع بين القمم الوسطى اليسرى واليمنى. void TriangulateRoad ( Vector3 center, Vector3 mL, Vector3 mR, EdgeVertices e ) { Vector3 mC = Vector3.Lerp(mL, mR, 0.5f); TriangulateRoadSegment(mL, mC, mR, e.v2, e.v3, e.v4); }
الآن يمكننا أيضًا إضافة المثلثين المتبقيين. TriangulateRoadSegment(mL, mC, mR, e.v2, e.v3, e.v4); roads.AddTriangle(center, mL, mC); roads.AddTriangle(center, mC, mR);
نحتاج أيضًا إلى إضافة إحداثيات الأشعة فوق البنفسجية للمثلثات. اثنان من قممهما في منتصف الطريق ، والباقي على الحافة. roads.AddTriangle(center, mL, mC); roads.AddTriangle(center, mC, mR); roads.AddTriangleUV( new Vector2(1f, 0f), new Vector2(0f, 0f), new Vector2(1f, 0f) ); roads.AddTriangleUV( new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(0f, 0f) );
في الوقت الحالي ، دعنا نقتصر على الخلايا التي لا توجد فيها أنهار. في هذه الحالات ، Triangulate
يخلق ببساطة مروحة من المثلثات. انقل هذا الرمز إلى طريقة منفصلة. ثم نضيف مكالمة TriangulateRoad
عندما يكون الطريق بالفعل. يمكن العثور على القمم الوسطى اليسرى واليمنى من خلال الاستكمال بين المركز وقمتين الزاوية. void Triangulate (HexDirection direction, HexCell cell) { … if (cell.HasRiver) { … } else { TriangulateWithoutRiver(direction, cell, center, e); } … } void TriangulateWithoutRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { TriangulateEdgeFan(center, e, cell.Color); if (cell.HasRoadThroughEdge(direction)) { TriangulateRoad( center, Vector3.Lerp(center, e.v1, 0.5f), Vector3.Lerp(center, e.v5, 0.5f), e ); } }
الطرق التي تمر بالخلايا.ضلوع الطريق
يمكننا الآن رؤية الطرق ، ولكن أقرب إلى مركز الخلايا التي تضيق. نظرًا لأننا لا نتحقق من الخيارات الأربعة عشر التي نتعامل معها ، فلا يمكننا تغيير مركز الطريق لإنشاء أشكال أكثر جمالًا. بدلاً من ذلك ، يمكننا إضافة حواف طريق إضافية في أجزاء أخرى من الخلية.عندما تمر الطرق عبر الخلية ، ولكن ليس في الاتجاه الحالي ، سنضيف مثلثًا إلى حواف الطريق. يتم تعريف هذا المثلث بواسطة القمم الوسطى اليسرى واليمنى. في هذه الحالة ، تقع القمة المركزية فقط في منتصف الطريق. الاثنان الآخران يقعان على ضلعها. void TriangulateRoadEdge (Vector3 center, Vector3 mL, Vector3 mR) { roads.AddTriangle(center, mL, mR); roads.AddTriangleUV( new Vector2(1f, 0f), new Vector2(0f, 0f), new Vector2(0f, 0f) ); }
جزء من حافة الطريق.عندما نحتاج إلى تثليث طريق كامل أو مجرد حافة ، نحتاج إلى تركه ل TriangulateRoad
. للقيام بذلك ، يجب أن تعرف هذه الطريقة ما إذا كانت الطريق تمر عبر اتجاه حافة الخلية الحالية. لذلك ، نضيف معلمة لهذا. void TriangulateRoad ( Vector3 center, Vector3 mL, Vector3 mR, EdgeVertices e, bool hasRoadThroughCellEdge ) { if (hasRoadThroughCellEdge) { Vector3 mC = Vector3.Lerp(mL, mR, 0.5f); TriangulateRoadSegment(mL, mC, mR, e.v2, e.v3, e.v4); roads.AddTriangle(center, mL, mC); roads.AddTriangle(center, mC, mR); roads.AddTriangleUV( new Vector2(1f, 0f), new Vector2(0f, 0f), new Vector2(1f, 0f) ); roads.AddTriangleUV( new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(0f, 0f) ); } else { TriangulateRoadEdge(center, mL, mR); } }
الآن TriangulateWithoutRiver
سيتعين عليها الاتصال TriangulateRoad
عندما تمر أي طرق عبر الخلية. وسيتعين عليه إرسال معلومات حول ما إذا كانت الطريق تمر عبر الحافة الحالية. void TriangulateWithoutRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { TriangulateEdgeFan(center, e, cell.Color); if (cell.HasRoads) { TriangulateRoad( center, Vector3.Lerp(center, e.v1, 0.5f), Vector3.Lerp(center, e.v5, 0.5f), e, cell.HasRoadThroughEdge(direction) ); } }
الطرق ذات الأضلاع المكتملة.تمهيد الطريق
اكتملت الطرق الآن. لسوء الحظ ، يخلق هذا النهج انتفاخات في وسط الخلايا. إن وضع القمم اليسرى واليمنى في المنتصف بين المركز والزوايا يناسبنا عندما يكون هناك طريق مجاور لها. ولكن إذا لم يكن كذلك ، فهناك انتفاخ. لتجنب ذلك ، في مثل هذه الحالات ، يمكننا وضع القمم بالقرب من المركز. بشكل أكثر تحديدا ، ثم الاستيفاء ¼ ، وليس with.دعنا ننشئ طريقة منفصلة لمعرفة أي من المتقاعمات التي يجب استخدامها. نظرًا لوجود اثنين منهم ، يمكننا وضع النتيجة Vector2
. سيكون المكون X هو مُحَقِّق للنقطة اليسرى ، والمكون Y سيكون مُحَقِّقًا للنقطة اليمنى. Vector2 GetRoadInterpolators (HexDirection direction, HexCell cell) { Vector2 interpolators; return interpolators; }
إذا كان هناك طريق يسير في الاتجاه الحالي ، فيمكننا وضع النقاط في المنتصف. Vector2 GetRoadInterpolators (HexDirection direction, HexCell cell) { Vector2 interpolators; if (cell.HasRoadThroughEdge(direction)) { interpolators.x = interpolators.y = 0.5f; } return interpolators; }
خلاف ذلك ، قد تكون الخيارات مختلفة. بالنسبة للنقطة اليسرى ، يمكننا استخدام ½ إذا كان هناك طريق يسير في الاتجاه السابق. إذا لم يكن كذلك ، فيجب علينا استخدام ¼. الأمر نفسه ينطبق على النقطة الصحيحة ، ولكن مع مراعاة الاتجاه التالي. Vector2 GetRoadInterpolators (HexDirection direction, HexCell cell) { Vector2 interpolators; if (cell.HasRoadThroughEdge(direction)) { interpolators.x = interpolators.y = 0.5f; } else { interpolators.x = cell.HasRoadThroughEdge(direction.Previous()) ? 0.5f : 0.25f; interpolators.y = cell.HasRoadThroughEdge(direction.Next()) ? 0.5f : 0.25f; } return interpolators; }
يمكنك الآن استخدام هذه الطريقة الجديدة لتحديد أي interpolators المستخدمة. بفضل هذا ، سيتم تمهيد الطرق. void TriangulateWithoutRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { TriangulateEdgeFan(center, e, cell.Color); if (cell.HasRoads) { Vector2 interpolators = GetRoadInterpolators(direction, cell); TriangulateRoad( center, Vector3.Lerp(center, e.v1, interpolators.x), Vector3.Lerp(center, e.v5, interpolators.y), e, cell.HasRoadThroughEdge(direction) ); } }
طرق سلسة.حزمة الوحدةمزيج من الأنهار والطرق
في المرحلة الحالية ، لدينا طرق وظيفية ، ولكن فقط إذا لم يكن هناك أنهار. إذا كان هناك نهر في الزنزانة ، فلن يتم تثليث الطرق.لا توجد طرق بالقرب من الأنهار.لنقم بإنشاء طريقة TriangulateRoadAdjacentToRiver
للتعامل مع هذا الموقف. نضعه على المعلمات المعتادة. سوف نسميها في بداية الطريقة TriangulateAdjacentToRiver
. void TriangulateAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { if (cell.HasRoads) { TriangulateRoadAdjacentToRiver(direction, cell, center, e); } … } void TriangulateRoadAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { }
بادئ ذي بدء ، سنفعل الشيء نفسه بالنسبة للطرق التي لا تحتوي على أنهار. سوف نتحقق مما إذا كان الطريق يمر عبر الحافة الحالية ، ونحصل على interpolators ، وننشئ قمم متوسطة ونتصل TriangulateRoad
. ولكن نظرًا لأن الأنهار ستظهر على المسار ، فنحن بحاجة إلى نقل الطرق بعيدًا عنها. ونتيجة لذلك ، سيكون مركز الطريق في وضع مختلف. نستخدم متغير لتخزين هذا الموقف الجديد roadCenter
. في البداية ، ستكون مساوية لمركز الخلية. void TriangulateRoadAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { bool hasRoadThroughEdge = cell.HasRoadThroughEdge(direction); Vector2 interpolators = GetRoadInterpolators(direction, cell); Vector3 roadCenter = center; Vector3 mL = Vector3.Lerp(roadCenter, e.v1, interpolators.x); Vector3 mR = Vector3.Lerp(roadCenter, e.v5, interpolators.y); TriangulateRoad(roadCenter, mL, mR, e, hasRoadThroughEdge); }
لذلك سننشئ طرقًا جزئية في الخلايا ذات الأنهار. الاتجاهات التي تمر من خلالها الأنهار ستقطع الفجوات في الطرق.الطرق ذات المساحات.بداية النهر أو نهايته
دعونا ننظر أولاً إلى الخلايا التي تحتوي على بداية النهر أو نهايته. حتى لا تتداخل الطرق مع الماء ، دعنا ننقل مركز الطريق من النهر. للحصول على اتجاه النهر الوارد أو الصادر ، أضف HexCell
العقار. public HexDirection RiverBeginOrEndDirection { get { return hasIncomingRiver ? incomingRiver : outgoingRiver; } }
الآن يمكننا استخدام هذه الخاصية HexGridChunk.TriangulateRoadAdjacentToRiver
لتحريك مركز الطريق في الاتجاه المعاكس. سيكون كافياً لتحريك الثلث إلى الضلع الأوسط في هذا الاتجاه. bool hasRoadThroughEdge = cell.HasRoadThroughEdge(direction); Vector2 interpolators = GetRoadInterpolators(direction, cell); Vector3 roadCenter = center; if (cell.HasRiverBeginOrEnd) { roadCenter += HexMetrics.GetSolidEdgeMiddle( cell.RiverBeginOrEndDirection.Opposite() ) * (1f / 3f); } Vector3 mL = Vector3.Lerp(roadCenter, e.v1, interpolators.x); Vector3 mR = Vector3.Lerp(roadCenter, e.v5, interpolators.y); TriangulateRoad(roadCenter, mL, mR, e, hasRoadThroughEdge);
طرق معدلة.بعد ذلك نحتاج إلى سد الفجوات. سنقوم بذلك عن طريق إضافة مثلثات إضافية إلى حواف الطريق عندما نكون قريبين من النهر. إذا كان هناك نهر في الاتجاه السابق ، فإننا نضيف مثلثًا بين مركز الطريق ومركز الخلية ونقطة الوسط اليسرى. وإذا كان النهر في الاتجاه التالي ، فإننا نضيف مثلثًا بين مركز الطريق والنقطة اليمنى الوسطى ومركز الخلية.سنفعل ذلك بغض النظر عن تكوين النهر ، لذلك ضع هذا الرمز في نهاية الطريقة. Vector3 mL = Vector3.Lerp(roadCenter, e.v1, interpolators.x); Vector3 mR = Vector3.Lerp(roadCenter, e.v5, interpolators.y); TriangulateRoad(roadCenter, mL, mR, e, hasRoadThroughEdge); if (cell.HasRiverThroughEdge(direction.Previous())) { TriangulateRoadEdge(roadCenter, center, mL); } if (cell.HasRiverThroughEdge(direction.Next())) { TriangulateRoadEdge(roadCenter, mR, center); }
لا يمكنك استخدام بيان آخر؟. , .
طرق جاهزة.الأنهار المستقيمة
تعتبر الخلايا ذات الأنهار المستقيمة صعبة بشكل خاص لأنها تقسم مركز الخلية إلى قسمين. لقد أضفنا بالفعل مثلثات إضافية لملء الفجوات بين الأنهار ، ولكن علينا أيضًا فصل الطرق الموجودة على جانبي النهر.تداخل الطرق مع نهر مستقيم.إذا لم يكن للخلية بداية أو نهاية النهر ، فيمكننا التحقق مما إذا كانت الأنهار الواردة والصادرة تسير في اتجاهين متعاكسين. إذا كان الأمر كذلك ، فلدينا نهر مباشر. if (cell.HasRiverBeginOrEnd) { roadCenter += HexMetrics.GetSolidEdgeMiddle( cell.RiverBeginOrEndDirection.Opposite() ) * (1f / 3f); } else if (cell.IncomingRiver == cell.OutgoingRiver.Opposite()) { }
لتحديد مكان النهر بالنسبة للاتجاه الحالي ، نحتاج إلى التحقق من الاتجاهات المجاورة. النهر إما يسارًا أو يمينًا. نظرًا لأننا نقوم بذلك في نهاية الطريقة ، فإننا نقوم بتخزين هذه الطلبات في متغيرات منطقية. سيؤدي ذلك أيضًا إلى تبسيط قراءة التعليمات البرمجية الخاصة بنا. bool hasRoadThroughEdge = cell.HasRoadThroughEdge(direction); bool previousHasRiver = cell.HasRiverThroughEdge(direction.Previous()); bool nextHasRiver = cell.HasRiverThroughEdge(direction.Next()); Vector2 interpolators = GetRoadInterpolators(direction, cell); Vector3 roadCenter = center; if (cell.HasRiverBeginOrEnd) { roadCenter += HexMetrics.GetSolidEdgeMiddle( cell.RiverBeginOrEndDirection.Opposite() ) * (1f / 3f); } else if (cell.IncomingRiver == cell.OutgoingRiver.Opposite()) { if (previousHasRiver) { } else { } } Vector3 mL = Vector3.Lerp(roadCenter, e.v1, interpolators.x); Vector3 mR = Vector3.Lerp(roadCenter, e.v5, interpolators.y); TriangulateRoad(roadCenter, mL, mR, e, hasRoadThroughEdge); if (previousHasRiver) { TriangulateRoadEdge(roadCenter, center, mL); } if (nextHasRiver) { TriangulateRoadEdge(roadCenter, mR, center); }
نحن بحاجة إلى تحويل مركز الطريق إلى متجه زاوي يشير في الاتجاه المعاكس من النهر. إذا كان النهر يمر من خلال الاتجاه السابق ، فهذه هي الزاوية الصلبة الثانية. خلاف ذلك ، هذه هي الزاوية الصلبة الأولى. else if (cell.IncomingRiver == cell.OutgoingRiver.Opposite()) { Vector3 corner; if (previousHasRiver) { corner = HexMetrics.GetSecondSolidCorner(direction); } else { corner = HexMetrics.GetFirstSolidCorner(direction); } }
لتحريك الطريق بحيث يكون مجاورًا للنهر ، نحتاج إلى تحريك مركز الطريق نصف المسافة إلى هذا الركن. ثم علينا أيضًا تحريك مركز الخلية ربع المسافة في هذا الاتجاه. else if (cell.IncomingRiver == cell.OutgoingRiver.Opposite()) { Vector3 corner; if (previousHasRiver) { corner = HexMetrics.GetSecondSolidCorner(direction); } else { corner = HexMetrics.GetFirstSolidCorner(direction); } roadCenter += corner * 0.5f; center += corner * 0.25f; }
طرق مقسمة.شاركنا شبكة من الطرق داخل هذه الخلية. هذا أمر طبيعي عندما تكون الطرق على جانبي النهر. ولكن إذا لم يكن هناك طريق على جانب واحد ، فسيكون لدينا جزء صغير من طريق معزول. هذا غير منطقي ، لذلك دعونا نتخلص من هذه الأجزاء.تأكد من وجود طريق يسير في الاتجاه الحالي. إذا لم يكن كذلك ، فتحقق من الاتجاه الآخر لنفس الجانب من النهر لوجود الطريق. إذا لم يكن هناك طريق عابر سواء هناك أو هناك ، فإننا نخرج من الطريقة قبل التثليث. if (previousHasRiver) { if ( !hasRoadThroughEdge && !cell.HasRoadThroughEdge(direction.Next()) ) { return; } corner = HexMetrics.GetSecondSolidCorner(direction); } else { if ( !hasRoadThroughEdge && !cell.HasRoadThroughEdge(direction.Previous()) ) { return; } corner = HexMetrics.GetFirstSolidCorner(direction); }
الطرق المقطوعة.الأنهار المتعرجة
النوع التالي من الأنهار متعرج. لا تشارك هذه الأنهار شبكة الطرق ، لذلك نحتاج فقط إلى تحريك مركز الطريق.متعرج يمر عبر الطرق.أسهل طريقة للتحقق من وجود علامات متعرجة بمقارنة اتجاهات الأنهار الواردة والصادرة. إذا كانت متجاورة ، فلدينا خط متعرج. هذا يؤدي إلى خيارين ممكنين ، اعتمادًا على اتجاه التدفق. if (cell.HasRiverBeginOrEnd) { … } else if (cell.IncomingRiver == cell.OutgoingRiver.Opposite()) { … } else if (cell.IncomingRiver == cell.OutgoingRiver.Previous()) { } else if (cell.IncomingRiver == cell.OutgoingRiver.Next()) { }
يمكننا تحريك مركز الطريق باستخدام أحد أركان اتجاه النهر الوارد. تعتمد الزاوية التي تحددها على اتجاه التدفق. انقل مركز الطريق من هذه الزاوية بمعامل 0.2. else if (cell.IncomingRiver == cell.OutgoingRiver.Previous()) { roadCenter -= HexMetrics.GetSecondCorner(cell.IncomingRiver) * 0.2f; } else if (cell.IncomingRiver == cell.OutgoingRiver.Next()) { roadCenter -= HexMetrics.GetFirstCorner(cell.IncomingRiver) * 0.2f; }
دفعت الطريق بعيدا عن التعرجات.داخل الأنهار الملتوية
تكوين النهر الأخير هو منحنى سلس. كما هو الحال مع النهر المباشر ، يمكن لهذا الطريق أيضًا فصل الطرق. ولكن في هذه الحالة ، ستكون الأطراف مختلفة. نحتاج أولاً للعمل مع داخل المنحنى.نهر منحني مع طرق معبدة.عندما يكون لدينا نهر على جانبي الاتجاه الحالي ، فإننا داخل المنحنى. else if (cell.IncomingRiver == cell.OutgoingRiver.Next()) { … } else if (previousHasRiver && nextHasRiver) { }
نحن بحاجة إلى تحريك مركز الطريق نحو الحافة الحالية للخلية ، وتقصير الطريق قليلاً. معامل 0.7 سيفعل. يجب أن يتحول مركز الخلية أيضًا بمعامل 0.5. else if (previousHasRiver && nextHasRiver) { Vector3 offset = HexMetrics.GetSolidEdgeMiddle(direction) * HexMetrics.innerToOuter; roadCenter += offset * 0.7f; center += offset * 0.5f; }
تقصير الطرق.كما في حالة الأنهار المستقيمة ، سنحتاج إلى قطع الأجزاء المعزولة من الطرق. في هذه الحالة ، يكفي التحقق من الاتجاه الحالي فقط. else if (previousHasRiver && nextHasRiver) { if (!hasRoadThroughEdge) { return; } Vector3 offset = HexMetrics.GetSolidEdgeMiddle(direction) * HexMetrics.innerToOuter; roadCenter += offset * 0.7f; center += offset * 0.5f; }
قطع الطرق.خارج الأنهار الملتوية
بعد التحقق من جميع الحالات السابقة ، كان الخيار الوحيد المتبقي هو الجزء الخارجي من النهر المنحني. في الخارج هناك ثلاثة أجزاء من الخلية. نحن بحاجة إلى إيجاد الاتجاه الأوسط. بعد استلامها ، يمكننا تحريك مركز الطريق نحو هذا الضلع بمعامل 0.25. else if (previousHasRiver && nextHasRiver) { … } else { HexDirection middle; if (previousHasRiver) { middle = direction.Next(); } else if (nextHasRiver) { middle = direction.Previous(); } else { middle = direction; } roadCenter += HexMetrics.GetSolidEdgeMiddle(middle) * 0.25f; }
تغيير الجزء الخارجي من الطريق.كخطوة أخيرة ، نحتاج إلى قطع الطرق على هذا الجانب من النهر. أسهل طريقة هي التحقق من جميع اتجاهات الطريق الثلاث بالنسبة إلى المنتصف. إذا لم تكن هناك طرق ، نتوقف عن العمل. else { HexDirection middle; if (previousHasRiver) { middle = direction.Next(); } else if (nextHasRiver) { middle = direction.Previous(); } else { middle = direction; } if ( !cell.HasRoadThroughEdge(middle) && !cell.HasRoadThroughEdge(middle.Previous()) && !cell.HasRoadThroughEdge(middle.Next()) ) { return; } roadCenter += HexMetrics.GetSolidEdgeMiddle(middle) * 0.25f; }
الطرق قبل وبعد القص.بعد معالجة جميع خيارات الأنهار ، يمكن أن تتعايش الأنهار والطرق. تتجاهل الأنهار الطرق وتتكيف الطرق مع الأنهار.مزيج من الأنهار والطرق.حزمة الوحدةمظهر الطرق
حتى تلك اللحظة ، استخدمنا إحداثيات الأشعة فوق البنفسجية كألوان للطرق. نظرًا لتغيير إحداثيات U فقط ، فقد عرضنا بالفعل الانتقال بين منتصف الطريق وحوافه.عرض إحداثيات الأشعة فوق البنفسجية.الآن بعد أن تم تثليث الطرق بشكل صحيح تمامًا ، يمكننا تغيير تظليل الطريق بحيث يعرض شيئًا أشبه بالطرق. كما في حالة الأنهار ، سيكون هذا تصورًا بسيطًا ، بدون زخرفة.سنبدأ باستخدام لون خالص للطرق. فقط استخدم لون المادة. لقد جعلتها حمراء. void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = _Color; o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; }
الطرق الحمراء.ويبدو بالفعل أفضل بكثير! ولكن دعنا نواصل ونمزج الطريق مع التضاريس ، باستخدام إحداثيات U كعامل خلط. void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = _Color; float blend = IN.uv_MainTex.x; o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = blend; }
يبدو أن هذا لم يغير أي شيء. حدث ذلك لأن شادرنا غير شفاف. الآن يحتاج إلى مزج ألفا. على وجه الخصوص ، نحن بحاجة إلى تظليل لسطح صائق التزاوج. يمكننا الحصول على تظليل المطلوبة عن طريق إضافة #pragma surface
خط إلى التوجيه decal:blend
. #pragma surface surf Standard fullforwardshadows decal:blend
مزيج الطرق.لذلك قمنا بإنشاء مزيج خطي سلس من منتصف إلى الحافة لا يبدو جميلًا جدًا. لجعلها تبدو كطريق ، نحتاج إلى منطقة صلبة ، يتبعها انتقال سريع إلى منطقة غير شفافة. يمكنك استخدام الوظيفة لهذا smoothstep
. يحول التقدم الخطي من 0 إلى 1 إلى منحنى على شكل S.تقدم خطي وسلس.تحتوي الوظيفة smoothstep
على معلمة الحد الأدنى والأقصى لتناسب المنحنى في فاصل تعسفي. تكون قيم الإدخال خارج النطاق محدودة للحفاظ على المنحنى مسطحًا. دعنا نستخدم 0.4 في بداية المنحنى و 0.7 في النهاية. هذا يعني أن إحداثيات U من 0 إلى 0.4 ستكون شفافة تمامًا. وستكون إحداثيات U من 0.7 إلى 1 معتمة تمامًا. يحدث الانتقال بين 0.4 و 0.7. float blend = IN.uv_MainTex.x; blend = smoothstep(0.4, 0.7, blend);
انتقال سريع بين المناطق الشفافة والشفافية.الطريق مع الضجيج
نظرًا لأن شبكة الطرق سيتم تشويهها ، فإن الطرق لها عروض متفاوتة. لذلك ، سيكون عرض الانتقال عند الحواف متغيرًا أيضًا. في بعض الأحيان تكون ضبابية ، وأحيانًا قاسية. مثل هذا التنوع يناسبنا ، إذا رأينا الطرق على أنها رملية أو ترابية.لنأخذ الخطوة التالية ونضيف ضوضاء إلى حواف الطريق. هذا سيجعلها أكثر تفاوتًا وأقل مضلعة. يمكننا القيام بذلك عن طريق أخذ عينات من نسيج الضوضاء. لأخذ العينات ، يمكنك استخدام إحداثيات عالم XZ ، تمامًا كما فعلنا عند تشويه رؤوس الخلايا.للوصول إلى موقع العالم في تظليل السطح ، أضف إلى بنية الإدخال float3 worldPos
. struct Input { float2 uv_MainTex; float3 worldPos; };
الآن يمكننا استخدام هذا الموضع surf
لتذوق النسيج الرئيسي. قم بالتصغير أيضًا ، وإلا سيتكرر النسيج كثيرًا. float4 noise = tex2D(_MainTex, IN.worldPos.xz * 0.025); fixed4 c = _Color; float blend = IN.uv_MainTex.x;
نقوم بتشويه الانتقال بضرب إحداثيات U في noise.x
. ولكن نظرًا لأن قيم الضوضاء في المتوسط 0.5 ، فستختفي معظم الطرق. لتجنب ذلك ، أضف 0.5 إلى الضوضاء قبل الضرب. float blend = IN.uv_MainTex.x; blend *= noise.x + 0.5; blend = smoothstep(0.4, 0.7, blend);
حواف مشوهة للطريق.لإنهاء هذا ، سنقوم أيضًا بتشويه لون الطرق. سيعطي هذا الطريق إحساسًا بالأوساخ المقابلة للحواف المشوشة.اضرب اللون في قناة ضوضاء أخرى ، على سبيل المثال noise.y
. لذا نحصل على متوسط نصف قيمة اللون. نظرًا لأن هذا كثير جدًا ، فسنخفض مقياس الضوضاء قليلاً ونضيف ثابتًا حتى يصل المجموع إلى 1. fixed4 c = _Color * (noise.y * 0.75 + 0.25);
الطرق الوعرة.حزمة الوحدة