কেস স্টাডি - বিল্ডিং রেসার

ভূমিকা

রেসার হল একটি ওয়েব-ভিত্তিক মোবাইল ক্রোম এক্সপেরিমেন্ট যা অ্যাক্টিভ থিওরি দ্বারা তৈরি করা হয়েছে। 5 জন পর্যন্ত বন্ধু তাদের ফোন বা ট্যাবলেটগুলিকে প্রতিটি স্ক্রীন জুড়ে রেস করার জন্য সংযুক্ত করতে পারে৷ Google ক্রিয়েটিভ ল্যাব থেকে ধারণা, ডিজাইন এবং প্রোটোটাইপ এবং Plan8-এর সাউন্ড দিয়ে সজ্জিত আমরা I/O 2013-এ লঞ্চের আগে 8 সপ্তাহের জন্য বিল্ডগুলিতে পুনরাবৃত্তি করেছি। এখন যেহেতু গেমটি কয়েক সপ্তাহ ধরে লাইভ হয়েছে আমাদের কাছে এটি কীভাবে কাজ করে সে সম্পর্কে বিকাশকারী সম্প্রদায় থেকে কিছু প্রশ্ন করার সুযোগ। নীচে মূল বৈশিষ্ট্যগুলির একটি ব্রেকডাউন এবং আমাদের প্রায়শই জিজ্ঞাসা করা প্রশ্নগুলির উত্তর রয়েছে৷

ট্র্যাক

আমরা একটি মোটামুটি সুস্পষ্ট চ্যালেঞ্জের মুখোমুখি হলাম কিভাবে একটি ওয়েব ভিত্তিক মোবাইল গেম তৈরি করা যায় যা বিভিন্ন ধরণের ডিভাইস জুড়ে ভাল কাজ করে। খেলোয়াড়দের বিভিন্ন ফোন এবং ট্যাবলেট দিয়ে একটি রেস তৈরি করতে সক্ষম হতে হবে। একজন খেলোয়াড়ের একটি Nexus 4 থাকতে পারে এবং সে তার বন্ধুর সাথে যার একটি আইপ্যাড ছিল তার বিরুদ্ধে প্রতিযোগিতা করতে চায়। প্রতিটি রেসের জন্য একটি সাধারণ ট্র্যাক আকার নির্ধারণ করার জন্য আমাদের একটি উপায় নিয়ে আসা দরকার। রেসে অন্তর্ভুক্ত প্রতিটি ডিভাইসের চশমার উপর নির্ভর করে সমাধানটি বিভিন্ন আকারের ট্র্যাক ব্যবহার করে জড়িত ছিল।

ট্র্যাক মাত্রা গণনা

প্রতিটি খেলোয়াড় যোগদানের সাথে সাথে, তাদের ডিভাইস সম্পর্কে তথ্য সার্ভারে পাঠানো হয় এবং অন্যান্য খেলোয়াড়দের সাথে ভাগ করা হয়। যখন ট্র্যাক তৈরি করা হচ্ছে, এই ডেটা ট্র্যাকের উচ্চতা এবং প্রস্থ গণনা করতে ব্যবহৃত হয়। আমরা সবচেয়ে ছোট পর্দার উচ্চতা খুঁজে বের করে উচ্চতা গণনা করি, এবং প্রস্থ হল সমস্ত পর্দার মোট প্রস্থ। সুতরাং নীচের উদাহরণে ট্র্যাকের প্রস্থ 1152 পিক্সেল এবং 519 পিক্সেলের উচ্চতা থাকবে।

লাল এলাকাটি এই উদাহরণের জন্য ট্র্যাকের মোট প্রস্থ এবং উচ্চতা দেখায়।
লাল এলাকাটি এই উদাহরণের জন্য ট্র্যাকের মোট প্রস্থ এবং উচ্চতা দেখায়।
this.getDimensions = function () {
  var response = {};
  response.width = 0;
  response.height = _gamePlayers[0].scrn.h; // First screen height
  response.screens = [];
  
  for (var i = 0; i < _gamePlayers.length; i++) {
    var player = _gamePlayers[i];
    response.width += player.scrn.w;

    if (player.scrn.h < response.height) {
      // Find the smallest screen height
      response.height = player.scrn.h;
    }
      
    response.screens.push(player.scrn);
  }
  
  return response;
}

ট্র্যাক অঙ্কন

Paper.js হল একটি ওপেন সোর্স ভেক্টর গ্রাফিক্স স্ক্রিপ্টিং ফ্রেমওয়ার্ক যা HTML5 ক্যানভাসের উপরে চলে। আমরা খুঁজে পেয়েছি Paper.js ট্র্যাকগুলির জন্য ভেক্টর আকার তৈরি করার জন্য নিখুঁত টুল, তাই আমরা SVG ট্র্যাকগুলিকে রেন্ডার করতে এর ক্ষমতা ব্যবহার করেছি যেগুলি Adobe Illustrator-এ একটি <canvas> উপাদানে তৈরি করা হয়েছিল। ট্র্যাক তৈরি করতে, TrackModel ক্লাস DOM-এ SVG কোড যুক্ত করে এবং TrackPathView এ পাঠানোর জন্য আসল মাত্রা এবং অবস্থান সম্পর্কে তথ্য সংগ্রহ করে যা ট্র্যাকটিকে একটি ক্যানভাসে আঁকবে।

paper.install(window);
_paper = new paper.PaperScope();
_paper.setup('track_canvas');
                    
var svg = document.getElementById('track');
var layer = new _paper.Layer();

_path = layer.importSvg(svg).firstChild.firstChild;
_path.strokeColor = '#14a8df';
_path.strokeWidth = 2;

একবার ট্র্যাক আঁকা হয়ে গেলে, ডিভাইস লাইন-আপ অর্ডারে তার অবস্থানের উপর ভিত্তি করে প্রতিটি ডিভাইস তার x অফসেট খুঁজে পায় এবং সেই অনুযায়ী ট্র্যাকটি অবস্থান করে।

var x = 0;

for (var i = 0; i < screens.length; i++) {
  if (i < PLAYER_INDEX) {
    x += screens[i].w;
  }
}
তারপর x অফসেটটি ট্র্যাকের উপযুক্ত অংশ দেখানোর জন্য ব্যবহার করা যেতে পারে।
তারপর x অফসেটটি ট্র্যাকের উপযুক্ত অংশ দেখানোর জন্য ব্যবহার করা যেতে পারে

CSS অ্যানিমেশন

Paper.js ট্র্যাক লেন আঁকার জন্য প্রচুর CPU প্রক্রিয়াকরণ ব্যবহার করে এবং এই প্রক্রিয়াটি বিভিন্ন ডিভাইসে কম বা বেশি সময় নেয়। এটি পরিচালনা করার জন্য, সমস্ত ডিভাইস ট্র্যাক প্রক্রিয়াকরণ শেষ না হওয়া পর্যন্ত লুপ করার জন্য আমাদের একটি লোডার প্রয়োজন৷ সমস্যা ছিল যে কোনো জাভাস্ক্রিপ্ট-ভিত্তিক অ্যানিমেশন Paper.js-এর CPU প্রয়োজনীয়তার কারণে ফ্রেমগুলি এড়িয়ে যাবে। CSS অ্যানিমেশনগুলি লিখুন, যা একটি পৃথক UI থ্রেডে চলে, যা আমাদেরকে "বিল্ডিং ট্র্যাক" পাঠ্য জুড়ে মসৃণভাবে অ্যানিমেট করার অনুমতি দেয়।

.glow {
  width: 290px;
  height: 290px;
  background: url('img/track-glow.png') 0 0 no-repeat;
  background-size: 100%;
  top: 0;
  left: -290px;
  z-index: 1;
  -webkit-animation: wipe 1.3s linear 0s infinite;
}

@-webkit-keyframes wipe {
  0% {
    -webkit-transform: translate(-300px, 0);
  }

  25% {
    -webkit-transform: translate(-300px, 0);
  }

  75% {
    -webkit-transform: translate(920px, 0);
  }

  100% {
    -webkit-transform: translate(920px, 0);
  }
}
}

CSS Sprites

ইন-গেম ইফেক্টের জন্য সিএসএসও কাজে এসেছে। মোবাইল ডিভাইসগুলি, তাদের সীমিত শক্তি সহ, ট্র্যাক জুড়ে চলমান গাড়িগুলিকে অ্যানিমেট করতে ব্যস্ত থাকে। তাই অতিরিক্ত উত্তেজনার জন্য আমরা গেমে প্রাক-রেন্ডার করা অ্যানিমেশনগুলি বাস্তবায়নের উপায় হিসাবে স্প্রাইট ব্যবহার করেছি। একটি CSS স্প্রাইট-এ, ট্রানজিশনগুলি একটি ধাপ-ভিত্তিক অ্যানিমেশন প্রয়োগ করে যা background-position বৈশিষ্ট্য পরিবর্তন করে, গাড়ির বিস্ফোরণ তৈরি করে।

#sprite {
  height: 100px; 
  width: 100px;
  background: url('sprite.jpg') 0 0 no-repeat;
  -webkit-animation: play-sprite 0.33s linear 0s steps(9) infinite;
}

@-webkit-keyframes play-sprite {
  0% {
    background-position: 0 0;
  }

  100% {
    background-position: -900px 0;
  }
}

এই কৌশলটির সমস্যা হল আপনি শুধুমাত্র একটি সারিতে রাখা স্প্রাইট শীট ব্যবহার করতে পারেন। একাধিক সারির মধ্য দিয়ে লুপ করার জন্য, অ্যানিমেশনকে একাধিক কীফ্রেম ঘোষণার মাধ্যমে চেইন করতে হবে।

#sprite {
  height: 100px; 
  width: 100px;
  background: url('sprite.jpg') 0 0 no-repeat;
  -webkit-animation-name: row1, row2, row3;
  -webkit-animation-duration: 0.2s;
  -webkit-animation-delay: 0s, 0.2s, 0.4s;
  -webkit-animation-timing-function: steps(5), steps(5), steps(5);
  -webkit-animation-fill-mode: forwards;
}

@-webkit-keyframes row1 {
  0% {
    background-position: 0 0;
  }

  100% {
    background-position: -500px 0;
  }
}

@-webkit-keyframes row2 {
  0% {
    background-position: 0 -100px;
  }

  100% {
    background-position: -500px -100px;
  }
}

@-webkit-keyframes row3 {
  0% {
    background-position: 0 -200px;
  }

  100% {
    background-position: -500px -200px;
  }
}

কার রেন্ডারিং

যেকোনো গাড়ি রেসিং গেমের মতো আমরা জানতাম যে ব্যবহারকারীকে ত্বরণ এবং পরিচালনার অনুভূতি দেওয়া গুরুত্বপূর্ণ। খেলার ভারসাম্য এবং মজার ফ্যাক্টরের জন্য আলাদা পরিমাণে ট্র্যাকশন প্রয়োগ করা গুরুত্বপূর্ণ ছিল, যাতে একবার একজন খেলোয়াড় পদার্থবিদ্যার প্রতি অনুধাবন করে, তারা কৃতিত্বের অনুভূতি পেতে পারে এবং আরও ভাল রেসার হয়ে উঠতে পারে।

আবারও আমরা Paper.js-এ কল করেছি যা গণিতের ইউটিলিটিগুলির একটি বিস্তৃত সেটের সাথে আসে। প্রতিটি ফ্রেমে গাড়ির অবস্থান এবং ঘূর্ণন মসৃণভাবে সামঞ্জস্য করার সময় আমরা গাড়িটিকে পথ বরাবর সরানোর জন্য এর কিছু পদ্ধতি ব্যবহার করেছি।

var trackOffset = _path.length - (_elapsed % _path.length);
var trackPoint = _path.getPointAt(trackOffset);
var trackAngle = _path.getTangentAt(trackOffset).angle;

// Apply the throttle
_velocity.length += _throttle;

if (!_throttle) {
  // Slow down since the throttle is off
  _velocity.length *= FRICTION;
}

if (_velocity.length > MAXVELOCITY) {
  _velocity.length = MAXVELOCITY;
}

_velocity.angle = trackAngle;
trackOffset -= _velocity.length;
_elapsed += _velocity.length;

// Find if a lap has been completed
if (trackOffset < 0) {
  while (trackOffset < 0) trackOffset += _path.length;

  trackPoint = _path.getPointAt(trackOffset);
  console.log('LAP COMPLETE!');
}

if (_velocity.length > 0.1) {
  // Render the car if there is actually velocity
  renderCar(trackPoint);
}

আমরা গাড়ি রেন্ডারিং অপ্টিমাইজ করার সময়, আমরা একটি আকর্ষণীয় পয়েন্ট খুঁজে পেয়েছি। iOS-এ, গাড়িতে একটি translate3d রূপান্তর প্রয়োগ করে সেরা পারফরম্যান্স অর্জন করা হয়েছিল:

_car.style.webkitTransform = 'translate3d('+_position.x+'px, '+_position.y+'px, 0px)rotate('+_rotation+'deg)';

অ্যান্ড্রয়েডের জন্য ক্রোমে, ম্যাট্রিক্স মান গণনা করে এবং একটি ম্যাট্রিক্স রূপান্তর প্রয়োগ করে সর্বোত্তম কর্মক্ষমতা অর্জন করা হয়েছিল:

var rad = _rotation.rotation * (Math.PI * 2 / 360);
var cos = Math.cos(rad);
var sin = Math.sin(rad);
var a = parseFloat(cos).toFixed(8);
var b = parseFloat(sin).toFixed(8);
var c = parseFloat(-sin).toFixed(8);
var d = a;
_car.style.webkitTransform = 'matrix(' + a + ', ' + b + ', ' + c + ', ' + d + ', ' + _position.x + ', ' + _position.y + ')';

ডিভাইস সিঙ্ক রাখা

বিকাশের সবচেয়ে গুরুত্বপূর্ণ (এবং কঠিন) অংশটি ছিল ডিভাইস জুড়ে গেমটি সিঙ্ক হয়েছে তা নিশ্চিত করা। আমরা ভেবেছিলাম ব্যবহারকারীরা ক্ষমা করতে পারেন যদি একটি গাড়ি মাঝে মাঝে একটি ধীর সংযোগের কারণে কয়েকটি ফ্রেম এড়িয়ে যায় তবে আপনার গাড়িটি একবারে একাধিক স্ক্রিনে প্রদর্শিত হলে এটি খুব মজার হবে না। এটি সমাধান করার জন্য প্রচুর ট্রায়াল এবং ত্রুটি প্রয়োজন, কিন্তু আমরা শেষ পর্যন্ত কয়েকটি কৌশলে স্থির হয়েছি যা এটিকে কার্যকর করেছে।

লেটেন্সি গণনা করা হচ্ছে

কম্পিউট ইঞ্জিন রিলে থেকে বার্তাগুলি পেতে কতক্ষণ সময় লাগে তা জানা ডিভাইসগুলি সিঙ্ক করার জন্য শুরুর বিন্দু। জটিল অংশটি হল যে প্রতিটি ডিভাইসের ঘড়িগুলি কখনই সম্পূর্ণ সিঙ্ক হবে না। এটি প্রায় পেতে, আমাদের ডিভাইস এবং সার্ভারের মধ্যে সময়ের পার্থক্য খুঁজে বের করতে হবে।

ডিভাইস এবং প্রধান সার্ভারের মধ্যে অফসেট সময় খুঁজে পেতে, আমরা বর্তমান ডিভাইস টাইমস্ট্যাম্প সহ একটি বার্তা পাঠাই। সার্ভার তখন সার্ভারের টাইমস্ট্যাম্প সহ আসল টাইমস্ট্যাম্প সহ উত্তর দেবে। আমরা সময়ের প্রকৃত পার্থক্য গণনা করতে প্রতিক্রিয়া ব্যবহার করি।

var currentTime = Date.now();
var latency = Math.round((currentTime - e.time) * .5);
var serverTime = e.serverTime;
currentTime -= latency;
var difference = currentTime - serverTime;

এটি একবার করাই যথেষ্ট নয়, কারণ সার্ভারে রাউন্ড ট্রিপ সর্বদা প্রতিসম হয় না, যার অর্থ সার্ভারে প্রতিক্রিয়া পেতে সার্ভারের পক্ষে এটি ফেরত দেওয়ার চেয়ে বেশি সময় লাগতে পারে। এটি প্রায় পেতে, আমরা মাঝারি ফলাফল গ্রহণ করে সার্ভার একাধিকবার পোল করি। এটি আমাদের ডিভাইস এবং সার্ভারের মধ্যে প্রকৃত পার্থক্যের 10ms এর মধ্যে পায়।

ত্বরণ/মন্দন

প্লেয়ার 1 যখন স্ক্রীন টিপে বা রিলিজ করে, তখন ত্বরণ ইভেন্টটি সার্ভারে পাঠানো হয়। একবার প্রাপ্ত হলে, সার্ভারটি তার বর্তমান টাইমস্ট্যাম্প যোগ করে এবং তারপর সেই ডেটাটি অন্য প্রতিটি প্লেয়ারের কাছে পাস করে।

যখন একটি ডিভাইস দ্বারা একটি "এক্সিলারেট অন" বা "এক্সিলারেট অফ" ইভেন্ট গ্রহণ করা হয়, তখন আমরা সেই বার্তাটি পেতে কতক্ষণ সময় নেয় তা খুঁজে বের করতে সার্ভার অফসেট (উপরে গণনা করা) ব্যবহার করতে পারি। এটি দরকারী, কারণ প্লেয়ার 1 20ms এর মধ্যে বার্তা পেতে পারে, কিন্তু প্লেয়ার 2 এটি পেতে 50ms সময় নিতে পারে৷ এর ফলে গাড়ি দুটি ভিন্ন জায়গায় থাকবে কারণ ডিভাইস 1 তাড়াতাড়ি ত্বরণ শুরু করবে।

আমরা ইভেন্টটি গ্রহণ করতে এবং ফ্রেমে রূপান্তর করতে সময় নিতে পারি। 60fps-এ, প্রতিটি ফ্রেম হল 16.67ms—তাই যে ফ্রেমগুলি মিস হয়েছে তার জন্য আমরা গাড়ির আরও বেগ (ত্বরণ) বা ঘর্ষণ (মন্দন) যোগ করতে পারি।

var frames = time / 16.67;
var onScreen = this.isOnScreen() && time < 75;

for (var i = 0; i < frames; i++) {
  if (onScreen) {
    _velocity.length += _throttle * Math.round(frames * .215);
  } else {
    _this.render();
  }
}}

উপরের উদাহরণে, যদি প্লেয়ার 1-এর স্ক্রিনে গাড়ি থাকে এবং বার্তাটি পেতে সময় লাগে 75ms এর কম, তাহলে এটি গাড়ির গতিবেগ সামঞ্জস্য করবে, পার্থক্য তৈরি করতে এটির গতি বাড়াবে৷ যদি ডিভাইসটি স্ক্রিনে না থাকে বা বার্তাটি খুব বেশি সময় নেয়, তাহলে এটি রেন্ডার ফাংশন চালাবে এবং আসলে গাড়িটিকে যেখানে থাকা দরকার সেখানে লাফিয়ে দেবে৷

সিঙ্ক করা গাড়ি রাখা

এমনকি ত্বরণে বিলম্বের জন্য হিসাব করার পরেও, গাড়িটি এখনও সিঙ্ক থেকে বেরিয়ে আসতে পারে এবং একসাথে একাধিক স্ক্রিনে উপস্থিত হতে পারে; বিশেষ করে যখন এক ডিভাইস থেকে অন্য ডিভাইসে রূপান্তরিত হয়। এটি প্রতিরোধ করার জন্য, সমস্ত স্ক্রীন জুড়ে গাড়িগুলিকে ট্র্যাকে একই অবস্থানে রাখার জন্য আপডেট ইভেন্টগুলি ঘন ঘন পাঠানো হয়।

যুক্তি হল যে, প্রতি 4টি ফ্রেমে, যদি গাড়িটি স্ক্রিনে দৃশ্যমান হয়, সেই ডিভাইসটি প্রতিটি ডিভাইসে তার মান পাঠায়। যদি গাড়িটি দৃশ্যমান না হয়, অ্যাপটি প্রাপ্ত মানগুলির সাথে আপডেট করে এবং তারপরে আপডেট ইভেন্টটি পেতে যে সময় লেগেছিল তার উপর ভিত্তি করে গাড়িটিকে এগিয়ে নিয়ে যায়।

this.getValues = function () {
  _values.p = _position.clone();
  _values.r = _rotation;
  _values.e = _elapsed;
  _values.v = _velocity.length;
  _values.pos = _this.position;

  return _values;
}

this.setValues = function (val, time) {
  _position.x = val.p.x;
  _position.y = val.p.y;
  _rotation = val.r;
  _elapsed = val.e;
  _velocity.length = val.v;

  var frames = time / 16.67;

  for (var i = 0; i < frames; i++) {
    _this.render();
  }
}

উপসংহার

আমরা রেসারের ধারণাটি শোনার সাথে সাথেই আমরা জানতাম যে এটি একটি বিশেষ প্রকল্প হওয়ার সম্ভাবনা রয়েছে। আমরা দ্রুত একটি প্রোটোটাইপ তৈরি করেছি যা আমাদেরকে কীভাবে লেটেন্সি এবং নেটওয়ার্ক পারফরম্যান্স কাটিয়ে উঠতে হয় তার মোটামুটি ধারণা দিয়েছে। এটি একটি চ্যালেঞ্জিং প্রজেক্ট ছিল যা আমাদের গভীর রাত এবং দীর্ঘ সপ্তাহান্তে ব্যস্ত রাখত, কিন্তু যখন গেমটি আকার নিতে শুরু করে তখন এটি একটি দুর্দান্ত অনুভূতি ছিল। শেষ পর্যন্ত, আমরা শেষ ফলাফল নিয়ে খুব খুশি। Google ক্রিয়েটিভ ল্যাবের ধারণাটি একটি মজাদার উপায়ে ব্রাউজার প্রযুক্তির সীমাবদ্ধতাকে ঠেলে দিয়েছে এবং ডেভেলপার হিসেবে আমরা এর বেশি কিছু চাইতে পারিনি।