ब्राउज़र में एक ओपन वर्ल्ड बनाना, भाग 29: एक कंट्रोलर, कोई भी बॉडी
Oleg Sidorkin द्वारा, Cinevva के CTO और सह-संस्थापक
यहाँ नए हैं? सीरीज़ गाइड का इस्तेमाल करें। यह बताती है कि स्पाइक क्या होता है और सभी भागों को लिंक करती है।
भाग 28 ने घास और occlusion को कवर किया। अट्ठाईस भागों ने खड़े होने लायक एक वर्ल्ड बनाया: ऐसी terrain जिसे आप पढ़ सकते हैं, ऐसा पानी जिसमें आप तैर सकते हैं, ऐसी foliage जो क्षितिज तक टिकी रहती है, एक ऐसा server जो याद रखता है कि आपने क्या बदला। यह भाग उस चीज़ के बारे में है जो इस सबके बीच से होकर गुज़रती है, और यह पूरे engine का फल है, क्योंकि लक्ष्य एक प्लेयर कंट्रोलर नहीं है। यह एक ऐसा कंट्रोलर है जिसे इस बात की परवाह नहीं कि वह कौन सी बॉडी चला रहा है, उसके animations कहाँ से आए, या स्टीयरिंग पर कोई इंसान है या कोई AI। स्पाइक 58 movement को एक physics engine के ऊपर pluggable behaviors के एक stack के रूप में बनाता है जिसे locomotion के बारे में कुछ भी नहीं पता, फिर तीन अलग-अलग बॉडीज़ को उसकी एक ही कॉपी के ज़रिए चलाकर architecture को सिद्ध करता है। स्पाइक 59 उस कंट्रोलर की एक भी लाइन बदले बिना उस पर एक असली retargeted avatar लटका देता है। स्पाइक 60 एक अलग animation पैक लटका देता है जिसे retargeting की ज़रूरत ही नहीं, और clip चुनने वाले logic को बाहर खींचकर ऐसी चीज़ में बदल देता है जिसे आप सचमुच test कर सकते हैं।
एक physics engine जिसे चलने के बारे में कुछ नहीं पता
Spike 58 को नए टैब में खोलें ↗ · सोर्स देखें
डिज़ाइन का नियम सख्त है और यही पूरी बात है: capsule engine के पास कोई locomotion नहीं है। न walk, न run, न jump, न friction, न top-speed cap, यहाँ तक कि न gravity। यह बस एक kinematic capsule को terrain के सापेक्ष integrate करता है और इससे ज़्यादा कुछ नहीं। हर locomotion behavior, walk, slide, glide, climb, swim, crouch, stamina, engine के साथ रजिस्टर किए गए एक स्वतंत्र कंट्रोलर में रहता है। हर frame पर engine हर कंट्रोलर को tick करता है, हर एक से पूछता है कि क्या वह control चाहता है, और सबसे ऊँची priority वाले दावेदार को velocity लिखने देता है। Walk का कोई खास दर्जा नहीं है। यह बस सबसे कम priority वाला कंट्रोलर है जो हमेशा हाँ कहता है, इसलिए जब और कुछ नहीं चलता तो यही default है। एक कंट्रोलर छह छोटे functions का बना होता है: एक tick जो हर frame पर अपनी internal state को अपडेट करता है, तब भी जब वह active नहीं होता, एक pure wantsControl predicate जो frame पर दावा करता है, एक applyForces जिसे सिर्फ़ विजेता चलाता है और जो velocity लिखता है और अगर चाहे तो अपनी gravity लगाता है, साथ ही वैकल्पिक onEnter, onExit, और stateName। Swim अपने applyForces से ownsCollision: true लौटाता है ताकि terrain handling अपने हाथ में ले ले, क्योंकि वरना उसका buoyancy spring engine के foot-snap से लड़ता।
वे चीज़ें भी जो locomotion नहीं हैं, फिर भी उस contract से होकर रिसती हैं, और यही उस अगले विचार की वजह बना। architecture को सार्थक बनाने वाला हिस्सा channels हैं। एक पुराने single-channel डिज़ाइन में सब कुछ एक ही arbitration से होकर गुज़रता था, जिसका मतलब था कि एक stamina tracker या एक crouch stance को locomotion होने का दिखावा करना पड़ता था और फिर सिर्फ़ अपनी bookkeeping चलाने के लिए wantsControl के false लौटाने वाले एक hack से control से इनकार करना पड़ता था। यह fix कंट्रोलर्स को नामित channels में बाँट देता है जो स्वतंत्र रूप से arbitrate करते हैं और एक तय क्रम में लागू होते हैं: पहले resource, फिर stance, फिर locomotion। Resource पहले चलता है क्योंकि उसके writes, जैसे stamina decay, बाकी द्वारा पढ़े जाते हैं। Stance दूसरे नंबर पर चलता है क्योंकि crouch द्वारा capsule की height घटाना तब लागू होना चाहिए जब walk उसे पढ़कर top speed cap करे, उससे पहले। Locomotion आखिर में चलता है और per-frame velocity write उसके पास है। तो एक stamina observer और एक crouch modifier और एक active swim कंट्रोलर सब साफ़-साफ़ साथ रहते हैं, हर एक अपने channel में, और किसी कंट्रोलर को अपने बारे में झूठ नहीं बोलना पड़ता। डेमो इस सबको एक नंगे capsule से visualize करता है जो active कंट्रोलर के हिसाब से रंग बदलता है, ताकि आप arbitration होते देख सकें: एक खड़ी ढलान पर green walk नारंगी हो जाता है जैसे ही slide ले लेता है, झील में cyan हो जाता है जैसे swim जीतता है, हवा में cream हो जाता है जब आप glide टैप करते हैं।
एक engine, तीन बॉडीज़
"कोई locomotion नहीं" का असली test प्लेयर नहीं है। यह है कि क्या वही engine, बिना छुए, कुछ ऐसा चला सकता है जो प्लेयर है ही नहीं। createCapsuleEngine एक pure factory है जिसमें कोई module-level state नहीं, कोई singletons नहीं, और कोई per-instance side effects नहीं, इसलिए स्पाइक इसे तीन बार instantiate करता है। प्लेयर पूरे कंट्रोलर सेट वाला एक instance है। एक सवारी करने लायक घोड़ा दूसरा instance है जो सिर्फ़ एक walk कंट्रोलर रजिस्टर करता है, जो इस पूरे विचार को शब्दशः साकार करता है: एक बॉडी की movement की शब्दावली बस वही कंट्रोलर्स हैं जो आपने रजिस्टर किए, इसलिए घोड़ा समतल पर तेज़ है और शारीरिक रूप से न चढ़ सकता है, न तैर सकता है, न glide कर सकता है, क्योंकि वे कंट्रोलर्स कभी जोड़े ही नहीं गए। एक स्वायत्त NPC तीसरा instance है, जिसे प्लेयर के साथ-साथ हर frame पर tick किया जाता है, एक wander कंट्रोलर से चलाया जाता है जो अपना खुद का input बनाता है ताकि बॉडी खुद को steer करे, keyboard पर कोई हाथ नहीं। engine को कभी पता नहीं चलता कि उसकी एक बॉडी घोड़ा है या दूसरी AI-चालित है। वे सब अलग-अलग कंट्रोलर सूचियों वाला वही capsule integrator हैं।
Mounting वह एक हिस्सा है जो जानबूझकर engine के बाहर रहता है। प्लेयर और घोड़े के बीच control अदला-बदली का मतलब है दो engines में तालमेल बिठाना, और एक कंट्रोलर एक engine के अंदर चलता है और उस सीमा के पार नहीं देख सकता, इसलिए mount logic host स्तर पर बैठता है: यह एक छोटी state machine है जो तय करती है कि इस frame में कौन सा engine step किया जाए, दूसरे को फ्रीज़ कर देती है, सवार को तीन-चौथाई सेकंड के एक smoothstep के साथ काठी पर बैठा देती है, और camera को बताती है कि किस बॉडी को track करना है। engine factory को इसके बारे में कभी कुछ नहीं सुनना पड़ता। architecture यही लकीर खींचता है और बनाए रखता है। एक बॉडी से जुड़े behaviors कंट्रोलर्स हैं; बॉडीज़ के बीच तालमेल host का काम है, और उन्हें अलग रखना ही वह वजह है कि एक चौथी या चालीसवीं बॉडी की कोई नई लागत नहीं होगी।
बिना ब्राउज़र के tested
क्योंकि engine न किसी window तक पहुँचता है, न किसी document तक, और न Three.js तक, पूरी चीज़ headless चलती है। स्पाइक एक Node harness के साथ आता है जो terrain interface को mock करता है, scripted input के साथ engine को frame दर frame चलाता है, और परिणामी state पर assert करता है, ताकि वे regressions जिन्हें play-testing से पकड़ना दर्दनाक है, इसके बजाय एक script में पकड़ी जाएँ: jump-and-land transition, swim में घुसने और निकलने की thresholds, surface के समतल होने पर climb का auto-clear होना, stamina के घटने की दरें। वही input और timestep sequence दो बार feed करना और byte-identical output state की जाँच करना engine को deterministic साबित करता है, जो वह गुण है जिस पर एक networked build आखिरकार टिकेगी। harness ने एक regression पकड़ी जो याद रखने लायक है: walkable ज़मीन से एक ऐसी ढलान पर कदम रखना जो walk से ज़्यादा खड़ी थी, पहले slide कंट्रोलर को engage कर देता था और प्लेयर को वापस पहाड़ी पर उछाल देता था। इसका fix एक descent gate था। Sliding तभी शुरू होता है जब capsule सचमुच ढलान पर गिर रहा हो, इसलिए एक खड़ी सतह में horizontally चलना अब saaf-suthre तरीके से रुक जाता है बजाय slide करने के, और वह test जो "एक non-walkable ढलान में चलना प्लेयर को रोकता है" पर assert करता है, इसे fix रखता है।
कंट्रोलर्स को छुए बिना एक असली avatar plug करना
Spike 59 को नए टैब में खोलें ↗ · सोर्स देखें
स्पाइक 59 इस बात का test है कि क्या कंट्रोलर परत सचमुच बॉडी से decoupled है। यह रंगीन capsule को एक असली skinned कैरेक्टर से बदल देता है, 3MIKE FBX rig जिस पर Quaternius Universal Animation Library clips retargeted हैं, और कंट्रोलर्स बिल्कुल नहीं बदलते। सीवन एक अकेला string है। हर कंट्रोलर पहले से ही stateName के ज़रिए एक state नाम रिपोर्ट करता है, idle, walk, run, jump, fall, land, slide, glide, swim, swimIdle, और avatar परत उस नाम को एक alias table के ज़रिए एक retargeted clip से map करती है और switch पर crossfade करती है। Walk grounded plus vertical velocity plus horizontal speed से अपनी sub-state खुद dynamically चुनता है, इसलिए एक अकेला walk कंट्रोलर idle, walk, run, jump, fall, और land चलाता है, और avatar बस रिपोर्ट किए गए नाम का अनुसरण करता है। क्योंकि यह स्पाइक single-player है, avatar पुराने networked स्पाइक्स से wire-integer indirection और multi-character abstraction को छोड़ देता है और state नाम को सीधे एक clip से map करता है। इसका सबूत यह है कि capsule से rigged इंसान तक का पूरा visual upgrade locomotion code की शून्य लाइनें छूता है, जो ठीक वही है जो एक pluggable कंट्रोलर से मिलना चाहिए।
एक पैक जिसे retargeting की ज़रूरत नहीं, और एक picker जिसे आप test कर सकते हैं
Spike 60 को नए टैब में खोलें ↗ · सोर्स देखें
स्पाइक 60 एक तीसरी बॉडी plug करता है, Synty का POLYGON Base Locomotion पैक, और loader लगभग कुछ भी नहीं है। हर clip एक standalone FBX के रूप में आता है जो उसी Synty skeleton की एक embedded कॉपी plus एक baked animation साथ लाता है, और क्योंकि कैरेक्टर rig और हर clip समान bone नाम इस्तेमाल करते हैं, आप clip को fbx.animations[0] से उठाते हैं और बिना किसी retargeting library के सीधे कैरेक्टर के mixer पर चला देते हैं। Three.js animation track targets को object identity के बजाय bone नाम से resolve करता है, इसलिए matching rig के सापेक्ष बनाया गया एक Synty या Mixamo-शैली का पैक बस काम कर जाता है। यह पिछले स्पाइक के UAL path के साथ जानबूझकर किया गया विरोधाभास है, जिसे भारी-भरकम retargeting की ज़रूरत है क्योंकि source clips और target rig अलग-अलग skeletons के सापेक्ष बनाए गए थे। वही कंट्रोलर, वही state-name सीवन, उसके पीछे दो पूरी तरह अलग animation pipelines।
स्पाइक 60 का दूसरा आधा हिस्सा clip चुनने वाले logic को testable बनाना है। कौन सा clip चलाना है यह चुनना judgment thresholds से भरा है, और वह logic avatar परत के अंदर FBXLoader और DOM के बगल में दफ़न था जहाँ इसे exercise नहीं किया जा सकता था। स्पाइक picker को pure functions में निकालता है जो न Three.js को छूते हैं न window को: वे एक सादा player record (velocity, horizontal speed, facing, grounded, ground normal, impact velocity) और एक clip-action stand-in लेते हैं, और एक clip alias string लौटाते हैं। इससे thresholds नामित, asserted constants के रूप में रह पाते हैं। Jump walk कंट्रोलर की असली sprint threshold से aligned speed buckets के हिसाब से walking, running, और sprinting variants में बँट जाता है; landings impact velocity के हिसाब से soft, medium, और hard में बँट जाती हैं; uphill और downhill clip variants एक slope-projection dot product से fire होते हैं जिसमें लगभग सात डिग्री से नीचे एक flat-clip deadzone होता है; एक idle-to-locomotion bridge हमेशा आगे की ओर resolve करता है ताकि एक standing start कभी किसी clip को उल्टा न चलाए; और एक stop bridge एक न्यूनतम time-in-loop से gated है, क्योंकि Synty के foot-phase stop clips में लगभग एक सेकंड का authored deceleration होता है जो उस step पर चिपकाए जाने पर हास्यास्पद लगता है जो प्लेयर ने मुश्किल से लिया। एक छोटा state debouncer fall state को कुछ frames के लिए रोके रखता है ताकि एक सीवन के पार एक frame के लिए ज़मीन का संपर्क खोने पर animation झिलमिलाए नहीं। उस सबको rendering shell से बाहर खींचने का मतलब है कि नियमों को बिना GPU के unit test किया जा सकता है, वही headless अनुशासन जो engine को खुद स्पाइक 58 में मिला, जो locomotion के उस अनुभव के बीच का फ़र्क है जिसे आप अंदाज़ा लगाकर tune करते हैं और उस locomotion अनुभव के बीच जिसे आप तय कर सकते हैं।
इस अध्याय में संदर्भित तकनीक
एक locomotion-रहित physics engine। capsule engine एक kinematic body को terrain के सापेक्ष integrate करता है और इसके पास कोई walk, run, jump, friction, speed cap, या gravity नहीं है। हर behavior एक रजिस्टर किया गया कंट्रोलर है जो tick, wantsControl, applyForces, और वैकल्पिक onEnter/onExit/stateName expose करता है। Walk बस सबसे कम priority वाला हमेशा-हाँ default है, और एक कंट्रोलर terrain handling अपने हाथ में लेने के लिए ownsCollision: true लौटा सकता है (swim लौटाता है, ताकि उसका buoyancy spring foot-snap से न लड़े)।
स्वतंत्र arbitration channels। कंट्रोलर्स नामित channels (resource, stance, locomotion) में रजिस्टर होते हैं जो अलग-अलग arbitrate करते हैं और एक तय क्रम में लागू होते हैं, इसलिए stamina जैसे observers और crouch जैसे modifiers active locomotion के साथ साथ रहते हैं, बजाय control का दिखावा करके उससे इनकार करने के। Resource के writes (stamina) stance और locomotion द्वारा पढ़े जाते हैं; stance के writes (crouch capsule height) locomotion के speed cap द्वारा पढ़े जाते हैं; locomotion आखिर में चलता है और velocity write उसके पास है।
एक factory, कई बॉडीज़। createCapsuleEngine एक pure factory है जिसमें कोई singletons नहीं, इसलिए वही engine प्लेयर, एक walk-only सवारी करने लायक घोड़े, और synthetic input से खिलाए गए एक खुद-steer करने वाले NPC को चलाता है। एक बॉडी की movement शब्दावली ठीक उसका रजिस्टर किया गया कंट्रोलर सेट है, इसलिए घोड़ा चढ़ या तैर नहीं सकता क्योंकि वे कंट्रोलर्स कभी जोड़े ही नहीं गए। Mounting host स्तर पर रहता है, किसी कंट्रोलर में नहीं, क्योंकि यह दो engines में तालमेल बिठाता है जो एक-दूसरे के पार नहीं देख सकते। देखें GPU-driven LOD।
Headless, deterministic testing। engine न किसी window, document, या Three.js को छूता है, इसलिए एक Node harness इसे frame दर frame चलाता है और jump-and-land, swim thresholds, climb auto-clear, और stamina drain पर assert करता है। समान input दो बार replay करना और समान output की जाँच करना determinism साबित करता है, और एक regression test उस descent gate को pin करता है जो एक खड़ी सतह में एक horizontal walk को प्लेयर को पीछे की ओर slide करने से रोकता है।
Testable picker के साथ body-agnostic avatar binding। कंट्रोलर्स एक state-name string रिपोर्ट करते हैं और visual परत उसे एक clip से map करती है, इसलिए एक capsule को एक retargeted 3MIKE + UAL avatar से बदलना locomotion code की शून्य लाइनें छूता है। Synty POLYGON clips rig के साथ bone नाम साझा करते हैं और बिना retargeting के चलते हैं (Three.js tracks को bone नाम से bind करता है), UAL path की भारी-भरकम retargeting के उलट। clip picker को jump speed buckets, landing severity, slope-projection variants, एक हमेशा-आगे idle bridge, एक stop-bridge न्यूनतम-समय गार्ड, और एक fall-state debouncer के लिए नामित constants वाले pure functions में निकाला गया है, इसलिए locomotion का अनुभव अंदाज़े के बजाय unit-testable है।
भाग 29, कुल 30 में से। पिछला: भाग 28 - क्षितिज तक घास, और ऐसी ज़मीन जो खुद को छिपाती है अगला: भाग 30 - एक camera जो दीवारों का सम्मान करती है सीरीज़ गाइड: /hi/blog/2026-02-25-open-world-browser-series-guide