Engine-এ JavaScript

Engine-এর স্ক্রিপ্ট API: lifecycle, কো-অর্ডিনেট ফাঁদ, একটি অবজেক্ট তৈরির তিনটি উপায়, এবং দেয়ালের মধ্য দিয়ে না হেঁটে কীভাবে একটাকে সরাতে হয়।

একনজরে#

প্রতিটি entity একটি JavaScript স্ক্রিপ্ট বহন করতে পারে। এটি ব্রাউজারে চলে, আপনার ট্যাবে, লাইভ সিনের বিরুদ্ধে, আর এটাই canonical স্ক্রিপ্টিং API: BASIC ডায়ালেক্ট, এবং এডিটরের Python, C# ও Rust ভিউ, সবগুলোই একই surface-এ কম্পাইল হয়।

সম্পূর্ণ তালিকাটি হলো API রেফারেন্স: 50টি namespace-এ 538টি member, প্রতিটির নিজস্ব পাতা, একটি সার্চসহ। এই পাতাটি হলো সেটা পড়ার আগে যা জানা দরকার।

Lifecycle#

function start() {}                     // once, when the entity starts
function update(dt) {}                  // every frame, dt in seconds
function fixedUpdate(dt) {}             // fixed timestep, for physics
function onCollisionEnter(other) {}     // first frame of contact
function onCollisionStay(other) {}      // every frame while touching
function onCollisionExit(other) {}      // contact just ended
function onTriggerEnter(other) {}       // entered a trigger volume
function onTriggerExit(other) {}
function onMessage(msg, data, senderId) {}   // entity-to-entity messaging
function onAnimationEvent(name, data) {}
function onDestroy() {}
function main() {}                      // console-game entry, async allowed

deltaTime, time, entityId এবং entity হলো গ্লোবাল; getComponent(type) স্ক্রিপ্টটির মালিক entity থেকে একটি কম্পোনেন্ট পড়ে।

Collision এবং trigger 2D ও 3D দুটোতেই ফায়ার করে, আর প্রতিটি পাশ নিজস্ব contact normal পায়: other.contact.normal other entity থেকে এটির দিকে নির্দেশ করে। এটাই একটি স্ক্রিপ্টকে "আমি এর উপর নেমেছি" আর "এটা আমার উপর নেমেছে" এই দুটোর পার্থক্য বলতে দেয়, একটি প্ল্যাটফর্মার স্টম্প পড়ে other.contact.normal.y > 0.5

দুটো কো-অর্ডিনেট ফাঁদ#

দুটোই আসল ডিবাগিং সময় খরচ করে, আর কোনোটাই জোরে ব্যর্থ হয় না

Mouse: page বনাম canvas#

input.mousePosition page কো-অর্ডিনেটে। scene.pick() canvas কো-অর্ডিনেট চায়। একটার বদলে অন্যটা দিলে এটা মিস করে না, এটা এমন একটা পয়েন্টে আঘাত করে যা উইন্ডোর মধ্যে ভিউপোর্টের অবস্থান দিয়ে অফসেট করা, তাই একটা click-to-move হিরো এমন কোথাও হাঁটে যেটা বিশ্বাসযোগ্য কিন্তু ভুল।

কোনো আর্গুমেন্ট ছাড়া scene.pick() কল করুন (এটা cursor-কে লক্ষ্য করে), অথবা সংখ্যাগুলো দরকার হলে input.mouseViewport ব্যবহার করুন।

Virtual control canvas পিক্সেলে#

input.addVirtualJoystick(id, x, y, size) এবং input.addVirtualButton(...) তাদের কেন্দ্র mouseViewport-এর মতোই একই স্পেসে বসায়। একটি joystick তার radial dead zone-এর পরে একটি normalised X/Y ফেরত দেয়; একটি button আলাদাভাবে held, pressed-this-frame এবং released-this-frame প্রকাশ করে। তাদের pointer ক্যাপচার ও consume করা হয়, তাই একটি কন্ট্রোলের উপর একটি touch একই সাথে একটি raw canvas action হিসেবেও জানানো হয় না।

ডিফল্ট joystick ইউনিফাইড। input.getJoystickX/Y() on-screen stick, একটি ফিজিক্যাল gamepad, অথবা কীবোর্ড (WASD/arrow) থেকে একটি সম্পূর্ণ vector বেছে নেয়, যেটা সোর্স সবচেয়ে শক্তিশালী। তাই একটি নিষ্ক্রিয় সংযুক্ত পad কীবোর্ড মুভমেন্ট বন্ধ করে দেয় না। এর Y অক্ষ 2D স্ক্রিন স্পেস অনুসরণ করে: উপরে -1

একটি অবজেক্ট তৈরির তিনটি উপায়, আর এরা একে অপরের বদলি নয়#

এটা কী তৈরি করেকতক্ষণ থাকে
scene.createEntity(def)একটি authored entity: এটা Scene Graph ও Inspector-এ দেখা যায়প্রজেক্টের জন্য, এটা সংরক্ষিত হয়
game.spawn(type, pos)একটি খালি প্রিমিটিভ: কোনো health, AI বা behaviour নেইplay সেশনের জন্য
game.spawnFrom(template, pos, opts)একটি পুরো entity-র কপি: component, physics body, tag, layer, scale এবং compiled behaviour স্ট্যাকplay সেশনের জন্য

game.spawn ধ্বংসাবশেষ ও প্রোটোটাইপের জন্য ঠিক আর শত্রুর জন্য অকেজো: এটা দিয়ে চালানো একটি spawner নিষ্ক্রিয় বল তৈরি করে। spawnFrom যে template কপি করে সেটা একটি সাধারণ entity যা আপনি এডিটরে কম্পোজ করেন এবং hierarchy-র চোখ দিয়ে বন্ধ করেন: আলাদা কোনো ফরম্যাট নেই, দ্বিতীয় কোনো এডিটর নেই, তাই আপনি যা দেখেন তা-ই দেখা যায়।

// Once, at start: a missing template should say so, not fail every wave silently
if (!game.hasTemplate('Skeleton')) console.warn('no Skeleton template');

const mob = game.spawnFrom('Skeleton', { x, y, z }, { tag: 'Enemy' });

template একটি id, একটি নাম বা একটি tag গ্রহণ করে। play-এর সময় তৈরি কপিগুলো play-scoped, তাই stop() সেগুলো সরিয়ে দেয় আর এডিট সিন কখনও দূষিত হয় না।

একটি entity বন্ধ করা মানে hidden এবং silent দুটোই: node disabled, script বন্ধ, physics body আর সাড়া দিচ্ছে না। শুধু লুকানোতে একটি অদৃশ্য শত্রু আপনাকে আঘাত করতেই থাকবে। game.setEnabled(id, on) একটি স্ক্রিপ্ট থেকে এটা চালায়, eye আইকন hierarchy থেকে চালায়।

কিছু সরানো: একটা body ঠেলুন, টেলিপোর্ট করবেন না#

প্রতি ফ্রেমে transform.position লেখা দেয়াল, অন্যান্য দানব এবং মেঝের মধ্য দিয়ে হাঁটে। এর বদলে velocity চালান, তবে শুধু তখনই যখন চালানোর মতো কিছু থাকে:

if (physics.hasBody) {
  const v = physics.getVelocity();
  physics.setVelocity({ x: vx, y: v.y, z: vz });   // pushes a real body
} else {
  transform.position.x += vx * dt;                 // legitimate for a flyer
}

কোনো body ছাড়া physics.setVelocity এমন একটা field ভরে দেয় যা কেউ integrate করে না, আর entity একদম থেমে যায়। এই কারণেই physics.hasBody আছে, আর এই কারণেই catalogue-এর প্রতিটি moving behaviour একটা উত্তর ধরে নেওয়ার বদলে প্রশ্নটা জিজ্ঞেস করে। থামা মানে velocity বাতিল করা: কিছুই না করলে body ভেসে চলতে থাকে।

একটা position লেখা এখন সত্যিই একটা physics body সরায়। Havok transform চালায়, উল্টোটা কখনও নয়, তাই একটা dynamic body-তে transform.position অ্যাসাইন করা আগে পরের step-এ overwrite হয়ে যেত: একটা physical entity-তে প্রতিটি টেলিপোর্ট নীরবে ব্যর্থ হতো। position proxy body-টাকে আবার sync করে, যেভাবে rotation proxy সবসময় করত।

Pathfinding একটা স্ন্যাপশট#

pathfinding.createGrid() একবার একটি গ্রিড raycast করে, যেখানে এটা কল হয়েছিল সেখানে কেন্দ্রীভূত। সেই বাক্স ছেড়ে গেলে null ফেরত দেয়, আর বেশিরভাগ caller তখন কোনো কথা না বলেই দেয়ালের মধ্য দিয়ে একটা সরলরেখায় fallback করে।

গ্রিডটা ডিফল্টভাবে অনুরোধ করা trip-এর উপর পুনরায় কেন্দ্রীভূত হয়। setAutoRecenter(false) একটি fixed world-এ এটা থেকে বেরিয়ে আসে, যেখানে rebuild করার খরচ একটা raycast pass বৃথা যাওয়া।

console.log লগ করে। Print আঁকে।#

কনসোল হলো console.log / warn / error, অন্য যেকোনো জায়গার মতোই। BASIC-এর Print স্ক্রিনে আঁকে আর প্রতি ফ্রেমে কল হয়, দুটোই ইচ্ছাকৃতভাবে আলাদা কমান্ড, আর এদের গুলিয়ে ফেলাই একটা কনসোলকে একটা ফ্রেম লুপ দিয়ে ভরিয়ে দেয়।

যেখানে একটা স্ক্রিপ্ট চলে#

এডিটরে এবং একটি web build-এ, স্ক্রিপ্ট ব্রাউজারে চলে। একটি নেটিভ ডেস্কটপ বা মোবাইল build-এ, এগুলো অপরিবর্তিতভাবে একটি এমবেডেড QuickJS রানটাইমে চলে, একই সোর্স, কোনো port নয়।

আপনি এডিট করার সময় স্ক্রিপ্ট ব্রাউজারের নিজস্ব ডেটাবেসে সংরক্ষিত থাকে, তাই একটি সংরক্ষিত না হওয়া buffer একটা reload-এ টিকে যায়; বাকি সবকিছুর সাথে এগুলো প্রজেক্টে সংরক্ষিত হয়।

সীমাবদ্ধতা ও সাধারণ সমস্যা#

  • আর্গুমেন্টসহ scene.pick() canvas কো-অর্ডিনেট চায়। উপরে দেখুন; এটাই সবচেয়ে সাধারণ ভুল-জায়গার bug।
  • game.spawn একটা primitive তৈরি করে, কোনো চরিত্র নয়। spawnFrom ব্যবহার করুন।
  • গেমের মাঝখানে জন্ম নেওয়া entity-র জন্য একটা body তৈরি করা হয়। Physics আগে শুধু Play-তে তৈরি হতো, তাই পরে spawn হওয়া যেকোনো কিছু নীরবে world-এর মধ্য দিয়ে পড়ে যেত, যখন তার হাতে-বসানো প্রতিবেশীরা ঠিকভাবে আচরণ করত।
  • আট ধাপ হলো AI সতীর্থের সীমা, আপনার নয়। একটা স্ক্রিপ্ট লুপের এমন কোনো সীমা নেই; একটা পলাতক while অন্য যেকোনো জায়গার মতোই ফ্রেম আটকে দেবে।
  • বাকি চারটি ভাষার ভিউ এটা থেকেই transpile করা। একটা Python বা Rust ভিউ এডিট করে ফিরে আসা সংরক্ষিত JavaScript-এর মধ্য দিয়ে যায়, কেন সেটার জন্য দেখুন BASIC পাতা