مطالعه موردی - یورش! آرنا

مقدمه

در ژوئن سال 2010، متوجه شدیم که نشریه محلی بوئینگ بوینگ در حال برگزاری مسابقه توسعه بازی است. ما این را بهانه ای کاملاً خوب برای ساختن یک بازی سریع و ساده در جاوا اسکریپت و <canvas> دیدیم، بنابراین دست به کار شدیم. بعد از مسابقه هنوز ایده های زیادی داشتیم و می خواستیم کاری را که شروع کردیم به پایان برسانیم. در اینجا مطالعه موردی نتیجه، یک بازی کوچک به نام Onslaught است! آرنا .

ظاهر یکپارچهسازی با سیستمعامل و پیکسلی

مهم این بود که بازی ما شبیه یک بازی سیستم سرگرمی نینتندو یکپارچهسازی با سیستمعامل باشد، با توجه به فرضیه مسابقه برای توسعه یک بازی مبتنی بر چیپ‌تون . اکثر بازی‌ها این نیاز را ندارند، اما به دلیل سهولت ایجاد دارایی و جذابیت طبیعی برای گیمرهای نوستالژیک، همچنان یک سبک هنری رایج (به ویژه در میان توسعه‌دهندگان مستقل) است.

هجوم! اندازه پیکسل آرنا
افزایش اندازه پیکسل می تواند کار طراحی گرافیکی را کاهش دهد.

با توجه به اینکه این اسپرایت ها چقدر کوچک هستند، تصمیم گرفتیم پیکسل های خود را دو برابر کنیم، به این معنی که یک اسپرایت 16x16 اکنون 32x32 پیکسل و غیره خواهد بود. از همان ابتدا، به جای اینکه مرورگر را وادار کنیم کارهای سنگین را انجام دهد، جنبه ایجاد دارایی را دوچندان کرده بودیم. اجرای این کار ساده‌تر بود، اما مزایای ظاهری مشخصی نیز داشت.

در اینجا یک سناریو است که ما در نظر گرفتیم:

<style>
canvas {
  width: 640px;
  height: 320px;
}
</style>
<canvas width="320" height="240">
  Sorry, your browser is not supported.
</canvas>

این روش به جای دو برابر کردن آنها در سمت ایجاد دارایی، از sprites 1x1 تشکیل شده است. از آنجا، CSS خود بوم را در دست گرفته و اندازه آن را تغییر می دهد. معیارهای ما نشان داد که این روش می‌تواند دو برابر سریع‌تر از رندر کردن تصاویر بزرگ‌تر (دوبرابر) باشد، اما متأسفانه تغییر اندازه CSS شامل anti-aliasing است، چیزی که ما نتوانستیم راهی برای جلوگیری از آن پیدا کنیم.

گزینه های تغییر اندازه بوم
سمت چپ: دارایی های پیکسل کامل در فتوشاپ دو برابر شده است. راست: تغییر اندازه CSS یک جلوه تار اضافه کرد.

این یک شکست برای بازی ما بود، زیرا تک تک پیکسل ها بسیار مهم هستند، اما اگر نیاز به تغییر اندازه بوم خود دارید و ضد aliasing برای پروژه شما مناسب است، می توانید این رویکرد را به دلایل عملکرد در نظر بگیرید.

ترفندهای جالب بوم نقاشی

همه ما می دانیم که <canvas> داغ جدید است، اما گاهی اوقات توسعه دهندگان هنوز استفاده از DOM را توصیه می کنند . اگر نمی‌خواهید از آن استفاده کنید، در اینجا نمونه‌ای از این است که چگونه <canvas> در زمان و انرژی ما صرفه‌جویی کرد.

هنگامی که یک دشمن در Onslaught ضربه می خورد! Arena ، قرمز چشمک می زند و به طور خلاصه یک انیمیشن "درد" را نمایش می دهد. برای محدود کردن تعداد گرافیک‌هایی که باید ایجاد می‌کردیم، فقط دشمنان را در «درد» در جهت رو به پایین نشان می‌دهیم. این در بازی قابل قبول به نظر می رسد و در زمان ایجاد جن صرفه جویی می کند. با این حال، برای هیولاهای رئیس، دیدن یک اسپرایت بزرگ (با ابعاد 64×64 پیکسل یا بیشتر) که از سمت چپ یا بالا به طور ناگهانی به سمت پایین برای قاب درد می‌چرخد، سخت بود.

یک راه حل واضح می تواند ترسیم یک قاب درد برای هر رئیس در هر یک از هشت جهت باشد، اما این کار بسیار زمان بر خواهد بود. به لطف <canvas> ، ما توانستیم این مشکل را در کد حل کنیم:

Beholder در Onslaught آسیب می بیند! آرنا
اثرات جالبی را می توان با استفاده از context.globalCompositeOperation ایجاد کرد.

ابتدا هیولا را به یک "بافر" پنهان <canvas> می کشیم، آن را با رنگ قرمز می پوشانیم، سپس نتیجه را به صفحه نمایش می دهیم. کد چیزی شبیه به این است:

// Get the "buffer" canvas (that isn't visible to the user)
var bufferCanvas = document.getElementById("buffer");
var buffer = bufferCanvas.getContext("2d");

// Draw your image on the buffer
buffer.drawImage(image, 0, 0);

// Draw a rectangle over the image using a nice translucent overlay
buffer.save();
buffer.globalCompositeOperation = "source-in";
buffer.fillStyle = "rgba(186, 51, 35, 0.6)"; // red
buffer.fillRect(0, 0, image.width, image.height);
buffer.restore();

// Copy the buffer onto the visible canvas
document.getElementById("stage").getContext("2d").drawImage(bufferCanvas, x, y);

حلقه بازی

توسعه بازی تفاوت های قابل توجهی با توسعه وب دارد. در پشته وب، واکنش به رویدادهایی که از طریق شنوندگان رویداد اتفاق می‌افتند، معمول است. بنابراین کد اولیه ممکن است کاری جز گوش دادن به رویدادهای ورودی انجام ندهد. منطق یک بازی متفاوت است، زیرا لازم است دائماً خود را به روز کنید. به عنوان مثال، اگر بازیکنی حرکت نکرده باشد، این نباید مانع از گرفتن او توسط گابلین ها شود!

در اینجا نمونه ای از حلقه بازی آورده شده است:

function main () {
  handleInput();
  update();
  render();
};

setInterval(main, 1);

اولین تفاوت مهم این است که تابع handleInput در واقع هیچ کاری را بلافاصله انجام نمی دهد. اگر کاربر کلیدی را در یک برنامه وب معمولی فشار دهد، منطقی است که فوراً عمل مورد نظر را انجام دهد. اما در یک بازی، همه چیز باید به ترتیب زمانی اتفاق بیفتد تا به درستی جریان یابد.

window.addEventListener("mousedown", function(e) {
  // A mouse click means the players wants to attack.
  // We don't actually do that yet, but instead tell the rest
  // of the program about the request.
  buttonStates[e.button] = true;
}, false);

function handleInput() {
  // Here is where we respond to the click
  if (buttonStates[LEFT_BUTTON]) {
    player.attacking = true;
    delete buttonStates[LEFT_BUTTON];
  }
};

اکنون ما در مورد ورودی می دانیم و می توانیم آن را در عملکرد update در نظر بگیریم، زیرا می دانیم که به بقیه قوانین بازی پایبند است.

function update() {
  // Check for collisions, states, whatever else is needed

  // If after that the player can still attack, do it!
  if (player.attacking && player.canAttack()) {
    player.attack();
  }
};

در نهایت، هنگامی که همه چیز محاسبه شد، نوبت به ترسیم مجدد صفحه می رسد! در DOM-land، مرورگر این افزایش را انجام می دهد. اما هنگام استفاده از <canvas> لازم است هر زمان که اتفاقی می افتد به صورت دستی دوباره ترسیم شود (که معمولاً هر فریم است!).

function render() {
  // First erase everything, something like:
  context.clearRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);

  // Draw the player (and whatever else you need)
  context.drawImage(
    player.getImage(),
    player.x, player.y
  );
};

مدلسازی مبتنی بر زمان

مدل‌سازی مبتنی بر زمان، مفهوم حرکت اسپرایت‌ها بر اساس مقدار زمان سپری شده از آخرین به‌روزرسانی فریم است. این تکنیک به بازی شما اجازه می دهد تا با حداکثر سرعت ممکن اجرا شود و در عین حال اطمینان حاصل شود که اسپرایت ها با سرعت ثابت حرکت می کنند.

برای استفاده از مدل‌سازی مبتنی بر زمان، باید زمان سپری شده از زمان ترسیم آخرین فریم را ثبت کنیم. برای ردیابی این موضوع باید تابع update() حلقه بازی خود را افزایش دهیم.

function update() {

  // NOTE: You'll need to initially seed this.lastUpdate
  // with the current time when your game loop starts
  // this.lastUpdate = Date.now();

  // Calculate elapsed time since last frame
  var now = Date.now();
  var elapsed = (now - this.lastUpdate);
  this.lastUpdate = now;

  // Do stuff with elapsed

};

اکنون که زمان سپری شده را داریم، می توانیم محاسبه کنیم که یک اسپرایت معین چقدر باید هر فریم را حرکت دهد. اول، ما باید چند چیز را در یک شیء اسپرایت ردیابی کنیم: موقعیت فعلی، سرعت و جهت.

var Sprite = function() {

  // The sprite's position relative to the top left of the game world
  this.position = {x: 0, y: 0};

  // The sprite's direction. A positive x value indicates moving to the right
  this.direction = {x: 1, y: 0};

  // How many pixels the sprite moves per second
  this.speed = 50;
};

با در نظر گرفتن این متغیرها، در اینجا نحوه انتقال نمونه ای از کلاس sprite بالا با استفاده از مدل سازی مبتنی بر زمان آورده شده است:

// Determine how far this sprite will move this frame
var distance = (sprite.speed / 1000) * elapsed;

// Apply the movement distance to the sprite's current position
// taking into account its direction
sprite.position.x += (distance * sprite.direction.x);
sprite.position.y += (distance * sprite.direction.y);

توجه داشته باشید که مقادیر direction.x و direction.y باید نرمال شوند، به این معنی که همیشه باید بین -1 و 1 قرار گیرند.

کنترل ها

کنترل ها احتمالاً بزرگترین مانع در هنگام توسعه Onslaught بوده اند! آرنا . اولین نسخه نمایشی فقط از صفحه کلید پشتیبانی می کرد. بازیکنان شخصیت اصلی را با کلیدهای جهت دار در اطراف صفحه حرکت داده و با نوار فاصله به سمتی که او رو به رو بود شلیک می کنند. در حالی که تا حدودی بصری و آسان برای درک، این بازی را تقریبا غیر قابل بازی در سطوح سخت تر می کند. با وجود ده‌ها دشمن و پرتابه که در هر لحظه به سمت بازیکن پرواز می‌کنند، ضروری است که بتوانید در هر جهتی که شلیک می‌کنید، بین افراد بد ببافید.

به منظور مقایسه با بازی های مشابه در ژانر خود، ما پشتیبانی از ماوس را برای کنترل یک شبکه هدف قرار دادیم که شخصیت از آن برای هدف گیری حملات خود استفاده می کند. هنوز هم می‌توان شخصیت را با صفحه کلید جابه‌جا کرد، اما پس از این تغییر می‌توانست به طور همزمان در هر جهت کامل 360 درجه شلیک کند. بازیکنان هاردکور از این ویژگی قدردانی می‌کردند، اما عواقب ناخوشایند ناامید کردن کاربران ترک پد را داشت.

هجوم! کنترل‌های آرنا مودال (منسوخ شده)
یک کنترل قدیمی یا مدال "چگونه بازی کنیم" در Onslaught! آرنا.

برای جا دادن به کاربران ترک‌پد، کنترل‌های کلید پیکان را بازگردانیم، این بار تا امکان شلیک در جهت(های) فشار داده شده را فراهم کنیم. در حالی که ما احساس می‌کردیم که به انواع بازیکنان غذا می‌دهیم، ناخودآگاه پیچیدگی زیادی را به بازی خود وارد می‌کردیم. در کمال تعجب، بعداً شنیدیم که برخی از بازیکنان از کنترل‌های اختیاری ماوس (یا صفحه کلید!) برای حمله آگاه نبودند، علی‌رغم مدال‌های آموزشی، که تا حد زیادی نادیده گرفته شدند.

هجوم! آموزش کنترل های آرنا
بازیکنان عمدتاً پوشش آموزشی را نادیده می گیرند. آنها ترجیح می دهند بازی کنند و لذت ببرند!

ما همچنین خوش شانس هستیم که برخی از طرفداران اروپایی داریم، اما ناامیدی از آنها شنیده ایم که ممکن است صفحه کلید QWERTY معمولی نداشته باشند و قادر به استفاده از کلیدهای WASD برای حرکت جهت دار نباشند. بازیکنان چپ دست نیز شکایت های مشابهی داشته اند.

با این طرح کنترل پیچیده ای که ما اجرا کرده ایم، مشکل بازی در دستگاه های تلفن همراه نیز وجود دارد. در واقع، یکی از رایج ترین درخواست های ما این است که Onslaught را بسازیم! Arena در Android، iPad و سایر دستگاه های لمسی (جایی که صفحه کلید وجود ندارد) موجود است. یکی از نقاط قوت اصلی HTML5 قابل حمل بودن آن است، بنابراین نصب بازی روی این دستگاه‌ها قطعاً قابل انجام است، فقط باید بسیاری از مشکلات (به ویژه کنترل‌ها و عملکرد) را حل کنیم.

برای پرداختن به این بسیاری از مسائل، ما بازی را با یک روش گیم پلی تک ورودی شروع کردیم که فقط شامل تعامل ماوس (یا لمس) است. بازیکنان روی صفحه کلیک کرده یا لمس می‌کنند و شخصیت اصلی به سمت مکان فشرده می‌رود و به طور خودکار به نزدیک‌ترین آدم بد حمله می‌کند. کد چیزی شبیه به این است:

// Find the nearest hostile target (if any) to the player
var player = this.getPlayerObject();
var hostile = this.getNearestHostile(player);
if (hostile !== null) {
  // Found one! Shoot in its direction
  var shoot = hostile.boundingBox().center().subtract(
    player.boundingBox().center()
  ).normalize();
}

// Move towards where the player clicked/touched
var move = this.targetReticle.position.clone().subtract(
  player.boundingBox().center()
).normalize();
var distance = this.targetReticle.position.clone().subtract(
  player.boundingBox().center()
).magnitude();

// Prevent jittering if the character is close enough
if (distance < 3) {
  move.zero();
}

// Move the player
if ((move.x !== 0) || (move.y !== 0)) {
  player.setDirection(move);
}

حذف عامل اضافی یعنی هدف قرار دادن دشمنان می‌تواند بازی را در برخی شرایط آسان‌تر کند، اما ما احساس می‌کنیم که ساده‌تر کردن کارها برای بازیکن مزایای زیادی دارد. استراتژی های دیگری مانند قرار دادن شخصیت در نزدیکی دشمنان خطرناک برای هدف قرار دادن آنها ظاهر می شود و توانایی پشتیبانی از دستگاه های لمسی بسیار ارزشمند است.

صوتی

در میان کنترل‌ها و عملکرد، یکی از بزرگترین مشکلات ما در حین توسعه Onslaught! Arena تگ <audio> HTML5 بود. احتمالاً بدترین جنبه تأخیر است: تقریباً در همه مرورگرها بین فراخوانی .play() و صدای واقعی پخش می شود. این می تواند تجربه یک گیمر را خراب کند، به خصوص زمانی که با یک بازی سریع مانند بازی ما بازی می کنید.

مشکلات دیگر عبارتند از : رویداد "پیشرفت" فعال نشد ، که می تواند باعث شود جریان بارگذاری بازی به طور نامحدود متوقف شود. به این دلایل، ما روشی را که ما آن را "سقوط به جلو" می نامیم، اتخاذ کردیم، که در آن اگر Flash بارگیری نشد، به HTML5 Audio تغییر می کنیم. کد چیزی شبیه به این است:

/*
This example uses the SoundManager 2 library by Scott Schiller:
http://www.schillmania.com/projects/soundmanager2/
*/

// Default to sm2 (Flash)
var api = "sm2";

function initAudio (callback) {
  switch (api) {
    case "sm2":
      soundManager.onerror = (function (init) {
        return function () {
          api = "html5";
          init(callback);
        };
      }(arguments.callee));
      break;
    case "html5":
      var audio = document.createElement("audio");

      if (
        audio
        && audio.canPlayType
        && audio.canPlayType("audio/mpeg;")
      ) {
        callback();
      } else {
        // No audio support :(
      }
      break;
  }
};

همچنین ممکن است برای یک بازی مهم باشد که از مرورگرهایی که فایل های MP3 را پخش نمی کنند (مانند Mozilla Firefox) پشتیبانی کند. در این صورت، پشتیبانی را می توان شناسایی کرد و به چیزی مانند Ogg Vorbis با کدی مانند زیر تغییر داد:

/*
Note: you could instead use "new Audio()" here,
but the client will throw an error if it doesn't support Audio,
which makes using "document.createElement" a safer approach.
*/

var audio = document.createElement("audio");

if (audio && audio.canPlayType) {
  if (!audio.canPlayType("audio/mpeg;")) {
    // Here you know you CANNOT use .mp3 files
    if (audio.canPlayType("audio/ogg; codecs=vorbis")) {
      // Here you know you CAN use .ogg files
    }
  }
}

ذخیره داده ها

شما نمی توانید بدون نمرات بالا یک بازی به سبک آرکید داشته باشید! ما می‌دانستیم که برای ادامه دادن به برخی از داده‌های بازی خود نیاز داریم، و در حالی که می‌توانستیم از چیزی قدیمی مانند کوکی‌ها استفاده کنیم، می‌خواستیم به فناوری‌های سرگرم‌کننده جدید HTML5 بپردازیم. مطمئناً هیچ کمبودی در گزینه‌ها از جمله ذخیره‌سازی محلی، ذخیره‌سازی جلسه و پایگاه‌های داده وب SQL وجود ندارد.

ALT_TEXT_HERE
نمرات بالا ذخیره می شود، و همچنین جایگاه شما در بازی پس از شکست دادن هر رئیس ذخیره می شود.

ما تصمیم گرفتیم از localStorage استفاده کنیم زیرا جدید، عالی و آسان برای استفاده است. از ذخیره جفت‌های کلید/مقدار اصلی پشتیبانی می‌کند که تمام بازی ساده ما مورد نیاز است. در اینجا یک مثال ساده از نحوه استفاده از آن آورده شده است:

if (typeof localStorage == "object") {
  localStorage.setItem("foo", "bar");
  localStorage.getItem("foo"); // Value is "bar"
  localStorage.removeItem("foo");
  localStorage.getItem("foo"); // Value is now null
}

برخی از "گوچاها" وجود دارد که باید از آنها آگاه بود. مهم نیست که چه چیزی را ارسال می کنید، مقادیر به صورت رشته ای ذخیره می شوند که می تواند منجر به نتایج غیرمنتظره ای شود:

localStorage.setItem("foo", false);
typeof localStorage.getItem("foo"); // Value is "false" (a string literal)
if (localStorage.getItem("foo")) {
  // It's true!
}

// Don't pass objects into setItem
localStorage.setItem("bar", {"key": "value"});
localStorage.getItem("bar"); // Value is "[object Object]" (a string literal)

// JSON stringify and parse when dealing with localStorage
localStorage.setItem("json", JSON.stringify({"key": "value"}));
typeof localStorage.getItem("json"); // string
JSON.parse(localStorage.getItem("json")); // {"key": "value"}

خلاصه

کار با HTML5 شگفت انگیز است. بیشتر پیاده‌سازی‌ها همه چیزهایی را که یک توسعه‌دهنده بازی نیاز دارد، از گرافیک گرفته تا ذخیره وضعیت بازی، انجام می‌دهند. در حالی که مشکلات فزاینده ای وجود دارد (مانند مشکلات تگ <audio> )، توسعه دهندگان مرورگر به سرعت در حال حرکت هستند و با چیزهایی که در حال حاضر عالی هستند، آینده برای بازی های ساخته شده بر روی HTML5 روشن به نظر می رسد.

هجوم! آرنا با لوگوی پنهان HTML5
هنگام بازی Onslaught می توانید با تایپ "html5" یک سپر HTML5 دریافت کنید! آرنا.