Advertisement
  1. Game Development

تحديث التمهيدي لخلق عوالم متساوي القياس ، الجزء 2

Scroll to top
Read Time: 17 min
This post is part of a series called Primer for Creating Isometric Worlds.
An Updated Primer for Creating Isometric Worlds, Part 1

() translation by (you can also view the original English article)

Final product imageFinal product imageFinal product image
What You'll Be Creating

في هذا الجزء الأخير من سلسلة البرنامج التعليمي ، سنقوم ببناء البرنامج التعليمي الأول والتعرف على تنفيذ التقاطات ، ومشغلات ، ومبادلة المستوى ، وإيجاد المسار ، والمسار التالي ، وتمرير المستوى ، وارتفاع متساوي القياس ، ومقذوفات isometric.

1. التقاطات

الشاحنة هي العناصر التي يمكن جمعها داخل المستوى ، عادة عن طريق المشي ببساطة عليها - على سبيل المثال ، القطع النقدية ، الأحجار الكريمة ، النقدية ، الذخيرة ، إلخ.

يمكن استيعاب بيانات الاستلام في بيانات مستوىنا على النحو التالي:

1
[
2
[1,1,1,1,1,1],
3
[1,0,0,0,0,1],
4
[1,0,8,0,0,1],
5
[1,0,0,8,0,1],
6
[1,0,0,0,0,1],
7
[1,1,1,1,1,1]
8
]

في هذا المستوى من البيانات ، نستخدم الرقم 8 للإشارة إلى شاحنة صغيرة على بلاط العشب (تمثل 1 و 0 الجدران والمربعات على التوالي ، كما كان من قبل). يمكن أن تكون هذه صورة من بلاط واحد مع بلاط عشبي متراكب مع صورة الالتقاط. من خلال هذا المنطق ، سنحتاج إلى حالتين مختلفتين من البلاط لكل مربع يحتوي على شاحنة صغيرة ، أي واحدة مع بيك اب وأخرى بدون أن تظهر بعد أن يتم جمع الشاحنة.

سيكون للفن المتساوي القياس النموذجي العديد من المربعات القابلة للمشي - لنفترض أن لدينا 30. ويعني الأسلوب أعلاه أنه إذا كان لدينا N pickups ، سنحتاج إلى بلاطات N × 30 بالإضافة إلى البلاطات الأصلية الثلاثين ، حيث سيحتاج كل طرف إلى إصدار واحد مع التقاطات واحد دون. هذا ليس فعال جدا. بدلاً من ذلك ، يجب أن نحاول إنشاء هذه المجموعات ديناميكيًا.

لحل هذه المشكلة ، يمكننا استخدام نفس الطريقة التي استخدمناها لوضع البطل في البرنامج التعليمي الأول. كلما صادفنا بلاطة صغيرة ، سنضع بلاط الأعشاب أولاً ثم نضع البيك آب أعلى بلاط العشب. بهذه الطريقة ، نحتاج فقط إلى بلاطات N بالإضافة إلى 30 بلاط قابل للمشي ، لكننا سنحتاج إلى قيم عدد لتمثل كل مجموعة في بيانات المستوى. لحل الحاجة إلى قيم تمثيل N x 30 ، يمكننا الاحتفاظ بشريحة pickupArray منفصلة لتخزين بيانات الالتقاط بشكل منفصل عن مستوى levelData. يظهر المستوى المكتمل مع الاستلام أدناه:

Isometric level with coin pickupIsometric level with coin pickupIsometric level with coin pickup

على سبيل المثال ، أنا أبقي الأمور بسيطة ولا أستخدم مصفوفة إضافية للشاحنات الصغيرة.

التقاط التقاطات

يتم الكشف عن التقاطات بنفس الطريقة التي يتم بها اكتشاف ألواح التصادم ، ولكن بعد تحريك الحرف.

1
if(onPickupTile()){
2
    pickupItem();
3
}
4
5
6
function onPickupTile(){//check if there is a pickup on hero tile

7
    return (levelData[heroMapTile.y][heroMapTile.x]==8);
8
}
9

في الدالة onPickupTile() ، نتحقق مما إذا كانت قيمة صفيف levelData في إحداثيات heroMapTile هي لوحة التقاط أم لا. يشير الرقم الموجود في صفيف levelData عند إحداثي التجانب إلى نوع الالتقاط. نتحقق من الاصطدامات قبل تحريك الشخصية ولكنك تحتاج إلى التحقق من التقاطات بعد ذلك ، لأنه في حالة الاصطدامات ، يجب ألا تشغل الشخصية المكان إذا كانت مشغولة بالفعل من قِبل بلاط التصادم ، ولكن في حالة التقاطات ، يكون الحرف حرًا في التحرك فوقها.

شيء آخر يجب ملاحظته هو أن بيانات التصادم لا تتغير أبداً ، ولكن تتغير بيانات الالتقاط كلما نلتقط أحد العناصر. (عادة ما ينطوي هذا فقط على تغيير القيمة في صفيف levelData من ، مثلا ، 8 إلى 0.)

هذا يؤدي إلى مشكلة: ماذا يحدث عندما نحتاج إلى إعادة تشغيل المستوى ، وبالتالي إعادة تعيين جميع الشاحنات الصغيرة إلى مواقعها الأصلية؟ ليس لدينا المعلومات اللازمة للقيام بذلك ، حيث تم تغيير مصفوفة المستوى مع قيام اللاعب بالتقاط العناصر. الحل هو استخدام صفيف مكرر للمستوى أثناء التشغيل وللحفاظ على مصفوفة المستوى الأصلي سليمة. على سبيل المثال ، نستخدم levelData و levelDataLive [] ، نستنسخ الأخير من السابق في بداية المستوى ، ولا نغير مستوى levelData[] أثناء التشغيل.

على سبيل المثال ، أنا تفرز التقاط عشوائي على بلاط العشب الشاغر بعد كل التقاط وتزايد pickupCount. تبدو وظيفة pickupItem مثل هذا.

1
function pickupItem(){
2
    pickupCount++;
3
    levelData[heroMapTile.y][heroMapTile.x]=0;
4
    //spawn next pickup

5
    spawnNewPickup();
6
}

يجب أن تلاحظ أننا نتحقق من وجود شاحنات صغيرة كلما كان الحرف في ذلك البلاط. يمكن أن يحدث هذا عدة مرات خلال ثانية (نتحقق فقط عندما يتحرك المستخدم ، ولكن قد نذهب مستديرة ودائرية داخل مربع) ، لكن المنطق أعلاه لن يفشل ؛ نظرًا لأننا قمنا بتعيين بيانات صفيف levelData إلى 0 في المرة الأولى التي نكتشف فيها عملية التقاط ، فستُرجع جميع الفحوص اللاحقة onPickupTile() false لهذا التجانب. تحقق من المثال التفاعلي أدناه:

2. البلاط الزناد

وكما يوحي الاسم ، فإن تشغيل البلاط يسبب شيئًا ما يحدث عندما يخطو اللاعب عليه أو يضغط على مفتاح عندما يكون عليه. قد يقومون بنقل اللاعب إلى موقع مختلف ، أو فتح بوابة ، أو إنتاج عدو ، لإعطاء بعض الأمثلة. بمعنى من المعاني ، فإن التقاطات هي مجرد شكل خاص من البلاط الزناد: عندما يخطو اللاعب على بلاط يحتوي على عملة معدنية ، تختفي العملة ويزداد عداد النقود.

دعونا ننظر في كيفية تنفيذ باب يأخذ اللاعب إلى مستوى مختلف. سيكون البلاط المجاور للباب بلاط الزناد. عندما يضغط اللاعب على المفتاح x ، فسيتم الانتقال إلى المستوى التالي.

Isometric level with doors trigger tilesIsometric level with doors trigger tilesIsometric level with doors trigger tiles

لتغيير المستويات ، كل ما نحتاج إلى القيام به هو تبديل صفيف levelData مع ذلك المستوى الجديد  ، وتعيين موضع واتجاه heroMapTile الجديد للحرف البطل. افترض وجود مستويين مع الأبواب للسماح بالمرور بينهما. ونظرًا لأن البلاط الأرضي المجاور للباب سيكون عبارة عن بلاطة الزناد في كلا المستويين ، يمكننا استخدام هذا كموضع جديد للحرف عند ظهورها في المستوى.

منطق التنفيذ هنا هو نفسه مثل التقاطات ، ومرة ​​أخرى نستخدم مصفوفة levelData لتخزين قيم المشغل. بالنسبة لمثالنا ، تشير 2 إلى بلاط الباب ، والقيمة المجاورة له هي المشغل. لقد استخدمت 101 و 102 مع الاتفاقية الأساسية التي تنص على أن أي بلاطة ذات قيمة أكبر من 100 هي بلاط المشغل والقيمة أقل من 100 يمكن أن يكون المستوى الذي يؤدي إلى:

1
var level1Data=
2
[[1,1,1,1,1,1],
3
[1,1,0,0,0,1],
4
[1,0,0,0,0,1],
5
[2,102,0,0,0,1],
6
[1,0,0,0,1,1],
7
[1,1,1,1,1,1]];
8
9
var level2Data=
10
[[1,1,1,1,1,1],
11
[1,0,0,0,0,1],
12
[1,0,8,0,0,1],
13
[1,0,0,0,101,2],
14
[1,0,1,0,0,1],
15
[1,1,1,1,1,1]];

يظهر رمز التحقق من وجود حدث المشغل أدناه:

1
var xKey=game.input.keyboard.addKey(Phaser.Keyboard.X);
2
3
xKey.onUp.add(triggerListener);// add a Signal listener for up event

4
5
function triggerListener(){
6
    var trigger=levelData[heroMapTile.y][heroMapTile.x];
7
    if(trigger>100){//valid trigger tile

8
        trigger-=100;
9
        if(trigger==1){//switch to level 1

10
            levelData=level1Data;
11
        }else {//switch to level 2

12
            levelData=level2Data;
13
        }
14
        for (var i = 0; i < levelData.length; i++)
15
        {
16
            for (var j = 0; j < levelData[0].length; j++)
17
            {
18
                trigger=levelData[i][j];
19
                if(trigger>100){//find the new trigger tile and place hero there

20
                    heroMapTile.y=j;
21
                    heroMapTile.x=i;
22
                    heroMapPos=new Phaser.Point(heroMapTile.y * tileWidth, heroMapTile.x * tileWidth);
23
                    heroMapPos.x+=(tileWidth/2);
24
                    heroMapPos.y+=(tileWidth/2);
25
                }
26
            }
27
        }
28
    }
29
}

يتحقق الدالة triggerListener() ما إذا كانت قيمة صفيف بيانات المشغل عند الإحداثيات المعينة أكبر من 100. إذا كان الأمر كذلك ، فإننا نجد أي مستوى نحتاج إلى التبديل إليه بطرح 100 من قيمة التجانب. تعثر الدالة على لوحة المشغل في المستوى الجديد levelData ، والتي ستكون موضع تفرخ لبطلنا. لقد جعلت من الزناد ليتم تفعيلها عند تحرير x ؛ إذا استمعنا فقط للمفتاح الذي يتم الضغط عليه ، فإننا ننتهي في حلقة حيث نقوم بالتبديل بين المستويات طالما يتم تثبيت المفتاح ، حيث أن الحرف دائمًا ينتشر في المستوى الجديد أعلى مربع الزناد.

هنا هو عرض العمل. جرّب التقاط العناصر عن طريق المشي فوقها ومستويات المبادلة من خلال الوقوف بجانب الأبواب وتسجيل x.

3. المقذوفات

المقذوف هو شيء يتحرك في اتجاه معين بسرعة معينة ، مثل رصاصة ، تعويذة سحرية ، كرة ، إلخ. كل شيء عن القذيفة هو نفس شخصية البطل ، بغض النظر عن الارتفاع: بدلاً من الدوران على طول الأرض ، والمقذوفات غالبا ما تطفو فوقها على ارتفاع معين. سوف تسافر الطلقة فوق مستوى الخصر للشخصية ، وقد تحتاج الكرة للارتداد.

هناك أمر واحد مهم هو ملاحظة أن الارتفاع غير المتساوي هو نفس الارتفاع في العرض الجانبي ثنائي الأبعاد ، على الرغم من أنه أصغر في القيمة. لا توجد تحويلات معقدة تشارك. إذا كانت الكرة 10 بكسل فوق الأرض في الإحداثيات الديكارتية ، يمكن أن يكون 10 أو 6 بكسل فوق الأرض في إحداثيات متساوية. (في حالتنا ، المحور ذو الصلة هو المحور y).

دعونا نحاول تنفيذ كذاب الكرة في المراعي المسورة لدينا. كنوع من الواقعية ، سنضيف ظلًا للكرة. كل ما نحتاج إلى القيام به هو إضافة قيمة ارتفاع الارتداد إلى القيمة Y المتساوية في الكرة. سوف تتغير قيمة إرتفاع القفزة من إطار إلى إطار اعتماداً على الجاذبية ، وبمجرد أن تضرب الكرة الأرض ، سنقوم بقلب السرعة الحالية على طول المحور y.

قبل أن نتعامل مع الارتداد في نظام متساوي القياس ، سنرى كيف يمكننا تنفيذه في نظام الديكارتي ثنائي الأبعاد. دعونا نمثل قوة القفز من الكرة مع zValue متغير. تخيل أنه ، في البداية ، الكرة لديها قوة القفز من 100 ، لذلك zValue = 100.

سنستخدم اثنين من المتغيرات الأخرى: الزيادة في القيمة ، التي تبدأ من 0 ، والجاذبية ، والتي تحتوي على قيمة -1. كل إطار ، نحن طرح incrementValue من zValue ، ونطرح الجاذبية من incrementValue من أجل خلق تأثير مخفف. عندما يصل zValue إلى 0 ، فهذا يعني أن الكرة وصلت إلى الأرض ؛ عند هذه النقطة ، نقوم بقلب علامة الزيادة بالقيمة بضربها في -1 ، وتحويلها إلى رقم موجب. هذا يعني أن الكرة ستتحرك صعودًا من الإطار التالي ، وبالتالي ارتدادها.

إليك كيفية ظهور ذلك في الكود:

1
if(game.input.keyboard.isDown(Phaser.Keyboard.X)){
2
    zValue=100;
3
}
4
incrementValue-=gravity;
5
zValue-=incrementValue;
6
if(zValue<=0){
7
    zValue=0;
8
    incrementValue*=-1;
9
}

تظل الشفرة نفسها بالنسبة إلى طريقة العرض المتساوي القياس أيضًا ، مع وجود اختلاف بسيط في أنه يمكنك استخدام قيمة أقل لبدء zValue. انظر أدناه كيفية إضافة zValue إلى قيمة y isometric للكرة أثناء التقديم.

1
function drawBallIso(){
2
    var isoPt= new Phaser.Point();//It is not advisable to create points in update loop

3
    var ballCornerPt=new Phaser.Point(ballMapPos.x-ball2DVolume.x/2,ballMapPos.y-ball2DVolume.y/2);
4
    isoPt=cartesianToIsometric(ballCornerPt);//find new isometric position for hero from 2D map position

5
    gameScene.renderXY(ballShadowSprite,isoPt.x+borderOffset.x+shadowOffset.x, isoPt.y+borderOffset.y+shadowOffset.y, false);//draw shadow to render texture

6
    gameScene.renderXY(ballSprite,isoPt.x+borderOffset.x+ballOffset.x, isoPt.y+borderOffset.y-ballOffset.y-zValue, false);//draw hero to render texture

7
}

تحقق من المثال التفاعلي أدناه:

افهم أن الدور الذي يلعبه الظل هو دور مهم جدا يضيف إلى واقعية هذا الوهم. لاحظ أيضًا أننا الآن نستخدم إحداثي الشاشتين (x و y) لتمثيل ثلاثة أبعاد في إحداثيات isometric— إن المحور y في إحداثيات الشاشة هو أيضًا المحور z في إحداثيات isometric. هذا يمكن أن يكون مربكا!

4. العثور على ومتابعة المسار

يمثل العثور على المسار والمسار التالي عمليات معقدة إلى حد ما. هناك طرق مختلفة تستخدم خوارزميات مختلفة للعثور على المسار بين نقطتين ، ولكن نظرًا لأن مستوى levelData هو مصفوفة ثنائية الأبعاد ، فإن الأمور تكون أسهل مما قد تكون عليه. لدينا عقدات فريدة ومحددة بشكل جيد يمكن للاعب أن يشغلها ، ويمكننا بسهولة التحقق من إمكانية مشاهدتها.

الوظائف ذات الصلة

نظرة عامة تفصيلية لخوارزميات Pathfinding تقع خارج نطاق هذه المقالة ، ولكن سأحاول شرح الطريقة الأكثر شيوعًا التي تعمل بها: خوارزمية المسار الأقصر ، والتي تعتبر خوارزميات A * و Dijkstra هي تطبيقات مشهورة.

نحن نهدف إلى العثور على عقد توصيل عقدة البداية وعقدة النهاية. من العقدة البادئة ، نزور جميع العقد المجاورة الثمانية ووضع علامة عليها كلها كما تمت زيارتها ؛ تتكرر هذه العملية الأساسية لكل عقدة تمت زيارتها حديثًا ، بشكل متكرر.

كل موضوع يتتبع العقد التي تمت زيارتها. عند القفز إلى العقد المجاورة ، يتم تخطي العقد التي تمت زيارتها بالفعل (يتوقف العودية) ؛ وإلا ، تستمر العملية حتى نصل إلى نقطة النهاية ، حيث ينتهي العودية ويتم إرجاع المسار الكامل المتبع كمصفوفة عقدة. في بعض الأحيان لا يتم الوصول إلى نقطة النهاية ، وفي هذه الحالة يفشل العثور على المسار. عادةً ما ننتهي بالعثور على مسارات متعددة بين العقدتين ، وفي هذه الحالة نحصل على المسار الذي يحتوي على أصغر عدد من العقد.

مسار البحث

من غير الحكمة إعادة اختراع العجلة عندما يتعلق الأمر بخوارزميات محددة جيدًا ، لذلك سنستخدم الحلول الحالية لأغراض اكتشاف المسار. لاستخدام Phaser ، نحتاج إلى حل JavaScript ، والبرنامج الذي اخترته هو EasyStarJS. نحن تهيئة محرك البحث عن المسار على النحو التالي.

1
easystar = new EasyStar.js();
2
easystar.setGrid(levelData);
3
easystar.setAcceptableTiles([0]);
4
easystar.enableDiagonals();// we want path to have diagonals

5
easystar.disableCornerCutting();// no diagonal path when walking at wall corners

وبما أن مستوى levelData الخاص بنا يحتوي على 0 و 1 فقط ، فيمكننا تمريره مباشرة كمجموعة العقد. لقد قمنا بتعيين قيمة 0 كعقدة للمشي. يمكننا تمكين القدرة على المشي القطري ولكن يمكنك تعطيل هذا عند المشي بالقرب من أركان البلاط غير القابل للمشي.

وذلك لأن ، إذا تم تمكينه ، قد يقطع البطل في البلاط غير القابل للمشي أثناء القيام بمسيرة قطرية. في مثل هذه الحالة ، لن يسمح اكتشاف الاصطدام لدينا للبطل بالمرور. يرجى أيضًا ملاحظة أنه في المثال ، قمت بإزالة الكشف عن التصادم تمامًا لأن ذلك لم يعد ضروريًا لنموذج السير القائم على AI.

سنقوم بالكشف عن الصنبور على أي بلاطة حرة داخل المستوى وحساب المسار باستخدام وظيفة findPath. يتلقى الأسلوب رد الاتصال plotAndMove صفيف العقدة المسار الناتج. نحن نحتفل في minimap مع المسار الذي تم العثور عليه حديثا.

1
game.input.activePointer.leftButton.onUp.add(findPath)
2
3
function findPath(){
4
    if(isFindingPath || isWalking)return;
5
    var pos=game.input.activePointer.position;
6
    var isoPt= new Phaser.Point(pos.x-borderOffset.x,pos.y-borderOffset.y);
7
    tapPos=isometricToCartesian(isoPt);
8
    tapPos.x-=tileWidth/2;//adjustment to find the right tile for error due to rounding off

9
    tapPos.y+=tileWidth/2;
10
    tapPos=getTileCoordinates(tapPos,tileWidth);
11
    if(tapPos.x>-1&&tapPos.y>-1&&tapPos.x<7&&tapPos.y<7){//tapped within grid

12
        if(levelData[tapPos.y][tapPos.x]!=1){//not wall tile

13
            isFindingPath=true;
14
            //let the algorithm do the magic

15
            easystar.findPath(heroMapTile.x, heroMapTile.y, tapPos.x, tapPos.y, plotAndMove);
16
            easystar.calculate();
17
        }
18
    }
19
}
20
function plotAndMove(newPath){
21
    destination=heroMapTile;
22
    path=newPath;
23
    isFindingPath=false;
24
    repaintMinimap();
25
    if (path === null) {
26
        console.log("No Path was found.");
27
    }else{
28
        path.push(tapPos);
29
        path.reverse();
30
        path.pop();
31
        for (var i = 0; i < path.length; i++)
32
        {
33
            var tmpSpr=minimap.getByName("tile"+path[i].y+"_"+path[i].x);
34
            tmpSpr.tint=0x0000ff;
35
            //console.log("p "+path[i].x+":"+path[i].y);

36
        }
37
        
38
    }
39
}
Isometric level with the newly found path highlighted in minimapIsometric level with the newly found path highlighted in minimapIsometric level with the newly found path highlighted in minimap

المسار التالي

وبمجرد أن يكون لدينا المسار كمصفوفة للعقد ، نحتاج إلى جعله يتبعه.

لنفترض أننا نريد أن نجعل الحرف يسير على البلاط الذي نضغط عليه. نحتاج أولاً للبحث عن مسار بين العقدة التي يشغلها الحرف حاليًا والعقدة التي نقرنا عليها. إذا تم العثور على مسار ناجح ، فإننا نحتاج إلى نقل الحرف إلى العقدة الأولى في مصفوفة العقدة من خلال تعيينه كوجهة. وبمجرد وصولنا إلى نقطة الوصول ، نتحقق مما إذا كانت هناك أي عقد أخرى في مصفوفة العقدة ، وإذا كان الأمر كذلك ، فقم بتعيين العقدة التالية كوجهة - وهكذا حتى تصل العقدة النهائية.

وسنقوم أيضًا بتغيير اتجاه المشغل استنادًا إلى العقدة الحالية وعقدة الوجهة الجديدة في كل مرة نصل فيها إلى العقدة. بين العقد ، نسير فقط في الاتجاه المطلوب حتى نصل إلى نقطة الوصول. هذا هو AI بسيط جدا ، وفي المثال يتم ذلك في طريقة aiWalk مبين جزئيا أدناه.

1
function aiWalk(){
2
    if(path.length==0){//path has ended

3
        if(heroMapTile.x==destination.x&&heroMapTile.y==destination.y){
4
            dX=0;
5
            dY=0;
6
            isWalking=false;
7
            return;
8
        }
9
    }
10
    isWalking=true;
11
    if(heroMapTile.x==destination.x&&heroMapTile.y==destination.y){//reached current destination, set new, change direction

12
        //wait till we are few steps into the tile before we turn

13
        stepsTaken++;
14
        if(stepsTaken<stepsTillTurn){
15
            return;
16
        }
17
        console.log("at "+heroMapTile.x+" ; "+heroMapTile.y);
18
        //centralise the hero on the tile    

19
        heroMapSprite.x=(heroMapTile.x * tileWidth)+(tileWidth/2)-(heroMapSprite.width/2);
20
        heroMapSprite.y=(heroMapTile.y * tileWidth)+(tileWidth/2)-(heroMapSprite.height/2);
21
        heroMapPos.x=heroMapSprite.x+heroMapSprite.width/2;
22
        heroMapPos.y=heroMapSprite.y+heroMapSprite.height/2;
23
        
24
        stepsTaken=0;
25
        destination=path.pop();//whats next tile in path

26
        if(heroMapTile.x<destination.x){
27
            dX = 1;
28
        }else if(heroMapTile.x>destination.x){
29
            dX = -1;
30
        }else {
31
            dX=0;
32
        }
33
        if(heroMapTile.y<destination.y){
34
            dY = 1;
35
        }else if(heroMapTile.y>destination.y){
36
            dY = -1;
37
        }else {
38
            dY=0;
39
        }
40
        if(heroMapTile.x==destination.x){
41
            dX=0;
42
        }else if(heroMapTile.y==destination.y){
43
            dY=0;
44
        }
45
        //......

46
    }
47
}

نحتاج إلى تصفية نقاط النقرات الصالحة عن طريق تحديد ما إذا كنا قد نقرنا داخل المنطقة القابلة للمشي ، بدلاً من استخدام بلاط الجدران أو غير ذلك من الأجزاء غير القابلة للمشي.

نقطة أخرى مثيرة للاهتمام لترميز الذكاء الاصطناعي: نحن لا نريد أن تتحول الشخصية إلى مواجهة البلاط التالي في مصفوفة العقدة بمجرد وصوله إلى المجموعة الحالية ، لأن مثل هذا التحول الفوري يؤدي إلى شخصيتنا التي تمشي على حدود البلاط. بدلاً من ذلك ، يجب أن ننتظر حتى يكون الحرف بضع خطوات داخل المربع قبل أن نبحث عن الوجهة التالية. ومن الأفضل أيضًا وضع البطل يدويًا في منتصف التجانب الحالي قبل أن ننتقل إليه ، لجعله يشعر بالكمال.

تحقق من عرض العمل أدناه:

5. التمرير متساوي القياس

عندما تكون منطقة المستوى أكبر بكثير من مساحة الشاشة المتاحة ، سنحتاج إلى التمرير.

Isometric level with 12x12 visible areaIsometric level with 12x12 visible areaIsometric level with 12x12 visible area

يمكن اعتبار منطقة الشاشة المرئية مستطيلاً أصغر ضمن المستطيل الأكبر لمنطقة المستوى الكامل. التمرير هو ، في الأساس ، مجرد تحريك المستطيل الداخلي داخل أكبر واحد. عادة ، عندما يحدث هذا التمرير ، يبقى موضع البطل هو نفسه بالنسبة لمستطيل الشاشة ، عادة في مركز الشاشة. ومن المثير للاهتمام أن كل ما نحتاج إليه لتنفيذ التمرير هو تتبع نقطة الزاوية في المستطيل الداخلي.

نقطة الزاوية هذه ، التي نمثلها في إحداثيات ديكارتية ، سوف تقع داخل مربع في بيانات المستوى. للتمرير ، نقوم بزيادة موضع x- و y لنقطة الزاوية في إحداثيات الديكارتية. الآن يمكننا تحويل هذه النقطة إلى إحداثيات isometric واستخدامها لرسم الشاشة.

يجب أن تكون القيم المحولة حديثًا ، في الفضاء isometric ، هي ركن الشاشة أيضًا ، مما يعني أنها جديدة (0,0). لذلك ، عند تحليل بيانات المستوى ورسمها ، فإننا نطرح هذه القيمة من الوضع المتساوي لكل جزء من البلاط ، ويمكننا تحديد ما إذا كان موضع البلاطة الجديد يقع داخل الشاشة.

بدلاً من ذلك ، يمكننا أن نقرر أننا سنقوم فقط برسم شبكة من البلاط XxY isometric  على الشاشة لجعل حلقة الرسم فعالة بالنسبة للمستويات الأكبر.

يمكننا التعبير عن هذا بالخطوات على النحو التالي:

  • تحديث الإحداثيات الديكارتية x-and y-coordinates.
  • حول هذا إلى الفضاء isometric.
  • اطرح هذه القيمة من موضع الرسم المتساوي القياس لكل جزء.
  • ارسم عددًا محدودًا مسبقًا من البلاطات على الشاشة بدءًا من هذه الزاوية الجديدة.
  • اختياري: ارسم البلاطة فقط إذا كان موضع السحب القياسي الجديد يقع داخل الشاشة.
1
var cornerMapPos=new Phaser.Point(0,0);
2
var cornerMapTile=new Phaser.Point(0,0);
3
var visibleTiles=new Phaser.Point(6,6);
4
5
//...

6
function update(){
7
    //...

8
    if (isWalkable())
9
    {
10
        heroMapPos.x +=  heroSpeed * dX;
11
        heroMapPos.y +=  heroSpeed * dY;
12
        
13
        //move the corner in opposite direction

14
        cornerMapPos.x -=  heroSpeed * dX;
15
        cornerMapPos.y -=  heroSpeed * dY;
16
        cornerMapTile=getTileCoordinates(cornerMapPos,tileWidth);
17
        //get the new hero map tile

18
        heroMapTile=getTileCoordinates(heroMapPos,tileWidth);
19
        //depthsort & draw new scene

20
        renderScene();
21
    }
22
}
23
function renderScene(){
24
    gameScene.clear();//clear the previous frame then draw again

25
    var tileType=0;
26
    //let us limit the loops within visible area

27
    var startTileX=Math.max(0,0-cornerMapTile.x);
28
    var startTileY=Math.max(0,0-cornerMapTile.y);
29
    var endTileX=Math.min(levelData[0].length,startTileX+visibleTiles.x);
30
    var endTileY=Math.min(levelData.length,startTileY+visibleTiles.y);
31
    startTileX=Math.max(0,endTileX-visibleTiles.x);
32
    startTileY=Math.max(0,endTileY-visibleTiles.y);
33
    //check for border condition

34
    for (var i = startTileY; i < endTileY; i++)
35
    {
36
        for (var j = startTileX; j < endTileX; j++)
37
        {
38
            tileType=levelData[i][j];
39
            drawTileIso(tileType,i,j);
40
            if(i==heroMapTile.y&&j==heroMapTile.x){
41
                drawHeroIso();
42
            }
43
        }
44
    }
45
}
46
function drawHeroIso(){
47
    var isoPt= new Phaser.Point();//It is not advisable to create points in update loop

48
    var heroCornerPt=new Phaser.Point(heroMapPos.x-hero2DVolume.x/2+cornerMapPos.x,heroMapPos.y-hero2DVolume.y/2+cornerMapPos.y);
49
    isoPt=cartesianToIsometric(heroCornerPt);//find new isometric position for hero from 2D map position

50
    gameScene.renderXY(sorcererShadow,isoPt.x+borderOffset.x+shadowOffset.x, isoPt.y+borderOffset.y+shadowOffset.y, false);//draw shadow to render texture

51
    gameScene.renderXY(sorcerer,isoPt.x+borderOffset.x+heroWidth, isoPt.y+borderOffset.y-heroHeight, false);//draw hero to render texture

52
}
53
function drawTileIso(tileType,i,j){//place isometric level tiles

54
    var isoPt= new Phaser.Point();//It is not advisable to create point in update loop

55
    var cartPt=new Phaser.Point();//This is here for better code readability.

56
    cartPt.x=j*tileWidth+cornerMapPos.x;
57
    cartPt.y=i*tileWidth+cornerMapPos.y;
58
    isoPt=cartesianToIsometric(cartPt);
59
    //we could further optimise by not drawing if tile is outside screen.

60
    if(tileType==1){
61
        gameScene.renderXY(wallSprite, isoPt.x+borderOffset.x, isoPt.y+borderOffset.y-wallHeight, false);
62
    }else{
63
        gameScene.renderXY(floorSprite, isoPt.x+borderOffset.x, isoPt.y+borderOffset.y, false);
64
    }
65
}

يرجى ملاحظة أن نقطة الزاوية تتزايد في الاتجاه المعاكس لتحديث موقف البطل أثناء تحركه. هذا يضمن أن يبقى البطل بمكانه بالنسبة للشاشة. تحقق من هذا المثال (استخدم الأسهم للتمرير ، انقر فوق لزيادة الشبكة المرئية).

اثنين من الملاحظات:

  • أثناء التمرير ، قد نحتاج إلى رسم مربعات إضافية على حدود الشاشة ، وإلا فقد نرى أن البلاط يختفي ويظهر على شاشتي الشاشة.
  • إذا كان لديك بلاطات تشغل أكثر من مساحة ، فستحتاج إلى رسم المزيد من البلاط على الحدود. على سبيل المثال ، إذا كان أكبر جزء في المجموعة بأكملها يقيس X ب Y ، فستحتاج إلى رسم أكثر من بلاطات على اليسار واليمين و Y المزيد من البلاط إلى الأعلى والأسفل. هذا يجعل من أن زوايا البلاط الأكبر ستظل مرئية عند التمرير داخل أو خارج الشاشة.
  • ما زلنا بحاجة للتأكد من عدم وجود مناطق فارغة على الشاشة أثناء رسمنا بالقرب من حدود المستوى.
  • يجب أن يتم تمرير المستوى فقط حتى يتم رسم الجزء الأكثر تطرفًا في الشاشة المتطابقة - بعد ذلك ، يجب أن يستمر الحرف في مساحة الشاشة دون تمرير المستوى. لهذا ، سوف نحتاج لتتبع جميع أركان مستطيل الشاشة الداخلية ، ونخنق منطق حركة التمرير واللاعبين وفقًا لذلك. هل أنت مستعد للتحدي لمحاولة تنفيذ ذلك بنفسك؟

خاتمة

تهدف هذه السلسلة بشكل خاص للمبتدئين الذين يحاولون استكشاف عوالم الألعاب المتساوية. العديد من المفاهيم الموضحة لها مقاربات بديلة أكثر تعقيدًا قليلاً ، ولقد اخترت عمدا أسهلها.

قد لا تستوفي معظم السيناريوهات التي قد تصادفها ، ولكن يمكن استخدام المعرفة المكتسبة للبناء على هذه المفاهيم لإنشاء حلول أكثر تعقيدًا. على سبيل المثال ، سيتم تقسيم فرز العمق البسيط الذي يتم تنفيذه عندما يكون لدينا مستويات متعددة الطوابق وبلاط النظام الأساسي تتحرك من قصة إلى أخرى.

ولكن هذا هو برنامج تعليمي لوقت آخر.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Game Development tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.