خلق عوالم متساوية القياس: كتاب تمهيدي لمطوري الألعاب
() translation by (you can also view the original English article)
في هذا البرنامج التعليمي ، سأقدم لك نظرة عامة واسعة حول ما تحتاج إلى معرفته لإنشاء عوالم متساوية القياس. سوف تتعلم ما هو الإسقاط isometric ، وكيف تمثل مستويات isometric كمصفوفات 2D. سنقوم بصياغة العلاقات بين العرض والمنطق ، بحيث يمكننا التعامل بسهولة مع الكائنات على الشاشة والتعامل مع اكتشاف التصادم المستند الى التبليط. سننظر أيضا في عمق التصنيف وشخصية الرسوم المتحركة.
تريد المزيد من النصائح حول إنشاء عوالم متساوية القياس؟ تحقق من مشاركة المتابعة ، خلق عوالم متساوية القياس: تمهيدي ل Gamedevs ، تابع ، وكتاب جوال ، اساسيات تطوير لعبة ستارلينج.
1. تساوي قياس العالم
العرض متساوي القياس هو طريقة عرض تستخدم لإنشاء وهم 3D من أجل لعبة 2D خلاف ذلك - يشار إليها أحيانا باسم Pseudo 3D أو 2.5D. توضح هذه الصور (المأخوذة من Diablo 2 و Age of Empires) ما أعنيه:






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

كل من هذه المربعات تكون بنفس حجم بعضها البعض ، وكل مربع ، وبالتالي يكون ارتفاع البلاط وعرض القرميد متشابهين.
للحصول على مستوى مع المراعي المغلقة من جميع الجوانب بالجدران ، ستبدو صفيف ثنائي الأبعاد للبيانات على النحو التالي:
1 |
[[1,1,1,1,1,1], |
2 |
[1,0,0,0,0,1], |
3 |
[1,0,0,0,0,1], |
4 |
[1,0,0,0,0,1], |
5 |
[1,0,0,0,0,1], |
6 |
[1,1,1,1,1,1]] |
هنا ، يشير 0
إلى بلاط الأعشاب و 1
إلى بلاط الجدران. سيؤدي ترتيب المربعات وفقًا لبيانات المستوى إلى إنتاج صورة المستوى أدناه:

يمكننا تحسين ذلك بإضافة بلاط الزاوية وبلاط الجدران المنفصل الرأسي والأفقي ، الأمر الذي يتطلب خمسة بلاطات إضافية:
1 |
[[3,1,1,1,1,4], |
2 |
[2,0,0,0,0,2], |
3 |
[2,0,0,0,0,2], |
4 |
[2,0,0,0,0,2], |
5 |
[2,0,0,0,0,2], |
6 |
[6,1,1,1,1,5]] |

آمل أن مفهوم النهج القائم على البلاط واضح الآن. هذا هو تنفيذ شبكة 2D مباشرة ، والذي يمكننا ان نرمز له على النحو التالي:
1 |
for (i, loop through rows) |
2 |
for (j, loop through columns) |
3 |
x = j * tile width |
4 |
y = i * tile height |
5 |
tileType = levelData[i][j] |
6 |
placetile(tileType, x, y) |
هنا نفترض أن عرض البلاطة وارتفاع البلاطة متساويان (ونفس الشيء لجميع المربعات) ، ومطابقة أبعاد صور البلاط. لذا ، فإن عرض البلاط وارتفاع البلاطة لهذا المثال كلاهما 50px ، مما يجعل حجم المستوى الإجمالي 300 × 300 بكسل - أي ، ستة صفوف وست أعمدة من البلاط قياس 50 × 50 بكسل لكل منهما.
في النهج القائم على التبليط العادي ، نقوم إما بتطبيق عرض من أعلى لأسفل أو عرض جانبي ؛ للحصول على عرض متساوي القياس ، نحتاج إلى تنفيذ الإسقاط isometric.
3. الإسقاط متساوي القياس
إن أفضل تفسير تقني لماهية "الإسقاط متساوي القياس" يعني ، حسب علمي ، هذا المقال من كلينت بيلانجر:
نحن زاوية الكاميرا لدينا على محورين (أرجح الكاميرا 45 درجة إلى جانب واحد ، ثم 30 درجة لأسفل). هذا يخلق شبكة شكل الماس (المعين) حيث تكون مساحات الشبكة ضعف عرضها كما هي طويلة. وقد شاع هذا النمط من الألعاب الاستراتيجية وألعاب RPG. إذا نظرنا إلى المكعب في هذا العرض ، فستظهر ثلاثة جوانب (أعلى وجانبين متقابلين).
على الرغم من أن الأمر يبدو معقدًا بعض الشيء ، إلا أن تنفيذ هذا العرض واضح في الواقع. ما نحتاج إلى فهمه هو العلاقة بين المساحة ثنائية الأبعاد والمساحة متساوية القياس - أي ، العلاقة بين بيانات المستوى والعرض ؛ التحويل من الإحداثيات "الديكارتية" من أعلى إلى أسفل إلى إحداثيات متساوي القياس.



(نحن لا نفكر في تقنية مبنية على الشكل السداسي ، وهي طريقة أخرى لتنفيذ عوالم متساوية القياس.)
وضع البلاط متساوي القياس
اسمحوا لي أن أحاول تبسيط العلاقة بين بيانات المستوى المخزنة كمصفوفة ثنائية الأبعاد والمظهر متساوي القياس - أي كيف نقوم بتحويل الإحداثيات الديكارتية إلى إحداثيات غير متساوية.
سنحاول إنشاء عرض متساوي القياس لبياناتنا على مستوى الأراضي العشبية المغلقة:
1 |
[[1,1,1,1,1,1], |
2 |
[1,0,0,0,0,1], |
3 |
[1,0,0,0,0,1], |
4 |
[1,0,0,0,0,1], |
5 |
[1,0,0,0,0,1], |
6 |
[1,1,1,1,1,1]] |
في هذا السيناريو ، يمكننا تحديد منطقة قابلة للمشي عن طريق التحقق مما إذا كان عنصر المصفوفة هو 0
في هذا التنسيق ، وبالتالي يشير إلى العشب. وكان تنفيذ العرض ثنائي الأبعاد للمستوى أعلاه بمثابة تكرار مباشر مع حلقتين ، حيث تم وضع مربعات متوازنة مع كل ارتفاع ثابت للبلاط وقيم عرض البلاط.
1 |
for (i, loop through rows) |
2 |
for (j, loop through columns) |
3 |
x = j * tile width |
4 |
y = i * tile height |
5 |
tileType = levelData[i][j] |
6 |
placetile(tileType, x, y) |
بالنسبة إلى طريقة العرض المتساوي القياس ، يبقى الرمز هو نفسه ، ولكن تتغير الدالة placeTile ()
.
للحصول على عرض متساوي القياس ، نحتاج إلى حساب الإحداثيات المتشابهة المقابلة داخل الحلقات.
معادلات القيام بذلك هي كما يلي ، حيث تمثل isoX
و isoY
isometric x- و y-coordinates ، و cartX
و cartY
تمثلان إحداثيات الديكارتية x و y-:
1 |
//Cartesian to isometric:
|
2 |
|
3 |
isoX = cartX - cartY; |
4 |
isoY = (cartX + cartY) / 2; |
1 |
//Isometric to Cartesian:
|
2 |
|
3 |
cartX = (2 * isoY + isoX) / 2; |
4 |
cartY = (2 * isoY - isoX) / 2; |
توضح هذه الوظائف كيف يمكنك التحويل من نظام إلى آخر:
1 |
function isoTo2D(pt:Point):Point{ |
2 |
var tempPt:Point = new Point(0, 0); |
3 |
tempPt.x = (2 * pt.y + pt.x) / 2; |
4 |
tempPt.y = (2 * pt.y - pt.x) / 2; |
5 |
return(tempPt); |
6 |
}
|
1 |
function twoDToIso(pt:Point):Point{ |
2 |
var tempPt:Point = new Point(0,0); |
3 |
tempPt.x = pt.x - pt.y; |
4 |
tempPt.y = (pt.x + pt.y) / 2; |
5 |
return(tempPt); |
6 |
}
|
يبدو pseudocode للحلقة كما يلي:
1 |
for(i, loop through rows) |
2 |
for(j, loop through columns) |
3 |
x = j * tile width |
4 |
y = i * tile height |
5 |
tileType = levelData[i][j] |
6 |
placetile(tileType, twoDToIso(new Point(x, y))) |



على سبيل مثال، دعونا نرى كيف يحصل موقف نموذجي 2D يحويلها إلى وضع متساوي القياس:
1 |
2D point = [100, 100]; |
2 |
// twoDToIso(2D point) will be calculated as below |
3 |
isoX = 100 - 100; // = 0 |
4 |
isoY = (100 + 100) / 2; // = 100 |
5 |
Iso point == [0, 100]; |
وبالمثل ، فإن إدخال [0 ، 0]
سيؤدي إلى [0 ، 0]
، و [10 ، 5]
سيعطي [5 ، 7.5]
.
تمكّننا الطريقة السابقة من إنشاء ارتباط مباشر بين بيانات مستوى ثنائي الأبعاد والإحداثيات المتشابهة. يمكننا العثور على إحداثيات البلاط في بيانات المستوى من إحداثيات ديكارتية باستخدام هذه الوظيفة:
1 |
function getTileCoordinates(pt:Point, tileHeight:Number):Point{ |
2 |
var tempPt:Point = new Point(0, 0); |
3 |
tempPt.x = Math.floor(pt.x / tileHeight); |
4 |
tempPt.y = Math.floor(pt.y / tileHeight); |
5 |
return(tempPt); |
6 |
}
|
(هنا ، نفترض بشكل أساسي أن ارتفاع البلاطة وعرض البلاط متساويان ، كما هو الحال في معظم الحالات.)
ومن ثم ، من إحداثيات الشاشة (متساوي القياس) ، يمكننا العثور على إحداثيات البلاط عن طريق الاتصال:
1 |
getTileCoordinates(isoTo2D(screen point), tile height); |
يمكن أن تكون نقطة الشاشة هذه ، على سبيل المثال ، موضع نقرة بالماوس أو موضع التقاط.
نصيحة: هناك طريقة أخرى لوضعها ، وهي نموذج Zigzag ، الذي يتبع أسلوبًا مختلفًا تمامًا.
تتحرك في الإحداثيات متساوية القياس
الحركة سهلة للغاية: يمكنك التعامل مع بيانات عالم اللعبة الخاصة بك في الإحداثيات الديكارتية واستخدام الوظائف السابقة لتحديثها على الشاشة. على سبيل المثال ، إذا كنت ترغب في نقل حرف إلى الأمام في الاتجاه الصاعد y
، فيمكنك ببساطة زيادة الخاصية y الخاصة بها ثم تحويل موضعها إلى إحداثيات isometric:
1 |
y = y + speed; |
2 |
placetile(twoDToIso(new Point(x, y))) |
تصنيف العمق
بالإضافة إلى التنسيب العادي ، سنحتاج إلى الاهتمام بالفرز العميق لرسم عالم متساوي القياس. هذا يجعلك متأكدًا من أن العناصر القريبة من المشغل مرسومة فوق العناصر البعيدة.
إن أبسط طريقة للفرز العميق هي ببساطة استخدام قيمة الإحداثي الصادي الديكارتية ، كما هو مذكور في هذا التلميح السريع: كلما زادت شاشة الكائن ، كلما كان يجب رسمه في وقت سابق. هذا العمل جيد طالما أننا لا نملك أي نقوش التي تشغل أكثر من مساحة واحدة للبلاط.
الطريقة الأكثر فعالية لفرزعميق لعوالم isometric هو كسر جميع البلاط إلى أبعاد بلاط واحد القياس وعدم السماح لصور أكبر. على سبيل المثال ، هنا هذا البلاط الذي لا يتناسب مع حجم البلاط القياسي - انظر كيف يمكننا تقسيمه إلى مربعات متعددة والتي تناسب كل منها أبعاد البلاط:



4. خلق الفن
يمكن للفن متساوي القياس أن يكون فن بيكسل ، ولكن ليس بالضرورة أن يكون. عند التعامل مع فن بكسل متساوي القياس ، يخبرك دليل RhysD بكل شيء تحتاج إلى معرفته. يمكن العثور على بعض النظريات على ويكيبيديا كذلك.
عند إنشاء فن متساوي القياس ، والقواعد العامة هي
- ابدأ بشبكة متوازنة فارغة وتمتاز بدقة بكسل.
- محاولة لكسر الفن في صورة بلاط واحد متساوي القياس.
- حاول أن تتأكد من أن كل جزء من البلاط قابل للمشي أو غير قابل للمشي. سيكون الأمر معقدًا إذا احتجنا إلى استخدام بلاط واحد يحتوي على مناطق قابلة للمشي وغير قابلة للمشي.
- سيحتاج معظم البلاط إلى بلاط بسلاسة في اتجاه واحد أو أكثر.
- يمكن أن تكون خادعة الظلال لتنفيذ ما لم نستخدم نهج الطبقات حيث نرسم الظلال على طبقة الأرض ثم رسم البطل (أو أشجار أو كائنات أخرى) على الطبقة العليا. إذا كان الأسلوب الذي تستخدمه ليس متعدد الطبقات ، فتأكد من أن الظلال تسقط إلى الأمام بحيث لا تسقط ، على سبيل المثال ، البطل عندما يقف خلف شجرة.
- في حالة احتياجك إلى استخدام صورة للبلاط أكبر من حجم المقياس القياسي المتساوي ، حاول استخدام بُعد يعد مضاعفًا لحجم ملف iso. من الأفضل أن يكون هناك نهج متعدد الطبقات في مثل هذه الحالات ، حيث يمكننا تقسيم الفن إلى قطع مختلفة بناءً على ارتفاعه. على سبيل المثال ، يمكن تقسيم الشجرة إلى ثلاث قطع: الجذر ، والجذع ، وأوراق الشجر. هذا يجعل من السهل فرز الأعماق حيث يمكننا رسم القطع في الطبقات المطابقة التي تتوافق مع ارتفاعاتها.
ستؤدي المكوّنات متساوية الأبعاد التي يزيد حجمها عن أبعاد البلاط الواحد إلى حدوث مشكلات تتعلق بفرز عميق. تمت مناقشة بعض المشكلات في هذه الارتباطات:
5. أحرف متساوية القياس
لا يتم تعقيد تنفيذ الأحرف في طريقة العرض متساوي القياس كما قد يبدو. فن الحرف يجب أن يتم إنشاؤه وفقا لمعايير معينة. سنحتاج أولاً إلى إصلاح عدد الاتجاهات المسموح بها في لعبتنا - عادة ما توفر الألعاب حركة رباعية أو حركة ثمانية.



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



نحن نضع الشخصيات بنفس الطريقة التي نضع فيها البلاط. يتم إنجاز حركة الحرف من خلال حساب الحركات في الإحداثيات الديكارتية ثم التحويل إلى إحداثيات متساوية. لنفترض أننا نستخدم لوحة المفاتيح للتحكم في الحرف.
سنقوم بتعيين متغيرين ، dX
و dY
، بناءً على مفاتيح الاتجاه التي يتم الضغط عليها. بشكل افتراضي ، ستكون هذه المتغيرات 0
، وسيتم تحديثها وفقًا للرسم البياني أدناه ، حيث تشير U
و D
و R
و L
إلى مفاتيح الأسهم لأعلى ولأسفل ولليمين ولليسار على التوالي. تمثل قيمة 1
تحت المفتاح المفتاح الذي يتم ضغطه ؛ 0
يعني أن المفتاح لا يتم الضغط عليه.
1 |
Key Pos |
2 |
U D R L dX dY |
3 |
================ |
4 |
0 0 0 0 0 0 |
5 |
1 0 0 0 0 1 |
6 |
0 1 0 0 0 -1 |
7 |
0 0 1 0 1 0 |
8 |
0 0 0 1 -1 0 |
9 |
1 0 1 0 1 1 |
10 |
1 0 0 1 -1 1 |
11 |
0 1 1 0 1 -1 |
12 |
0 1 0 1 -1 -1 |
الآن ، باستخدام قيم dX
و dY
، يمكننا تحديث الإحداثيات الديكارتية على النحو التالي:
1 |
newX = currentX + (dX * speed); |
2 |
newY = currentY + (dY * speed); |
لذا فإن dX
و dY
يقفان للتغيير في وضعي x و y الخاصين بالحرف ، على أساس الضغط على المفاتيح.
يمكننا بسهولة حساب الإحداثيات متساوية القياس الجديدة ، كما ناقشنا بالفعل:
1 |
Iso = twoDToIso(new Point(newX, newY)) |
بمجرد أن نحصل على الوضع القياسي الجديد ، نحتاج إلى نقل الشخصية إلى هذا الموضع. استنادًا إلى القيم التي لدينا في dX
و dY
، يمكننا تحديد الاتجاه الذي تواجهه الحرف واستخدام صورة الحرف المقابلة.
كشف التصادم
يتم إجراء اكتشاف التصادم عن طريق التحقق مما إذا كان التبليط في الموضع الجديد المحسوب عبارة عن بلاط غير قابلة للمشاة. لذا ، بمجرد العثور على الموقع الجديد ، لا نقوم بتحريك الشخصية هناك ، ولكن تحقق أولاً لمعرفة ما الذي يشغله في هذه المساحة.
1 |
tile coordinate = getTileCoordinates(isoTo2D(iso point), tile height); |
2 |
if (isWalkable(tile coordinate)) { |
3 |
moveCharacter(); |
4 |
} else { |
5 |
//do nothing; |
6 |
} |
في الدالة isWalkable ()
، نتحقق مما إذا كانت قيمة مصفوفة بيانات المستوى في الإحداثيات المحددة عبارة عن بلاط قابل للسحب أم لا. يجب أن نحرص على تحديث الاتجاه الذي تواجهه الشخصية - حتى لو لم يتحرك ، كما هو الحال في حالة ضربه على بلاط غير قابل للسير.
ترتيب التصنيف مع الأحرف
ضع في اعتبارك شخصية وبلاط شجرة في عالم متساوي القياس.
لفهم عمق التصنيف بشكل صحيح ، يجب علينا أن نفهم أنه كلما كان إحداثيات x- و y-character أقل من تلك الخاصة بالشجرة ، تتداخل الشجرة مع الحرف. عندما تكون إحداثيات x- و y للحرف أكبر من تلك الخاصة بالشجرة ، تتداخل الحرف مع الشجرة.
عندما يكون لديهم نفس الإحداثي السيني ، فإننا نقررالاعتمادًا على الإحداثي الصادي فقط: أيهما كان التنسيق الأعلى للإحداثيات y يتداخل مع الآخر. عندما يكون لديهم نفس الإحداثي الصادي ، فإننا نقرر الاعتمادًا على الإحداثي السيني وحده: أيهما يتطابق مع الإحداثيات x الأعلى مع الأخرى.
وهناك نسخة مبسطة من هذا هو فقط لرسم المستويات بدءا من البلاط الأبعد - وهذا هو ، البلاط [0] [0]
- ثم رسم جميع البلاط في كل صف واحدا تلو الآخر. إذا كان الحرف يشغل مربعًا ، فنرسم البلاط الأرضي أولاً ثم تقديم خصائص البلاط. هذا سوف يعمل بشكل جيد ، لأن الحرف لا يمكن أن يشغل بلاط الجدار.
يجب أن يتم التصنيف في كل مرة يتم فيها أي موضع لتغييرات البلاط. على سبيل المثال ، نحن بحاجة إلى القيام بذلك كلما تحركت الشخصيات. ثم نقوم بتحديث المشهد المعروض ، بعد إجراء فرز عميق ، لإظهار تغييرات العمق.
6. هل تريد الذهاب!
الآن ، ضع معرفتك الجديدة لاستخدام جيد من خلال إنشاء نموذج أول للعمل ، مع عناصر تحكم لوحة المفاتيح وفرز عمق المناسبة واكتشاف التصادم. هنا العرض التجريبي:
انقر لإعطاء التركيز على SWF ، ثم استخدم مفاتيح الأسهم. انقر هنا للحصول على نسخة كاملة الحجم.
قد تجد هذه الفئة مفيدة مفيدة (لقد كتبتها في AS3 ، ولكن يجب أن تكون قادرا على فهمها في أي لغة برمجة أخرى):
1 |
package com.csharks.juwalbose |
2 |
{
|
3 |
import flash.display.Sprite; |
4 |
import flash.geom.Point; |
5 |
|
6 |
public class IsoHelper |
7 |
{
|
8 |
/**
|
9 |
* convert an isometric point to 2D
|
10 |
* */
|
11 |
public static function isoTo2D(pt:Point):Point{ |
12 |
//gx=(2*isoy+isox)/2;
|
13 |
//gy=(2*isoy-isox)/2
|
14 |
var tempPt:Point=new Point(0,0); |
15 |
tempPt.x=(2*pt.y+pt.x)/2; |
16 |
tempPt.y=(2*pt.y-pt.x)/2; |
17 |
return(tempPt); |
18 |
}
|
19 |
/**
|
20 |
* convert a 2d point to isometric
|
21 |
* */
|
22 |
public static function twoDToIso(pt:Point):Point{ |
23 |
//gx=(isox-isoxy;
|
24 |
//gy=(isoy+isox)/2
|
25 |
var tempPt:Point=new Point(0,0); |
26 |
tempPt.x=pt.x-pt.y; |
27 |
tempPt.y=(pt.x+pt.y)/2; |
28 |
return(tempPt); |
29 |
}
|
30 |
|
31 |
/**
|
32 |
* convert a 2d point to specific tile row/column
|
33 |
* */
|
34 |
public static function getTileCoordinates(pt:Point, tileHeight:Number):Point{ |
35 |
var tempPt:Point=new Point(0,0); |
36 |
tempPt.x=Math.floor(pt.x/tileHeight); |
37 |
tempPt.y=Math.floor(pt.y/tileHeight); |
38 |
|
39 |
return(tempPt); |
40 |
}
|
41 |
|
42 |
/**
|
43 |
* convert specific tile row/column to 2d point
|
44 |
* */
|
45 |
public static function get2dFromTileCoordinates(pt:Point, tileHeight:Number):Point{ |
46 |
var tempPt:Point=new Point(0,0); |
47 |
tempPt.x=pt.x*tileHeight; |
48 |
tempPt.y=pt.y*tileHeight; |
49 |
|
50 |
return(tempPt); |
51 |
}
|
52 |
|
53 |
}
|
54 |
}
|
إذا واجهتك مشكلة بالفعل ، فإليك الرمز الكامل من العرض التوضيحي الخاص بي (في نموذج شفرة الفلاش و AS3 الزمني):
1 |
// Uses senocular's KeyObject class
|
2 |
// http://www.senocular.com/flash/actionscript/?file=ActionScript_3.0/com/senocular/utils/KeyObject.as
|
3 |
|
4 |
import flash.display.Sprite; |
5 |
import com.csharks.juwalbose.IsoHelper; |
6 |
import flash.display.MovieClip; |
7 |
import flash.geom.Point; |
8 |
import flash.filters.GlowFilter; |
9 |
import flash.events.Event; |
10 |
import com.senocular.utils.KeyObject; |
11 |
import flash.ui.Keyboard; |
12 |
import flash.display.Bitmap; |
13 |
import flash.display.BitmapData; |
14 |
import flash.geom.Matrix; |
15 |
import flash.geom.Rectangle; |
16 |
|
17 |
var levelData=[[1,1,1,1,1,1], |
18 |
[1,0,0,2,0,1], |
19 |
[1,0,1,0,0,1], |
20 |
[1,0,0,0,0,1], |
21 |
[1,0,0,0,0,1], |
22 |
[1,1,1,1,1,1]]; |
23 |
|
24 |
var tileWidth:uint = 50; |
25 |
var borderOffsetY:uint = 70; |
26 |
var borderOffsetX:uint = 275; |
27 |
|
28 |
var facing:String = "south"; |
29 |
var currentFacing:String = "south"; |
30 |
var hero:MovieClip=new herotile(); |
31 |
hero.clip.gotoAndStop(facing); |
32 |
var heroPointer:Sprite; |
33 |
var key:KeyObject = new KeyObject(stage);//Senocular KeyObject Class |
34 |
var heroHalfSize:uint=20; |
35 |
|
36 |
//the tiles
|
37 |
var grassTile:MovieClip=new TileMc(); |
38 |
grassTile.gotoAndStop(1); |
39 |
var wallTile:MovieClip=new TileMc(); |
40 |
wallTile.gotoAndStop(2); |
41 |
|
42 |
//the canvas
|
43 |
var bg:Bitmap = new Bitmap(new BitmapData(650,450)); |
44 |
addChild(bg); |
45 |
var rect:Rectangle=bg.bitmapData.rect; |
46 |
|
47 |
//to handle depth
|
48 |
var overlayContainer:Sprite=new Sprite(); |
49 |
addChild(overlayContainer); |
50 |
|
51 |
//to handle direction movement
|
52 |
var dX:Number = 0; |
53 |
var dY:Number = 0; |
54 |
var idle:Boolean = true; |
55 |
var speed:uint = 5; |
56 |
var heroCartPos:Point=new Point(); |
57 |
var heroTile:Point=new Point(); |
58 |
|
59 |
//add items to start level, add game loop
|
60 |
function createLevel() |
61 |
{
|
62 |
var tileType:uint; |
63 |
for (var i:uint=0; i<levelData.length; i++) |
64 |
{
|
65 |
for (var j:uint=0; j<levelData[0].length; j++) |
66 |
{
|
67 |
tileType = levelData[i][j]; |
68 |
placeTile(tileType,i,j); |
69 |
if (tileType == 2) |
70 |
{
|
71 |
levelData[i][j] = 0; |
72 |
}
|
73 |
}
|
74 |
}
|
75 |
overlayContainer.addChild(heroPointer); |
76 |
overlayContainer.alpha=0.5; |
77 |
overlayContainer.scaleX=overlayContainer.scaleY=0.5; |
78 |
overlayContainer.y=290; |
79 |
overlayContainer.x=10; |
80 |
depthSort(); |
81 |
addEventListener(Event.ENTER_FRAME,loop); |
82 |
}
|
83 |
|
84 |
//place the tile based on coordinates
|
85 |
function placeTile(id:uint,i:uint,j:uint) |
86 |
{
|
87 |
var pos:Point=new Point(); |
88 |
if (id == 2) |
89 |
{
|
90 |
|
91 |
id = 0; |
92 |
pos.x = j * tileWidth; |
93 |
pos.y = i * tileWidth; |
94 |
pos = IsoHelper.twoDToIso(pos); |
95 |
hero.x = borderOffsetX + pos.x; |
96 |
hero.y = borderOffsetY + pos.y; |
97 |
//overlayContainer.addChild(hero);
|
98 |
heroCartPos.x = j * tileWidth; |
99 |
heroCartPos.y = i * tileWidth; |
100 |
heroTile.x=j; |
101 |
heroTile.y=i; |
102 |
heroPointer=new herodot(); |
103 |
heroPointer.x=heroCartPos.x; |
104 |
heroPointer.y=heroCartPos.y; |
105 |
|
106 |
}
|
107 |
var tile:MovieClip=new cartTile(); |
108 |
tile.gotoAndStop(id+1); |
109 |
tile.x = j * tileWidth; |
110 |
tile.y = i * tileWidth; |
111 |
overlayContainer.addChild(tile); |
112 |
}
|
113 |
|
114 |
//the game loop
|
115 |
function loop(e:Event) |
116 |
{
|
117 |
if (key.isDown(Keyboard.UP)) |
118 |
{
|
119 |
dY = -1; |
120 |
}
|
121 |
else if (key.isDown(Keyboard.DOWN)) |
122 |
{
|
123 |
dY = 1; |
124 |
}
|
125 |
else
|
126 |
{
|
127 |
dY = 0; |
128 |
}
|
129 |
if (key.isDown(Keyboard.RIGHT)) |
130 |
{
|
131 |
dX = 1; |
132 |
if (dY == 0) |
133 |
{
|
134 |
facing = "east"; |
135 |
}
|
136 |
else if (dY==1) |
137 |
{
|
138 |
facing = "southeast"; |
139 |
dX = dY=0.5; |
140 |
}
|
141 |
else
|
142 |
{
|
143 |
facing = "northeast"; |
144 |
dX=0.5; |
145 |
dY=-0.5; |
146 |
}
|
147 |
}
|
148 |
else if (key.isDown(Keyboard.LEFT)) |
149 |
{
|
150 |
dX = -1; |
151 |
if (dY == 0) |
152 |
{
|
153 |
facing = "west"; |
154 |
}
|
155 |
else if (dY==1) |
156 |
{
|
157 |
facing = "southwest"; |
158 |
dY=0.5; |
159 |
dX=-0.5; |
160 |
}
|
161 |
else
|
162 |
{
|
163 |
facing = "northwest"; |
164 |
dX = dY=-0.5; |
165 |
}
|
166 |
}
|
167 |
else
|
168 |
{
|
169 |
dX = 0; |
170 |
if (dY == 0) |
171 |
{
|
172 |
//facing="west";
|
173 |
}
|
174 |
else if (dY==1) |
175 |
{
|
176 |
facing = "south"; |
177 |
}
|
178 |
else
|
179 |
{
|
180 |
facing = "north"; |
181 |
}
|
182 |
}
|
183 |
if (dY == 0 && dX == 0) |
184 |
{
|
185 |
hero.clip.gotoAndStop(facing); |
186 |
idle = true; |
187 |
}
|
188 |
else if (idle||currentFacing!=facing) |
189 |
{
|
190 |
idle = false; |
191 |
currentFacing = facing; |
192 |
hero.clip.gotoAndPlay(facing); |
193 |
}
|
194 |
if (! idle && isWalkable()) |
195 |
{
|
196 |
heroCartPos.x += speed * dX; |
197 |
heroCartPos.y += speed * dY; |
198 |
heroPointer.x=heroCartPos.x; |
199 |
heroPointer.y=heroCartPos.y; |
200 |
|
201 |
var newPos:Point = IsoHelper.twoDToIso(heroCartPos); |
202 |
//collision check
|
203 |
hero.x = borderOffsetX + newPos.x; |
204 |
hero.y = borderOffsetY + newPos.y; |
205 |
heroTile=IsoHelper.getTileCoordinates(heroCartPos,tileWidth); |
206 |
depthSort(); |
207 |
//trace(heroTile);
|
208 |
}
|
209 |
tileTxt.text="Hero is on x: "+heroTile.x +" & y: "+heroTile.y; |
210 |
}
|
211 |
|
212 |
//check for collision tile
|
213 |
function isWalkable():Boolean{ |
214 |
var able:Boolean=true; |
215 |
var newPos:Point =new Point(); |
216 |
newPos.x=heroCartPos.x + (speed * dX); |
217 |
newPos.y=heroCartPos.y + (speed * dY); |
218 |
switch (facing){ |
219 |
case "north": |
220 |
newPos.y-=heroHalfSize; |
221 |
break; |
222 |
case "south": |
223 |
newPos.y+=heroHalfSize; |
224 |
break; |
225 |
case "east": |
226 |
newPos.x+=heroHalfSize; |
227 |
break; |
228 |
case "west": |
229 |
newPos.x-=heroHalfSize; |
230 |
break; |
231 |
case "northeast": |
232 |
newPos.y-=heroHalfSize; |
233 |
newPos.x+=heroHalfSize; |
234 |
break; |
235 |
case "southeast": |
236 |
newPos.y+=heroHalfSize; |
237 |
newPos.x+=heroHalfSize; |
238 |
break; |
239 |
case "northwest": |
240 |
newPos.y-=heroHalfSize; |
241 |
newPos.x-=heroHalfSize; |
242 |
break; |
243 |
case "southwest": |
244 |
newPos.y+=heroHalfSize; |
245 |
newPos.x-=heroHalfSize; |
246 |
break; |
247 |
}
|
248 |
newPos=IsoHelper.getTileCoordinates(newPos,tileWidth); |
249 |
if(levelData[newPos.y][newPos.x]==1){ |
250 |
able=false; |
251 |
}else{ |
252 |
//trace("new",newPos);
|
253 |
}
|
254 |
return able; |
255 |
}
|
256 |
|
257 |
//sort depth & draw to canvas
|
258 |
function depthSort() |
259 |
{
|
260 |
bg.bitmapData.lock(); |
261 |
bg.bitmapData.fillRect(rect,0xffffff); |
262 |
var tileType:uint; |
263 |
var mat:Matrix=new Matrix(); |
264 |
var pos:Point=new Point(); |
265 |
for (var i:uint=0; i<levelData.length; i++) |
266 |
{
|
267 |
for (var j:uint=0; j<levelData[0].length; j++) |
268 |
{
|
269 |
tileType = levelData[i][j]; |
270 |
//placeTile(tileType,i,j);
|
271 |
|
272 |
pos.x = j * tileWidth; |
273 |
pos.y = i * tileWidth; |
274 |
pos = IsoHelper.twoDToIso(pos); |
275 |
mat.tx = borderOffsetX + pos.x; |
276 |
mat.ty = borderOffsetY + pos.y; |
277 |
if(tileType==0){ |
278 |
bg.bitmapData.draw(grassTile,mat); |
279 |
}else{ |
280 |
bg.bitmapData.draw(wallTile,mat); |
281 |
}
|
282 |
if(heroTile.x==j&&heroTile.y==i){ |
283 |
mat.tx=hero.x; |
284 |
mat.ty=hero.y; |
285 |
bg.bitmapData.draw(hero,mat); |
286 |
}
|
287 |
|
288 |
}
|
289 |
}
|
290 |
bg.bitmapData.unlock(); |
291 |
//add character rectangle
|
292 |
}
|
293 |
createLevel(); |
نقاط التسجيل
اعطي اهتماما خاصا لنقاط التسجيل للبلاط والبطل. (يمكن اعتبار نقاط التسجيل كنقاط أصل لكل رمز متحرك معين.) لا تقع هذه الصور بشكل عام داخل الصورة ، بل ستكون الزاوية العلوية اليسرى من المربع المحيط بالعاطفة المتحركة.
سيتعين علينا تغيير رمز الرسم لدينا لإصلاح نقاط التسجيل بشكل صحيح ، خاصة بالنسبة للبطل.
كشف التصادم
نقطة أخرى مثيرة للاهتمام هو أن نلاحظ أن كشف الاصطدام يعتمد على النقطة التي يكون فيها البطل.
لكن بطل وحجم، ولا يمكن أن تكون ممثلة بدقة بفارق نقطة واحدة، لذلك نحن بحاجة لتمثيل البطل على شكل مستطيل والتحقق من اصطدام ضد كل ركن من أركان هذا المستطيل بحيث لا توجد تداخلات مع البلاط وغيرها، وبالتالي لا اثر للعمق .
اختصارات
في العرض التوضيحي ، أعيد رسم المشهد مرة أخرى بكل إطار استنادًا إلى الموقع الجديد للبطل. نجد البلاط الذي يشغله البطل ويرسم البطل فوق سطح الأرض عندما تصل حلقات التقديم إلى تلك القراميد.
ولكن إذا نظرنا عن قرب ، سنجد أنه لا داعي للتكرار عبر كل البلاطات في هذه الحالة. يتم رسم بلاط العشب والبلاط العلوي والسفلي دائمًا قبل رسم البطل ، لذلك لا نحتاج أبدًا لإعادة رسمه على الإطلاق. أيضا ، بلاط الجدار السفلي واليمين دائما أمام البطل ، وبالتالي رسم بعدالبطل.
أساسا ، إذن ، نحن بحاجة فقط إلى إجراء فرز عميق بين الجدار داخل المنطقة النشطة والبطل - وهذا هو ، نوعين من البلاط. يساعدك ملاحظة هذه الأنواع من الاختصارات على توفير الكثير من وقت المعالجة ، والذي قد يكون ضروريًا للأداء.
الاستنتاج
الآن ، يجب أن يكون لديك أساسًا رائعًا لبناء ألعاب isometric خاصة بك: يمكنك جعل العالم والكائنات فيه ، وتمثيل بيانات المستوى في صفيفات ثنائية الأبعاد بسيطة ، وتحويلها بين التنسيق الديكارتية والتنسيقية ، والتعامل مع مفاهيم مثل الفرز المتعمق والرسوم المتحركة الشخصية. استمتع بخلق عوالم متساوية القياس!