إنشاء لعبة سباق ثلاثية الأبعاد


عندما كنت طفلاً ، نادرًا ما ذهبت إلى أروقة الأروقة لأنني لم أكن بحاجة إليها حقًا ، لأن لدي ألعاب رائعة لـ 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. تنسق كاميرا العرض على مستوى عرض عادي
  3. تحجيم الإحداثيات المسقطة لإحداثيات الشاشة المادية (في حالتنا ، هذا قماش)


ملاحظة: في النظام ثلاثي الأبعاد الحالي ، يتم تنفيذ مرحلة الدوران بين المرحلتين 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,    // method to update game logic is provided by caller
        render = options.render,    // method to render the game is provided by caller
        step   = options.step,      // fixed frame step (1/fps) is specified by caller
        now    = null,
        last   = Util.timestamp(),
        dt     = 0,
        gdt    = 0;

    function frame() {
      now = Util.timestamp();
      dt  = Math.min(1, (now - last) / 1000); // using requestAnimationFrame have to be able to handle large delta's caused when it 'hibernates' in a background or non-visible tab
      gdt = gdt + dt;
      while (gdt > step) {
        gdt = gdt - step;
        update(step);
      }
      render();
      last = now;
      requestAnimationFrame(frame);
    }
    frame(); // lets get this party started
  });
}

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

الصور والعفاريت


قبل بدء دورة اللعبة ، نقوم بتحميل صفحتين منفصلتين (أوراق الرموز المتحركة):

  • الخلفية - ثلاث طبقات المنظر للسماء والتلال والأشجار
  • العفاريت - العفاريت الآلية (بالإضافة إلى الأشجار واللوحات الإعلانية التي ستتم إضافتها إلى الإصدار النهائي)


تم إنشاء ورقة العفريت باستخدام مهمة صغيرة Rake and Ruby Gem sprite-factory .

تقوم هذه المهمة بإنشاء أوراق الرموز المتحركة المدمجة بالإضافة إلى إحداثيات x و y و w و h التي سيتم تخزينها في الثوابت BACKGROUNDو SPRITES.

ملاحظة: لقد قمت بإنشاء الخلفيات باستخدام Inkscape ، ومعظم الرموز المتحركة هي رسومات مأخوذة من إصدار Outrun القديم لـ Genesis وتستخدم كأمثلة تدريبية.

متغيرات اللعبة


بالإضافة إلى صور الخلفيات والعفاريت ، سنحتاج إلى العديد من متغيرات اللعبة ، وهي:

var fps           = 60;                      // how many 'update' frames per second
var step          = 1/fps;                   // how long is each frame (in seconds)
var width         = 1024;                    // logical canvas width
var height        = 768;                     // logical canvas height
var segments      = [];                      // array of road segments
var canvas        = Dom.get('canvas');       // our canvas...
var ctx           = canvas.getContext('2d'); // ...and its drawing context
var background    = null;                    // our background image (loaded below)
var sprites       = null;                    // our spritesheet (loaded below)
var resolution    = null;                    // scaling factor to provide resolution independence (computed)
var roadWidth     = 2000;                    // actually half the roads width, easier math if the road spans from -roadWidth to +roadWidth
var segmentLength = 200;                     // length of a single segment
var rumbleLength  = 3;                       // number of segments per red/white rumble strip
var trackLength   = null;                    // z length of entire track (computed)
var lanes         = 3;                       // number of lanes
var fieldOfView   = 100;                     // angle (degrees) for field of view
var cameraHeight  = 1000;                    // z height of camera
var cameraDepth   = null;                    // z distance camera is from screen (computed)
var drawDistance  = 300;                     // number of segments to draw
var playerX       = 0;                       // player x offset from center of road (-1 to 1 to stay independent of roadWidth)
var playerZ       = null;                    // player relative z distance from camera (computed)
var fogDensity    = 5;                       // exponential fog density
var position      = 0;                       // current camera Z position (add playerZ to get player's absolute Z position)
var speed         = 0;                       // current speed
var maxSpeed      = segmentLength/step;      // top speed (ensure we can't move more than 1 segment in a single frame to make collision detection easier)
var accel         =  maxSpeed/5;             // acceleration rate - tuned until it 'felt' right
var breaking      = -maxSpeed;               // deceleration rate when braking
var decel         = -maxSpeed/5;             // 'natural' deceleration rate when neither accelerating, nor braking
var offRoadDecel  = -maxSpeed/2;             // off road deceleration is somewhere in between
var offRoadLimit  =  maxSpeed/4;             // limit when off road deceleration no longer applies (e.g. you can always go at least this speed even when off road)

يمكن تخصيص بعضها باستخدام عناصر تحكم واجهة المستخدم لتغيير القيم الحرجة أثناء تنفيذ البرنامج حتى تتمكن من رؤية كيفية تأثيرها على عرض الطريق. يتم إعادة حساب الآخرين من قيم واجهة المستخدم المخصصة في الطريقة 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); // at top speed, should be able to cross from left to right (-1 to 1) in 1 second

  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);     // dont ever let player go too far out of bounds
  speed   = Util.limit(speed, 0, maxSpeed); // or exceed maxSpeed

}

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

هندسة الطرق


قبل أن نتمكن من تقديم عالم اللعبة ، نحتاج إلى إنشاء مجموعة من segmentsهذه الطريقة resetRoad().

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


بالمعنى الدقيق للكلمة ، p2 من كل جزء مطابق لـ p1 من المقطع السابق ، ولكن يبدو لي أنه من الأسهل تخزينها كنقاط منفصلة وتحويل كل جزء على حدة.

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

function resetRoad() {
  segments = [];
  for(var n = 0 ; n < 500 ; n++) { // arbitrary road length
    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) || // behind us
        (segment.p2.screen.y >= maxy))          // clip by (already rendered) segment
      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 }, // num segments
  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;   // centrifugal force multiplier when going around curves

وبعد ذلك سنقوم فقط بتحديث الموقف 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; // background sky layer scroll speed when going around curve (or up hill)
var hillSpeed   = 0.002; // background hill layer scroll speed when going around curve (or up hill)
var treeSpeed   = 0.003; // background tree layer scroll speed when going around curve (or up hill)
var skyOffset   = 0;     // current sky scroll offset
var hillOffset  = 0;     // current hill scroll offset
var treeOffset  = 0;     // current tree scroll offset

... وزيادته خلال الوقت 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 بدلاً من واحدة ... وإذا لم أتمكن من شرح شيء ما تمامًا ، فقد حدث خطأ ما في مكان ما

... ولكن وقت عمل المشروع "قيد التشغيل نهاية الأسبوع تقريبًا "، وبصراحة ، تبدو لي المنحنيات جميلة جدًا ، وفي النهاية ، هذا هو الأهم.

All Articles