عندما كنت طفلاً ، نادرًا ما ذهبت إلى أروقة الأروقة لأنني لم أكن بحاجة إليها حقًا ، لأن لدي ألعاب رائعة لـ C64 في المنزل ... ولكن هناك ثلاث ألعاب أركيد كنت أملك المال لها دائمًا - Donkey Kong و Dragons Lair و Outrun ...... وأحببت حقا Outrun - السرعة والتلال وأشجار النخيل والموسيقى ، حتى في الإصدار الضعيف لـ C64.لذلك قررت أن أحاول كتابة لعبة سباق ثلاثية الأبعاد شبه قديمة بأسلوب Outrun أو Pitstop أو Pole position. أنا لا أخطط لتجميع لعبة كاملة وكاملة ، ولكن يبدو لي أنه سيكون من المثير للاهتمام إعادة فحص الآليات التي أدركت بها هذه الألعاب حيلها. المنحنيات والتلال والعفاريت والشعور بالسرعة ...إذن ، هذا هو "مشروع نهاية الأسبوع" ، الذي استغرق في نهاية المطاف خمسة أو ستة أسابيع في عطلة نهاية الأسبوعالنسخة القابلة للعب تشبه عرضًا تقنيًا أكثر من كونها لعبة حقيقية. في الواقع ، إذا كنت تريد إنشاء سباق زائف ثلاثي الأبعاد حقيقي ، فسيكون هذا هو الحد الأدنى من الأساس الذي تحتاجه لتحويله تدريجيًا إلى لعبة.إنه غير مصقول ، قبيح قليلاً ، ولكنه يعمل بكامل طاقته. سأوضح لك كيفية تنفيذها بنفسك في أربع خطوات بسيطة.يمكنك أيضا اللعبعن الأداء
يعتمد أداء هذه اللعبة بشكل كبير على الجهاز / المتصفح. في المتصفحات الحديثة ، يعمل بشكل جيد ، خاصة في تلك التي تحتوي على تسريع GPU قماش ، ولكن يمكن أن يتسبب برنامج تشغيل الرسومات السيئ في تجميده. في اللعبة ، يمكنك تغيير دقة العرض ومسافة العرض.حول بنية الكود
حدث أن تم تنفيذ المشروع في Javascript (بسبب بساطة النماذج الأولية) ، ولكن ليس المقصود منه إظهار تقنيات أو تقنيات Javascript الموصى بها. في الواقع ، لسهولة الفهم ، يتم تضمين جافا سكريبت لكل مثال مباشرة في صفحة HTML (الرعب!) ؛ والأسوأ من ذلك أنها تستخدم المتغيرات والوظائف العالمية.إذا كنت أقوم بإنشاء لعبة حقيقية ، فستكون الشفرة أكثر تنظيمًا وتبسيطًا ، ولكن نظرًا لأن هذا عرض توضيحي تقني للعبة سباق ، فقد قررت التمسك بـ KISS .الجزء 1. الطرق المستقيمة.
لذا ، كيف نبدأ في إنشاء لعبة سباق ثلاثية الأبعاد زائفة؟حسنًا ، نحن بحاجة- كرر علم المثلثات
- تذكر أساسيات الإسقاط ثلاثي الأبعاد
- إنشاء حلقة لعبة
- تنزيل صور العفريت
- بناء هندسة الطرق
- تجعل الخلفية
- اجعل الطريق
- تجعل السيارة
- تنفيذ دعم لوحة المفاتيح للتحكم في الماكينة
ولكن قبل أن نبدأ ، دعنا نقرأ Lou's Pseudo 3d Page [ ترجمة حبري] - المصدر الوحيد للمعلومات (التي يمكنني العثور عليها) حول كيفية إنشاء لعبة سباق psevdotrohmernuyu.انتهيت من قراءة مقال لو؟ غرامة! سنقوم بإنشاء مجموعة متنوعة من تلاله الواقعية باستخدام تقنية الشرائح ثلاثية الأبعاد. سنقوم بذلك تدريجياً خلال الأجزاء الأربعة التالية. ولكننا سنبدأ الآن ، مع الإصدار v1 ، وننشئ هندسة طريق مستقيم بسيطة للغاية عن طريق عرضه على عنصر لوحة HTML5.يمكن رؤية العرض هنا .
القليل من علم المثلثات
قبل أن نبدأ في التنفيذ ، دعنا نستخدم أساسيات علم المثلثات لتذكر كيفية إبراز نقطة في العالم ثلاثي الأبعاد على شاشة ثنائية الأبعاد.في أبسط الحالات ، إذا لم تلمس المتجهات والمصفوفات ، فسيتم استخدام قانون المثلثات المتشابهة للإسقاط ثلاثي الأبعاد .نحن نستخدم الرموز التالية:- ح = ارتفاع الكاميرا
- د = المسافة من الكاميرا إلى الشاشة
- z = المسافة من الكاميرا إلى السيارة
- y = إحداثيات الشاشة y
ثم يمكننا استخدام قانون المثلثات المتشابهة للحسابy = h * d / z
كما هو موضح في الرسم البياني:يمكنك أيضًا رسم رسم تخطيطي مماثل في عرض علوي بدلاً من عرض جانبي ، واشتقاق معادلة مماثلة لحساب إحداثيات X للشاشة:س = ث * د / ض
حيث w = نصف عرض الطريق (من الكاميرا إلى حافة الطريق).كما ترى ، بالنسبة إلى x ، و y ، فإننا نقيسها بعاملد / ض
نظم الإحداثيات
في شكل رسم تخطيطي ، يبدو جميلًا وبسيطًا ، ولكن عندما تبدأ في الترميز ، يمكنك الحصول على القليل من الارتباك ، لأننا اخترنا أسماء عشوائية ، وليس من الواضح ما حددنا إحداثيات العالم ثلاثي الأبعاد ، وما هي إحداثيات الشاشة ثنائية الأبعاد. نفترض أيضًا أن الكاميرا في مركز أصل العالم ، على الرغم من أنها ستتبع الآلة في الواقع.إذا اقتربت بشكل أكثر رسمية ، فنحن بحاجة إلى تنفيذ:- التحويل من إحداثيات العالم إلى إحداثيات الشاشة
- تنسق كاميرا العرض على مستوى عرض عادي
- تحجيم الإحداثيات المسقطة لإحداثيات الشاشة المادية (في حالتنا ، هذا قماش)
ملاحظة: في النظام ثلاثي الأبعاد الحالي ، يتم تنفيذ مرحلة الدوران بين المرحلتين 1 و 2 ، ولكن نظرًا لأننا سنقوم بمحاكاة المنحنيات ، فإننا لا نحتاج إلى دوران.
تنبؤ
يمكن تمثيل معادلات الإسقاط الرسمية على النحو التالي:- يتم احتساب نقطة معادلات التحويل ( ترجمة ) بالنسبة للغرفة
- معادلات الإسقاط ( المشروع ) هي اختلافات في "قانون المثلثات المتشابهة" الموضحة أعلاه.
- معادلات القياس ( المقياس ) تأخذ في الاعتبار الفرق بين:
- الرياضيات ، حيث 0،0 في المركز والمحور ص لأعلى ، و
- , 0,0 , y :
: 3d- Vector
Matrix
3d-, , WebGL ( )… . Outrun.
ستكون القطعة الأخيرة من اللغز طريقة لحساب د - المسافة من الكاميرا إلى مستوى العرض.فبدلاً من كتابة قيمة d المحددة بشدة ، سيكون من المفيد حسابها من مجال الرؤية الرأسي المطلوب. وبفضل هذا ، سنتمكن من "تكبير / تصغير" الكاميرا إذا لزم الأمر.إذا افترضنا أننا نعرض على مستوى إسقاط عادي ، تكون إحداثياته في النطاق من -1 إلى +1 ، فيمكن حساب d على النحو التالي:د = 1 / تان (فوف / 2)
من خلال تعريف fov كمتغير (من العديد) من المتغيرات ، يمكننا ضبط النطاق لضبط خوارزمية العرض.هيكل كود جافا سكريبت
في بداية المقال ، سبق أن قلت أن الرمز لا يتوافق تمامًا مع المبادئ التوجيهية لكتابة جافا سكريبت - إنه عرض "سريع وقذر" مع متغيرات ووظائف عالمية بسيطة. ومع ذلك ، نظرًا لأنني سأقوم بإنشاء أربعة إصدارات منفصلة (مستقيم ، منحنيات ، تلال وعفاريت) ، فسأقوم بتخزين بعض الطرق التي يمكن إعادة استخدامها داخل common.js
الوحدات التالية:- Dom هو عدد قليل من وظائف مساعد DOM الثانوية.
- Util - المرافق العامة ، وخاصة الوظائف الرياضية المساعدة.
- اللعبة - وظائف دعم الألعاب العامة ، مثل تنزيل الصور وحلقة اللعبة.
- تقديم - وظائف تقديم المساعد على قماش.
سأشرح بالتفصيل الأساليب من common.js
فقط إذا كانت تتعلق باللعبة نفسها ، وليست مجرد وظائف رياضية مساعدة أو DOM. نأمل من الاسم والسياق أن يتضح ما يجب أن تفعله الأساليب.كالعادة ، كود المصدر موجود في الوثائق النهائية.
حلقة لعبة بسيطة
قبل تقديم شيء ما ، نحتاج إلى حلقة لعبة. إذا كنت تقرأ أي من مقالاتي السابقة حول الألعاب ( كرة الطاولة ، اختراق ، تتريس ، الثعابين أو boulderdash )، ثم رأيتم بالفعل أمثلة من بلدي دورة اللعبة المفضلة مع خطوة زمنية محددة .لن أتعمق في التفاصيل ، وأقوم ببساطة بإعادة استخدام جزء من التعليمات البرمجية من الألعاب السابقة لإنشاء حلقة لعبة بخطوة زمنية ثابتة باستخدام requestAnimationFrame .المبدأ هو أن كل من الأمثلة الأربعة الخاصة بي يمكنها الاتصال Game.run(...)
واستخدام نسخها الخاصةupdate
- تحديث عالم اللعبة بخطوة زمنية محددة.render
- تحديث عالم اللعبة عندما يسمح المتصفح.
run: function(options) {
Game.loadImages(options.images, function(images) {
var update = options.update,
render = options.render,
step = options.step,
now = null,
last = Util.timestamp(),
dt = 0,
gdt = 0;
function frame() {
now = Util.timestamp();
dt = Math.min(1, (now - last) / 1000);
gdt = gdt + dt;
while (gdt > step) {
gdt = gdt - step;
update(step);
}
render();
last = now;
requestAnimationFrame(frame);
}
frame();
});
}
مرة أخرى ، هذه طبعة جديدة من الأفكار من ألعابي القماشية السابقة ، لذلك إذا كنت لا تفهم كيف تعمل حلقة اللعبة ، فارجع إلى إحدى المقالات السابقة.الصور والعفاريت
قبل بدء دورة اللعبة ، نقوم بتحميل صفحتين منفصلتين (أوراق الرموز المتحركة):- الخلفية - ثلاث طبقات المنظر للسماء والتلال والأشجار
- العفاريت - العفاريت الآلية (بالإضافة إلى الأشجار واللوحات الإعلانية التي ستتم إضافتها إلى الإصدار النهائي)
تم إنشاء ورقة العفريت باستخدام مهمة صغيرة Rake and Ruby Gem sprite-factory .تقوم هذه المهمة بإنشاء أوراق الرموز المتحركة المدمجة بالإضافة إلى إحداثيات x و y و w و h التي سيتم تخزينها في الثوابت BACKGROUND
و SPRITES
.ملاحظة: لقد قمت بإنشاء الخلفيات باستخدام Inkscape ، ومعظم الرموز المتحركة هي رسومات مأخوذة من إصدار Outrun القديم لـ Genesis وتستخدم كأمثلة تدريبية.
متغيرات اللعبة
بالإضافة إلى صور الخلفيات والعفاريت ، سنحتاج إلى العديد من متغيرات اللعبة ، وهي:var fps = 60;
var step = 1/fps;
var width = 1024;
var height = 768;
var segments = [];
var canvas = Dom.get('canvas');
var ctx = canvas.getContext('2d');
var background = null;
var sprites = null;
var resolution = null;
var roadWidth = 2000;
var segmentLength = 200;
var rumbleLength = 3;
var trackLength = null;
var lanes = 3;
var fieldOfView = 100;
var cameraHeight = 1000;
var cameraDepth = null;
var drawDistance = 300;
var playerX = 0;
var playerZ = null;
var fogDensity = 5;
var position = 0;
var speed = 0;
var maxSpeed = segmentLength/step;
var accel = maxSpeed/5;
var breaking = -maxSpeed;
var decel = -maxSpeed/5;
var offRoadDecel = -maxSpeed/2;
var offRoadLimit = maxSpeed/4;
يمكن تخصيص بعضها باستخدام عناصر تحكم واجهة المستخدم لتغيير القيم الحرجة أثناء تنفيذ البرنامج حتى تتمكن من رؤية كيفية تأثيرها على عرض الطريق. يتم إعادة حساب الآخرين من قيم واجهة المستخدم المخصصة في الطريقة reset()
.ندير فيراري
نقوم بتنفيذ عمليات الربط الرئيسية Game.run
، والتي توفر إدخالًا بسيطًا للوحة المفاتيح يقوم بتعيين أو إعادة تعيين المتغيرات التي تشير إلى الإجراءات الحالية للاعب:Game.run({
...
keys: [
{ keys: [KEY.LEFT, KEY.A], mode: 'down', action: function() { keyLeft = true; } },
{ keys: [KEY.RIGHT, KEY.D], mode: 'down', action: function() { keyRight = true; } },
{ keys: [KEY.UP, KEY.W], mode: 'down', action: function() { keyFaster = true; } },
{ keys: [KEY.DOWN, KEY.S], mode: 'down', action: function() { keySlower = true; } },
{ keys: [KEY.LEFT, KEY.A], mode: 'up', action: function() { keyLeft = false; } },
{ keys: [KEY.RIGHT, KEY.D], mode: 'up', action: function() { keyRight = false; } },
{ keys: [KEY.UP, KEY.W], mode: 'up', action: function() { keyFaster = false; } },
{ keys: [KEY.DOWN, KEY.S], mode: 'up', action: function() { keySlower = false; } }
],
...
}
يتم التحكم في حالة اللاعب من خلال المتغيرات التالية:- السرعة - السرعة الحالية.
- الموضع - الموضع Z الحالي على المسار. لاحظ أن هذا هو وضع الكاميرا ، وليس فيراري.
- playerX - وضع اللاعب الحالي على X على الطريق. تطبيع في النطاق من -1 إلى +1 ، حتى لا تعتمد على القيمة الفعلية
roadWidth
.
يتم تعيين هذه المتغيرات داخل الطريقة update
، والتي تنفذ الإجراءات التالية:- التحديثات
position
بناء على التيار speed
. - التحديثات
playerX
عند الضغط على المفتاح الأيسر أو الأيمن. - يزيد
speed
إذا تم الضغط على المفتاح لأعلى. - ينخفض
speed
إذا تم الضغط على مفتاح لأسفل. - يقلل
speed
إذا لم يتم الضغط على المفتاحين لأعلى ولأسفل. - يقلل
speed
إذا كان playerX
يقع على حافة الطريق وعلى العشب.
في حالة الطرق المباشرة ، تكون الطريقة update
واضحة وبسيطة تمامًا:function update(dt) {
position = Util.increase(position, dt * speed, trackLength);
var dx = dt * 2 * (speed/maxSpeed);
if (keyLeft)
playerX = playerX - dx;
else if (keyRight)
playerX = playerX + dx;
if (keyFaster)
speed = Util.accelerate(speed, accel, dt);
else if (keySlower)
speed = Util.accelerate(speed, breaking, dt);
else
speed = Util.accelerate(speed, decel, dt);
if (((playerX < -1) || (playerX > 1)) && (speed > offRoadLimit))
speed = Util.accelerate(speed, offRoadDecel, dt);
playerX = Util.limit(playerX, -2, 2);
speed = Util.limit(speed, 0, maxSpeed);
}
لا تقلق ، سيصبح الأمر أكثر صعوبة عندما نضيف العفاريت والتعرف على التصادم في النسخة النهائية.هندسة الطرق
قبل أن نتمكن من تقديم عالم اللعبة ، نحتاج إلى إنشاء مجموعة من segments
هذه الطريقة resetRoad()
.سيتم عرض كل جزء من هذه الأجزاء من الطريق في النهاية من إحداثيات العالم بحيث يتحول إلى مضلع ثنائي الأبعاد في إحداثيات الشاشة. لكل مقطع ، نقوم بتخزين نقطتين ، p1 هو مركز الحافة الأقرب إلى الكاميرا ، و p2 هو مركز الحافة الأبعد عن الكاميرا.بالمعنى الدقيق للكلمة ، p2 من كل جزء مطابق لـ p1 من المقطع السابق ، ولكن يبدو لي أنه من الأسهل تخزينها كنقاط منفصلة وتحويل كل جزء على حدة.نحن ننفصل عن rumbleLength
بعضنا لأنه يمكننا الحصول على منحنيات وتلال تفصيلية جميلة ، ولكن في نفس الوقت خطوط أفقية. إذا كان لكل مقطع لاحق لون مختلف ، فسيؤدي ذلك إلى إنشاء تأثير ستروب سيء. لذلك ، نريد أن يكون لدينا العديد من الأجزاء الصغيرة ، ولكن نجمعها معًا لتشكيل خطوط أفقية منفصلة.function resetRoad() {
segments = [];
for(var n = 0 ; n < 500 ; n++) {
segments.push({
index: n,
p1: { world: { z: n *segmentLength }, camera: {}, screen: {} },
p2: { world: { z: (n+1)*segmentLength }, camera: {}, screen: {} },
color: Math.floor(n/rumbleLength)%2 ? COLORS.DARK : COLORS.LIGHT
});
}
trackLength = segments.length * segmentLength;
}
نقوم بتهيئة p1 و p2 فقط بإحداثيات العالم z ، لأننا لا نحتاج سوى الطرق المستقيمة. الإحداثيات ص ستكون دائمًا 0 ، وستعتمد الإحداثيات س دائمًا على القيمة المتدرجة +/- roadWidth
. لاحقًا ، عندما نضيف منحنيات وتلال ، سيتغير هذا الجزء.سنقوم أيضًا بتعيين كائنات فارغة لتخزين تمثيلات هذه النقاط في الكاميرا وعلى الشاشة حتى لا يتم إنشاء مجموعة من الكائنات المؤقتة في كل منها render
. لتقليل جمع القمامة ، يجب علينا تجنب تخصيص الكائنات داخل حلقة اللعبة.عندما تصل السيارة إلى نهاية الطريق ، نعود ببساطة إلى بداية الدورة. لتبسيط ذلك ، سننشئ طريقة للعثور على جزء لأي قيمة من Z ، حتى إذا تجاوزت طول الطريق:function findSegment(z) {
return segments[Math.floor(z/segmentLength) % segments.length];
}
تقديم الخلفية
render()
تبدأ الطريقة بجعل صورة الخلفية. في الأجزاء التالية ، حيث سنضيف منحنيات وتلال ، سنحتاج إلى الخلفية لأداء تمرير المنظر ، لذلك سنبدأ الآن في التحرك في هذا الاتجاه ، مما يجعل الخلفية ثلاث طبقات منفصلة:function render() {
ctx.clearRect(0, 0, width, height);
Render.background(ctx, background, width, height, BACKGROUND.SKY);
Render.background(ctx, background, width, height, BACKGROUND.HILLS);
Render.background(ctx, background, width, height, BACKGROUND.TREES);
...
تقديم الطريق
بعد ذلك ، تتكرر وظيفة التجسيد من خلال جميع الأجزاء والمشاريع p1 و p2 لكل مقطع من إحداثيات العالم إلى إحداثيات الشاشة ، وتقليم المقطع إذا لزم الأمر ، وعرضه بطريقة أخرى: var baseSegment = findSegment(position);
var maxy = height;
var n, segment;
for(n = 0 ; n < drawDistance ; n++) {
segment = segments[(baseSegment.index + n) % segments.length];
Util.project(segment.p1, (playerX * roadWidth), cameraHeight, position, cameraDepth, width, height, roadWidth);
Util.project(segment.p2, (playerX * roadWidth), cameraHeight, position, cameraDepth, width, height, roadWidth);
if ((segment.p1.camera.z <= cameraDepth) ||
(segment.p2.screen.y >= maxy))
continue;
Render.segment(ctx, width, lanes,
segment.p1.screen.x,
segment.p1.screen.y,
segment.p1.screen.w,
segment.p2.screen.x,
segment.p2.screen.y,
segment.p2.screen.w,
segment.color);
maxy = segment.p2.screen.y;
}
أعلاه ، رأينا بالفعل الحسابات اللازمة لإسقاط نقطة ؛ تجمع نسخة جافا سكريبت بين التحويل والإسقاط والقياس في طريقة واحدة:project: function(p, cameraX, cameraY, cameraZ, cameraDepth, width, height, roadWidth) {
p.camera.x = (p.world.x || 0) - cameraX;
p.camera.y = (p.world.y || 0) - cameraY;
p.camera.z = (p.world.z || 0) - cameraZ;
p.screen.scale = cameraDepth/p.camera.z;
p.screen.x = Math.round((width/2) + (p.screen.scale * p.camera.x * width/2));
p.screen.y = Math.round((height/2) - (p.screen.scale * p.camera.y * height/2));
p.screen.w = Math.round( (p.screen.scale * roadWidth * width/2));
}
بالإضافة إلى حساب الشاشة العاشر و ذ لكل نقطة P1 و P2، ونحن نستخدم نفس الحسابات الإسقاط لحساب العرض المتوقع ( ث ) من هذا الجزء.من خلال إحداثيات الشاشة x و y للنقطتين p1 و p2 ، بالإضافة إلى العرض المتوقع للطريق w ، يمكننا بسهولة حساب بمساعدة وظيفة مساعدة Render.segment
جميع المضلعات اللازمة لعرض العشب والطريق والخطوط الأفقية وخطوط التقسيم ، باستخدام الوظيفة الإضافية العامة Render.polygon
(انظر . common.js
) .تقديم سيارة
أخيرًا ، آخر شيء تحتاجه الطريقة render
هو عرض فيراري: Render.player(ctx, width, height, resolution, roadWidth, sprites, speed/maxSpeed,
cameraDepth/playerZ,
width/2,
height);
تسمى هذه الطريقة player
، وليس car
، لأنه في النسخة النهائية من اللعبة سيكون هناك سيارات أخرى على الطريق ، ونريد فصل فيراري اللاعب عن السيارات الأخرى. تستخدموظيفة المساعد Render.player
طريقة قماشية تسمى drawImage
عرض العفريت ، بعد أن قامت بقياسها مسبقًا باستخدام نفس مقياس الإسقاط الذي تم استخدامه من قبل:د / ض
حيث z في هذه الحالة هي المسافة النسبية من الآلة إلى الكاميرا ، المخزنة في playerZ المتغير .بالإضافة إلى ذلك ، فإن الوظيفة "تهز" السيارة قليلاً بسرعات عالية ، مما يضيف القليل من العشوائية إلى معادلة التحجيم ، اعتمادًا على السرعة / maxSpeed .وهذا ما حصلنا عليه:استنتاج
لقد قمنا بقدر كبير من العمل فقط لإنشاء نظام بطرق مستقيمة. أضفنا- دوم وحدة مساعد عام دوم
- استخدام وحدة الرياضيات العامة
- تقديم وحدة مساعد قماش عامة ...
- ... بما في ذلك
Render.segment
، Render.polygon
وRender.sprite
- دورة لعبة ثابتة الملعب
- تنزيل الصورة
- معالج لوحة المفاتيح
- المنظر الخلفية
- ورقة العفريت مع السيارات والأشجار واللوحات الإعلانية
- الهندسة البدائية للطريق
- طريقة
update()
التحكم في الجهاز - طريقة
render()
لتقديم الخلفية والطريق وسيارة اللاعب - علامة HTML5
<audio>
مع موسيقى السباق (مكافأة مخفية!)
... والتي أعطتنا أساسًا جيدًا لمزيد من التطوير.الجزء 2. المنحنيات.
في هذا الجزء ، سنشرح بمزيد من التفصيل كيف تعمل المنحنيات.في الجزء السابق ، قمنا بتجميع هندسة الطريق على شكل مجموعة من الأجزاء ، كل منها يحتوي على إحداثيات عالمية يتم تحويلها بالنسبة للكاميرا ثم يتم عرضها على الشاشة.نحتاج فقط إلى تنسيق العالم z لكل نقطة ، لأنه على الطرق المستقيمة ، كان كل من x و y يساوي صفر.إذا أردنا إنشاء نظام ثلاثي الأبعاد يعمل بكامل طاقته ، فيمكننا تنفيذ المنحنيات بحساب خطوط x و z للمضلعات الموضحة أعلاه. ومع ذلك ، سيكون هذا النوع من الهندسة صعبًا إلى حد ما ، ولهذا سيكون من الضروري إضافة مرحلة التدوير ثلاثي الأبعاد إلى معادلات الإسقاط ...... إذا ذهبنا بهذه الطريقة ، فسيكون من الأفضل استخدام WebGL أو نظائرها ، ولكن هذا المشروع ليس لديه مهام أخرى لمشروعنا. نريد فقط استخدام حيل زائفة ثلاثية الأبعاد في المدرسة القديمة لمحاكاة المنحنيات.لذلك ، ربما ستفاجأ عندما تعلم أننا لن نحسب الإحداثيات س لأجزاء الطريق على الإطلاق ...بدلاً من ذلك ، سنستخدم نصيحة لو :"لمنح الطريق ، ما عليك سوى تغيير موضع خط المنتصف لشكل المنحنى ... بدءًا من الجزء السفلي من الشاشة ، يزداد مقدار تغير مركز الطريق إلى اليسار أو اليمين تدريجيًا . "
في حالتنا ، الخط المركزي هو القيمة التي يتم cameraX
تمريرها إلى حسابات الإسقاط. هذا يعني أنه عندما نؤدي render()
كل جزء من الطريق ، يمكنك محاكاة المنحنيات عن طريق تغيير القيمة cameraX
عن طريق زيادة القيمة تدريجيًا.لمعرفة مقدار التغيير ، نحتاج إلى تخزين قيمة في كل جزء curve
. تشير هذه القيمة إلى مقدار ما يجب إزاحة المقطع من الخط المركزي للكاميرا. هي ستكون:- سلبي لمنحنيات الاستدارة لليسار
- إيجابي للمنحنيات التي تتحول إلى اليمين
- أقل لمنحنيات ناعمة
- المزيد للمنحنيات الحادة
يتم اختيار القيم نفسها بشكل تعسفي تمامًا ؛ من خلال التجربة والخطأ ، يمكننا أن نجد قيمًا جيدة تبدو عندها المنحنيات "صحيحة":var ROAD = {
LENGTH: { NONE: 0, SHORT: 25, MEDIUM: 50, LONG: 100 },
CURVE: { NONE: 0, EASY: 2, MEDIUM: 4, HARD: 6 }
};
بالإضافة إلى اختيار القيم الجيدة للمنحنيات ، نحتاج إلى تجنب أي ثغرات في التحولات عندما يتحول الخط إلى منحنى (أو العكس). يمكن تحقيق ذلك عن طريق التليين عند الدخول والخروج من المنحنيات. سنقوم بذلك عن طريق زيادة (أو تقليل) قيمة curve
كل جزء تدريجيًا باستخدام وظائف التنعيم التقليدية حتى تصل إلى القيمة المطلوبة:easeIn: function(a,b,percent) { return a + (b-a)*Math.pow(percent,2); },
easeOut: function(a,b,percent) { return a + (b-a)*(1-Math.pow(1-percent,2)); },
easeInOut: function(a,b,percent) { return a + (b-a)*((-Math.cos(percent*Math.PI)/2) + 0.5); },
أي ، الآن ، مع مراعاة وظيفة إضافة جزء واحد إلى الهندسة ...function addSegment(curve) {
var n = segments.length;
segments.push({
index: n,
p1: { world: { z: n *segmentLength }, camera: {}, screen: {} },
p2: { world: { z: (n+1)*segmentLength }, camera: {}, screen: {} },
curve: curve,
color: Math.floor(n/rumbleLength)%2 ? COLORS.DARK : COLORS.LIGHT
});
}
يمكننا إنشاء طريقة للدخول السلس والعثور والخروج السلس من طريق منحني:function addRoad(enter, hold, leave, curve) {
var n;
for(n = 0 ; n < enter ; n++)
addSegment(Util.easeIn(0, curve, n/enter));
for(n = 0 ; n < hold ; n++)
addSegment(curve);
for(n = 0 ; n < leave ; n++)
addSegment(Util.easeInOut(curve, 0, n/leave));
}
... وفي الأعلى ، يمكنك فرض هندسة إضافية ، على سبيل المثال ، منحنيات على شكل حرف S:function addSCurves() {
addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, -ROAD.CURVE.EASY);
addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.CURVE.MEDIUM);
addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.CURVE.EASY);
addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, -ROAD.CURVE.EASY);
addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, -ROAD.CURVE.MEDIUM);
}
التغييرات على طريقة التحديث ()
التغييرات الوحيدة التي يجب إجراؤها على الطريقة update()
هي تطبيق نوع من قوة الطرد المركزي عندما تتحرك الآلة على طول المنحنى.قمنا بتعيين عامل تعسفي يمكن تعديله وفقًا لتفضيلاتنا.var centrifugal = 0.3;
وبعد ذلك سنقوم فقط بتحديث الموقف playerX
بناءً على سرعته الحالية وقيمة المنحنى ومضاعف قوة الطرد المركزي:playerX = playerX - (dx * speedPercent * playerSegment.curve * centrifugal);
تقديم المنحنى
قلنا أعلاه أنه يمكنك تقديم منحنيات محاكاة من خلال تغيير القيمة cameraX
المستخدمة في حسابات الإسقاط أثناء تنفيذ render()
كل جزء من الطريق.للقيام بذلك ، سنقوم بتخزين dx متغير محرك الأقراص ، وزيادة لكل مقطع بقيمة curve
، وكذلك المتغير x ، والذي سيتم استخدامه كإزاحة للقيمة cameraX
المستخدمة في حسابات الإسقاط.لتنفيذ المنحنيات ، نحتاج إلى ما يلي:- انقل الإسقاط p1 لكل قطعة بمقدار x
- انقل الإسقاط p2 لكل قطعة بمقدار x + dx
- زيادة x للمقطع التالي بمقدار dx
أخيرًا ، من أجل تجنب التحولات الممزقة عند عبور حدود المقاطع ، يجب أن نجعل dx مُهيأًا بالقيمة المحرفة لمنحنى المقاطع الأساسية الحالية.قم بتغيير الطريقة render()
كما يلي:var baseSegment = findSegment(position);
var basePercent = Util.percentRemaining(position, segmentLength);
var dx = - (baseSegment.curve * basePercent);
var x = 0;
for(n = 0 ; n < drawDistance ; n++) {
...
Util.project(segment.p1, (playerX * roadWidth) - x, cameraHeight, position - (segment.looped ? trackLength : 0), cameraDepth, width, height, roadWidth);
Util.project(segment.p2, (playerX * roadWidth) - x - dx, cameraHeight, position - (segment.looped ? trackLength : 0), cameraDepth, width, height, roadWidth);
x = x + dx;
dx = dx + segment.curve;
...
}
المنظر الخلفية التمرير
أخيرًا ، نحتاج إلى تمرير طبقات خلفية المنظر ، وتخزين الإزاحة لكل طبقة ...var skySpeed = 0.001;
var hillSpeed = 0.002;
var treeSpeed = 0.003;
var skyOffset = 0;
var hillOffset = 0;
var treeOffset = 0;
... وزيادته خلال الوقت update()
اعتمادًا على قيمة المنحنى لقطاع المشغل الحالي وسرعته ...skyOffset = Util.increase(skyOffset, skySpeed * playerSegment.curve * speedPercent, 1);
hillOffset = Util.increase(hillOffset, hillSpeed * playerSegment.curve * speedPercent, 1);
treeOffset = Util.increase(treeOffset, treeSpeed * playerSegment.curve * speedPercent, 1);
... ثم استخدم هذا الإزاحة عند عمل render()
طبقات الخلفية.Render.background(ctx, background, width, height, BACKGROUND.SKY, skyOffset);
Render.background(ctx, background, width, height, BACKGROUND.HILLS, hillOffset);
Render.background(ctx, background, width, height, BACKGROUND.TREES, treeOffset);
استنتاج
إذن ، هنا نحصل على منحنيات زائفة ثلاثية الأبعاد:الجزء الرئيسي من الرمز الذي أضفناه هو بناء هندسة الطريق بالقيمة المقابلة curve
. تحقيق ذلك ، إضافة قوة الطرد المركزي خلال الوقت update()
أسهل بكثير.يتم تقديم المنحنى في بضعة أسطر من التعليمات البرمجية ، ولكن قد يكون من الصعب فهم (ووصف) ما يحدث بالضبط هنا. هناك العديد من الطرق لمحاكاة المنحنيات ومن السهل جدًا التجول عند تنفيذها في طريق مسدود. من الأسهل أن تنجرف في مهمة خارجية وتحاول القيام بكل شيء "بشكل صحيح" ؛ قبل أن تدرك ذلك ، ستبدأ في إنشاء نظام ثلاثي الأبعاد يعمل بكامل طاقته مع المصفوفات والتناوب والهندسة ثلاثية الأبعاد الحقيقية ... والتي ، كما قلت ، ليست مهمتنا.عندما كتبت هذا المقال ، كنت على يقين من أنه كانت هناك بالتأكيد مشاكل في تنفيذ المنحنيات. في محاولة لتصور الخوارزمية ، لم أفهم لماذا أحتاج إلى قيمتين لمحركات الأقراص dx و x بدلاً من واحدة ... وإذا لم أتمكن من شرح شيء ما تمامًا ، فقد حدث خطأ ما في مكان ما... ولكن وقت عمل المشروع "قيد التشغيل نهاية الأسبوع تقريبًا "، وبصراحة ، تبدو لي المنحنيات جميلة جدًا ، وفي النهاية ، هذا هو الأهم.