إنشاء لعبة سباق Pseudo-3D: تنفيذ التلال وإنهاء اللعبة

الجزء 3. التلال



في الجزء السابق ، ابتكرنا لعبة سباق ثلاثية الأبعاد بسيطة ، مع إدراك الطرق المستقيمة والمنحنيات فيها.

هذه المرة سنعتني بالتلال. لحسن الحظ ، سيكون أسهل بكثير من إنشاء طرق منحنية.

في الجزء الأول ، استخدمنا قانون المثلثات المتشابهة لإنشاء إسقاط منظور ثلاثي الأبعاد:


... مما أدى بنا إلى الحصول على المعادلات الخاصة بإحداثيات إحداثيات العالم ثلاثي الأبعاد في إحداثيات الشاشة ثنائية الأبعاد.


... ولكن منذ ذلك الحين عملنا فقط مع الطرق المستقيمة ، وكانت إحداثيات العالم بحاجة إلى المكون z فقط ، لأن كل من x و y كانا يساوي صفر.

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


نعم ، هذا يكفي للحصول على التلال. ما عليك سوى إضافة المكوِّن y إلى إحداثيات العالم لكل جزء من الطريق .

التغييرات في هندسة الطرق


سنقوم بتعديل الطريقة الحالية addSegmentحتى تتمكن الوظيفة التي تطلق عليها من تمرير p2.world.y و p1.world.y تتوافق مع p2.world.y من المقطع السابق:

function addSegment(curve, y) {
  var n = segments.length;
  segments.push({
     index: n,
        p1: { world: { y: lastY(), z:  n   *segmentLength }, camera: {}, screen: {} },
        p2: { world: { y: y,       z: (n+1)*segmentLength }, camera: {}, screen: {} },
     curve: curve,
     color: Math.floor(n/rumbleLength)%2 ? COLORS.DARK : COLORS.LIGHT
  });
}

function lastY() {
  return (segments.length == 0) ? 0 : segments[segments.length-1].p2.world.y;
}

أضف ثوابت للإشارة إلى التلال المنخفضة ( LOW) والمتوسطة ( MEDIUM) والعالية ( HIGH):

var ROAD = {
  LENGTH: { NONE: 0, SHORT:  25, MEDIUM:  50, LONG:  100 },
  HILL:   { NONE: 0, LOW:    20, MEDIUM:  40, HIGH:   60 },
  CURVE:  { NONE: 0, EASY:    2, MEDIUM:   4, HARD:    6 }
};

قم بتغيير الطريقة الحالية addRoad()بحيث تتلقى الوسيطة y ، التي سيتم استخدامها مع وظائف النعومة للصعود التدريجي والنزول من التل:

function addRoad(enter, hold, leave, curve, y) {
  var startY   = lastY();
  var endY     = startY + (Util.toInt(y, 0) * segmentLength);
  var n, total = enter + hold + leave;
  for(n = 0 ; n < enter ; n++)
    addSegment(Util.easeIn(0, curve, n/enter), Util.easeInOut(startY, endY, n/total));
  for(n = 0 ; n < hold  ; n++)
    addSegment(curve, Util.easeInOut(startY, endY, (enter+n)/total));
  for(n = 0 ; n < leave ; n++)
    addSegment(Util.easeInOut(curve, 0, n/leave), Util.easeInOut(startY, endY, (enter+hold+n)/total));
}

علاوة على ذلك ، على غرار ما فعلناه في الجزء الثاني addSCurves()، يمكننا فرض أي طرق نحتاجها لبناء الهندسة ، على سبيل المثال:

function addLowRollingHills(num, height) {
  num    = num    || ROAD.LENGTH.SHORT;
  height = height || ROAD.HILL.LOW;
  addRoad(num, num, num,  0,  height/2);
  addRoad(num, num, num,  0, -height);
  addRoad(num, num, num,  0,  height);
  addRoad(num, num, num,  0,  0);
  addRoad(num, num, num,  0,  height/2);
  addRoad(num, num, num,  0,  0);
}

التغييرات على طريقة التحديث


في لعبة arcade التي نقوم بإنشائها ، لن نحاول محاكاة الواقع ، لذلك لا تؤثر التلال على اللاعب أو عالم اللعبة بأي شكل من الأشكال ، مما يعني أن update()التغييرات ليست مطلوبة في الطريقة .

تقديم التل


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

المنظر الخلفية التمرير


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

ستكون أبسط آلية هي الإزاحة الخلفية المعتادة بالنسبة إلى الموضع playerY(الذي يجب أن يكون محرفًا من المواضع العالمية ص لجزء اللاعب الحالي).

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

استنتاج


هذا كل شيء ، الآن يمكننا استكمال المنحنيات المزيفة بتلال حقيقية:


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

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

الجزء 4. نسخة جاهزة



في هذا الجزء سوف نضيف:

  • اللوحات والأشجار
  • سيارات أخرى
  • التعرف على التصادم
  • ذكاء اصطناعي بدائي للسيارات
  • واجهة مع توقيت اللفة وسجل اللفة

... وهذا سيزودنا بمستوى كافٍ من التفاعل حتى نطلق على مشروعنا في النهاية "لعبة".

ملاحظة حول بنية الكود


, /, Javascript.

. () , ...

… , , , , .



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

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

تولد هذه المهمة أوراقًا مجمعة من ملفات صور منفصلة ، وتحسب أيضًا الإحداثيات x ، y ، w ، h ، التي سيتم تخزينها في ثابت SPRITES:

var SPRITES = {
  PALM_TREE:   { x:    5, y:    5, w:  215, h:  540 },
  BILLBOARD08: { x:  230, y:    5, w:  385, h:  265 },

  // ... etc

  CAR04:       { x: 1383, y:  894, w:   80, h:   57 },
  CAR01:       { x: 1205, y: 1018, w:   80, h:   56 },
};

إضافة اللوحات والأشجار


أضف إلى كل جزء من الطريق مصفوفة تحتوي على أجزاء من الأشياء على طول حواف الطريق.

يتكون كل نقش متحرك من المجموعة sourceالمأخوذة من المجموعة SPRITES، مع إزاحة أفقية offset، يتم تطبيعها بحيث تشير -1 إلى الحافة اليسرى للطريق ، ويعني +1 الحافة اليمنى ، مما يسمح لنا بعدم الاعتماد على القيمة roadWidth.

يتم وضع بعض العفاريت عمدًا ، وبعضها عشوائي.

function addSegment() {
  segments.push({
    ...
    sprites: [],
    ...
  });
}

function addSprite(n, sprite, offset) {
  segments[n].sprites.push({ source: sprite, offset: offset });
}

function resetSprites() {

  addSprite(20,  SPRITES.BILLBOARD07, -1);
  addSprite(40,  SPRITES.BILLBOARD06, -1);
  addSprite(60,  SPRITES.BILLBOARD08, -1);
  addSprite(80,  SPRITES.BILLBOARD09, -1);
  addSprite(100, SPRITES.BILLBOARD01, -1);
  addSprite(120, SPRITES.BILLBOARD02, -1);
  addSprite(140, SPRITES.BILLBOARD03, -1);
  addSprite(160, SPRITES.BILLBOARD04, -1);
  addSprite(180, SPRITES.BILLBOARD05, -1);

  addSprite(240, SPRITES.BILLBOARD07, -1.2);
  addSprite(240, SPRITES.BILLBOARD06,  1.2);

  
  for(n = 250 ; n < 1000 ; n += 5) {
    addSprite(n, SPRITES.COLUMN, 1.1);
    addSprite(n + Util.randomInt(0,5), SPRITES.TREE1, -1 - (Math.random() * 2));
    addSprite(n + Util.randomInt(0,5), SPRITES.TREE2, -1 - (Math.random() * 2));
  }

  ...
}

ملاحظة: إذا كنا ننشئ لعبة حقيقية ، فيمكننا كتابة محرر طريق لإنشاء خريطة بصريًا بالتلال والمنحنيات ، بالإضافة إلى إضافة آلية لترتيب النقوش المتحركة على طول الطريق ... ولكن بالنسبة لمهامنا ، يمكننا فقط القيام بذلك برمجيًا addSprite().

مضيفا آلات


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

var cars      = [];  // array of cars on the road
var totalCars = 200; // total number of cars on the road

function addSegment() {
  segments.push({
    ...
    cars: [], // array of cars within this segment
    ...
  });
}

يتيح لنا تخزين هيكلين لبيانات السيارات التنقل بسهولة بين جميع السيارات بطريقة ما update()، ونقلها من جزء إلى آخر إذا لزم الأمر ؛ في الوقت نفسه ، يسمح لنا هذا بتنفيذ render()الآلات فقط على الأجزاء المرئية.

يتم إعطاء كل آلة تحولًا أفقيًا عشوائيًا وموضع z ومصدر العفريت وسرعة:

function resetCars() {
  cars = [];
  var n, car, segment, offset, z, sprite, speed;
  for (var n = 0 ; n < totalCars ; n++) {
    offset = Math.random() * Util.randomChoice([-0.8, 0.8]);
    z      = Math.floor(Math.random() * segments.length) * segmentLength;
    sprite = Util.randomChoice(SPRITES.CARS);
    speed  = maxSpeed/4 + Math.random() * maxSpeed/(sprite == SPRITES.SEMI ? 4 : 2);
    car = { offset: offset, z: z, sprite: sprite, speed: speed };
    segment = findSegment(car.z);
    segment.cars.push(car);
    cars.push(car);
  }
}

جعل التل (عودة)


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

for(n = 0 ; n < drawDistance ; n++) {

  ...

  if ((segment.p1.camera.z <= cameraDepth) || // behind us
      (segment.p2.screen.y >= maxy))          // clip by (already rendered) segment
    continue;

  ...

  maxy = segment.p2.screen.y;
}

سيسمح لنا هذا بقطع الأجزاء التي ستغطيها التلال التي تم تقديمها بالفعل.

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

عرض اللوحات والأشجار والسيارات


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

هذا يعقد أسلوبنا ويجبرنا render()على تجاوز أجزاء الطريق على مرحلتين:

  1. من الأمام للخلف لتقديم الطريق
  2. إلى الأمام لتقديم العفاريت


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

يمكننا حل المشكلة الأخيرة عن طريق حفظ قيمة maxyكل جزء كخط clipفي الخطوة 1. ثم يمكننا قص العفاريت لهذا الجزء على طول الخط clipفي الخطوة 2.

ويحدد بقية منطق العرض كيفية تغيير حجم العفريت ووضعه استنادًا إلى معامل scaleوإحداثيات screenأجزاء الطريق (محسوبة على المرحلة 1) ، ونتيجة لذلك لدينا في المرحلة الثانية من الطريقة render()ما يلي:

// back to front painters algorithm
for(n = (drawDistance-1) ; n > 0 ; n--) {
  segment = segments[(baseSegment.index + n) % segments.length];

  // render roadside sprites
  for(i = 0 ; i < segment.sprites.length ; i++) {
    sprite      = segment.sprites[i];
    spriteScale = segment.p1.screen.scale;
    spriteX     = segment.p1.screen.x + (spriteScale * sprite.offset * roadWidth * width/2);
    spriteY     = segment.p1.screen.y;
    Render.sprite(ctx, width, height, resolution, roadWidth, sprites, sprite.source, spriteScale, spriteX, spriteY, (sprite.offset < 0 ? -1 : 0), -1, segment.clip);
  }

  // render other cars
  for(i = 0 ; i < segment.cars.length ; i++) {
    car         = segment.cars[i];
    sprite      = car.sprite;
    spriteScale = Util.interpolate(segment.p1.screen.scale, segment.p2.screen.scale, car.percent);
    spriteX     = Util.interpolate(segment.p1.screen.x,     segment.p2.screen.x,     car.percent) + (spriteScale * car.offset * roadWidth * width/2);
    spriteY     = Util.interpolate(segment.p1.screen.y,     segment.p2.screen.y,     car.percent);
    Render.sprite(ctx, width, height, resolution, roadWidth, sprites, car.sprite, spriteScale, spriteX, spriteY, -0.5, -1, segment.clip);
  }

}

اصطدام مع اللوحات الإعلانية والأشجار


الآن بعد أن نتمكن من إضافة وتجسيد الكائنات على طول حواف الطريق ، نحتاج إلى تغيير الطريقة update()لتحديد ما إذا كان اللاعب قد واجه أيًا من هذه النقوش المتحركة في فقرته الحالية:

نحن نستخدم طريقة مساعدة Util.overlap()لتنفيذ التعرف العام على تقاطع المستطيلات. إذا تم الكشف عن تقاطع ، نوقف السيارة:

if ((playerX < -1) || (playerX > 1)) {
  for(n = 0 ; n < playerSegment.sprites.length ; n++) {
    sprite  = playerSegment.sprites[n];
    spriteW = sprite.source.w * SPRITES.SCALE;
    if (Util.overlap(playerX, playerW, sprite.offset + spriteW/2 * (sprite.offset > 0 ? 1 : -1), spriteW)) {
      // stop the car
      break;
    }
  }
}

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

اصطدام السيارات


بالإضافة إلى التصادمات مع النقوش المتحركة على طول حواف الطريق ، نحتاج إلى التعرف على التصادمات مع السيارات الأخرى ، وإذا تم اكتشاف تقاطع ، فإننا نبطئ اللاعب من خلال "دفعه" إلى الخلف خلف الآلة التي اصطدم بها:

for(n = 0 ; n < playerSegment.cars.length ; n++) {
  car  = playerSegment.cars[n];
  carW = car.sprite.w * SPRITES.SCALE;
  if (speed > car.speed) {
    if (Util.overlap(playerX, playerW, car.offset, carW, 0.8)) {
      // slow the car
      break;
    }
  }
}

تحديث الجهاز


حتى تتحرك السيارات الأخرى على طول الطريق ، سنمنحهم أبسط الذكاء الاصطناعي:

  • يركب بسرعة ثابتة
  • يلتف تلقائيا حول اللاعب عند التجاوز
  • يلتف تلقائيا حول السيارات الأخرى عند التجاوز

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

كل هذا يحدث أثناء دورة اللعبة update()أثناء مكالمة updateCars()ننقل فيها كل سيارة إلى الأمام بسرعة ثابتة وننتقل من جزء إلى آخر إذا تحركوا مسافة كافية خلال هذا الإطار.

function updateCars(dt, playerSegment, playerW) {
  var n, car, oldSegment, newSegment;
  for(n = 0 ; n < cars.length ; n++) {
    car         = cars[n];
    oldSegment  = findSegment(car.z);
    car.offset  = car.offset + updateCarOffset(car, oldSegment, playerSegment, playerW);
    car.z       = Util.increase(car.z, dt * car.speed, trackLength);
    car.percent = Util.percentRemaining(car.z, segmentLength); // useful for interpolation during rendering phase
    newSegment  = findSegment(car.z);
    if (oldSegment != newSegment) {
      index = oldSegment.cars.indexOf(car);
      oldSegment.cars.splice(index, 1);
      newSegment.cars.push(car);
    }
  }
}

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

في مشروعنا ، نستخدم القوة الغاشمة لمنظمة العفو الدولية الساذجة ، مما يجبر كل آلة على:

  • نتطلع إلى 20 قطعة
  • إذا وجدت سيارة أبطأ أمامها تعبر طريقها ، فالتف حولها
  • انعطف يمينًا من العقبات على الجانب الأيسر من الطريق
  • انعطف يسارًا من العقبات على الجانب الأيمن من الطريق
  • أدر ما يكفي لتجنب العقبات أمام المسافة المتبقية

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

function updateCarOffset(car, carSegment, playerSegment, playerW) {

  var i, j, dir, segment, otherCar, otherCarW, lookahead = 20, carW = car.sprite.w * SPRITES.SCALE;

  // optimization, dont bother steering around other cars when 'out of sight' of the player
  if ((carSegment.index - playerSegment.index) > drawDistance)
    return 0;

  for(i = 1 ; i < lookahead ; i++) {
    segment = segments[(carSegment.index+i)%segments.length];

    if ((segment === playerSegment) && (car.speed > speed) && (Util.overlap(playerX, playerW, car.offset, carW, 1.2))) {
      if (playerX > 0.5)
        dir = -1;
      else if (playerX < -0.5)
        dir = 1;
      else
        dir = (car.offset > playerX) ? 1 : -1;
      return dir * 1/i * (car.speed-speed)/maxSpeed; // the closer the cars (smaller i) and the greater the speed ratio, the larger the offset
    }

    for(j = 0 ; j < segment.cars.length ; j++) {
      otherCar  = segment.cars[j];
      otherCarW = otherCar.sprite.w * SPRITES.SCALE;
      if ((car.speed > otherCar.speed) && Util.overlap(car.offset, carW, otherCar.offset, otherCarW, 1.2)) {
        if (otherCar.offset > 0.5)
          dir = -1;
        else if (otherCar.offset < -0.5)
          dir = 1;
        else
          dir = (car.offset > otherCar.offset) ? 1 : -1;
        return dir * 1/i * (car.speed-otherCar.speed)/maxSpeed;
      }
    }
  }
}

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

واجهه المستخدم


أخيرًا ، سننشئ واجهة HTML بدائية:

<div id = "hud">
  <span id = "speed" class = "hud"> <span id = "speed_value" class = "value"> 0 </span> ميل في الساعة </ span>
  <span id = "current_lap_time" class = "hud"> الوقت: <span id = "current_lap_time_value" class = "value"> 0.0 </span> </span> 
  <span id = "last_lap_time" class = "hud"> اللفة الأخيرة: <span id = "last_lap_time_value" class = "value"> 0.0 </span> </span>
  <span id = "fast_lap_time" class = "hud"> أسرع لفة: <span id = "fast_lap_time_value" class = "value"> 0.0 </span> </span>
</div>

... وإضافة نمط CSS إليها

#hud                   { position: absolute; z-index: 1; width: 640px; padding: 5px 0; font-family: Verdana, Geneva, sans-serif; font-size: 0.8em; background-color: rgba(255,0,0,0.4); color: black; border-bottom: 2px solid black; box-sizing: border-box; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; }
#hud .hud              { background-color: rgba(255,255,255,0.6); padding: 5px; border: 1px solid black; margin: 0 5px; transition-property: background-color; transition-duration: 2s; -webkit-transition-property: background-color; -webkit-transition-duration: 2s; }
#hud #speed            { float: right; }
#hud #current_lap_time { float: left;  }
#hud #last_lap_time    { float: left; display: none;  }
#hud #fast_lap_time    { display: block; width: 12em;  margin: 0 auto; text-align: center; transition-property: background-color; transition-duration: 2s; -webkit-transition-property: background-color; -webkit-transition-duration: 2s; }
#hud .value            { color: black; font-weight: bold; }
#hud .fastest          { background-color: rgba(255,215,0,0.5); }


... وسنقوم بتنفيذ التحديث () خلال دورة اللعبة:

if (position > playerZ) {
  if (currentLapTime && (startPosition < playerZ)) {
    lastLapTime    = currentLapTime;
    currentLapTime = 0;
    if (lastLapTime <= Util.toFloat(Dom.storage.fast_lap_time)) {
      Dom.storage.fast_lap_time = lastLapTime;
      updateHud('fast_lap_time', formatTime(lastLapTime));
      Dom.addClassName('fast_lap_time', 'fastest');
      Dom.addClassName('last_lap_time', 'fastest');
    }
    else {
      Dom.removeClassName('fast_lap_time', 'fastest');
      Dom.removeClassName('last_lap_time', 'fastest');
    }
    updateHud('last_lap_time', formatTime(lastLapTime));
    Dom.show('last_lap_time');
  }
  else {
    currentLapTime += dt;
  }
}

updateHud('speed',            5 * Math.round(speed/500));
updateHud('current_lap_time', formatTime(currentLapTime));

updateHud()تسمح لنا طريقة المساعد بتحديث عناصر DOM فقط عندما تتغير القيم ، لأن مثل هذا التحديث يمكن أن يكون عملية بطيئة ويجب ألا ننفذها بمعدل 60 إطارًا في الثانية إذا لم تتغير القيم نفسها.

function updateHud(key, value) { // accessing DOM can be slow, so only do it if value has changed
  if (hud[key].value !== value) {
    hud[key].value = value;
    Dom.set(hud[key].dom, value);
  }
}

استنتاج



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

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

الرمز بواسطة github ، ويمكنك محاولة تحويله إلى لعبة سباق أكثر تقدمًا. يمكنك أيضًا تجربة:

  • إضافة مؤثرات صوتية للسيارات
  • تحسين مزامنة الموسيقى
  • تنفيذ ملء الشاشة
  • ( , , , ..)
  • (, ..)
  • ,
  • , -
  • ,
  • ( , ..)
  • drawDistance
  • x,y
  • ( , )
  • شوكة وصلات الطرق
  • تغيير الليل والنهار
  • احوال الطقس
  • الأنفاق والجسور والسحب والجدران والمباني
  • مدينة ، صحراء ، محيط
  • إضافة سياتل وإبرة الفضاء إلى الخلفيات
  • "الأوغاد" - أضف منافسين للتنافس معهم
  • أوضاع اللعبة - أسرع لفة ، سباق واحد على واحد (التقاط القطع النقدية؟ ، إطلاق النار على الأشرار؟)
  • طن من خيارات تخصيص اللعب
  • إلخ
  • ...

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

المراجع



روابط إلى عروض توضيحية قابلة للتشغيل:


All Articles