സാങ്കേതികമായ കാരണങ്ങളാൽ ഒരു കളി തോൽക്കുന്നത് കളിക്കാർക്ക് ഒട്ടും ഇഷ്ടമല്ല. പ്ലാറ്റ്‌ഫോം വ്യക്തമായിരുന്നു, ടൈമിംഗ് കൃത്യമായിരുന്നു, എന്നാൽ അവർ ഒരു തെറ്റ് ചെയ്തതുകൊണ്ടല്ല, മറിച്ച് ബ്രൗസർ ടാബ് ഫോക്കസ് (focus) നഷ്ടപ്പെട്ടതുകൊണ്ട് ഗെയിം അവരെ തോൽപ്പിച്ചു.

ഞാൻ ഇത് നേരിട്ട് അനുഭവിച്ചത് Solstice Leap എന്ന ഗെയിമിലായിരുന്നു. ഒരു സിംഗിൾ മെക്കാനിക് അടിസ്ഥാനമാക്കിയുള്ള ഒരു Three.js ആർക്കേഡ് ഗെയിമാണിത്: ചാടാൻ തയ്യാറെടുക്കുന്നതിനായി (charge) ഒരു ബട്ടൺ അമർത്തിപ്പിടിക്കുക, തുടർന്ന് അത് വിടുമ്പോൾ ചാടുക. പ്ലേടെസ്റ്റുകൾക്കിടയിൽ, ഞാൻ വളരെ അസ്വസ്ഥമാക്കുന്ന ഒരു പാറ്റേൺ ശ്രദ്ധിച്ചു. ചാർജ്ജ് തയ്യാറായിക്കൊണ്ടിരിക്കുമ്പോൾ ആരെങ്കിലും ഒരു മെസ്സേജിന് മറുപടി നൽകാൻ Alt-Tab ഉപയോഗിക്കുകയോ അല്ലെങ്കിൽ മറ്റൊരു ടാബിൽ ക്ലിക്ക് ചെയ്യുകയോ ചെയ്താൽ, വിൻഡോ വീണ്ടും ഫോക്കസ് ചെയ്യുമ്പോഴോ അല്ലെങ്കിൽ ഫോക്കസ് നഷ്ടപ്പെട്ട ഉടൻ തന്നെയോ കഥാപാത്രം ശൂന്യതയിലേക്ക് തെറിച്ചുപോകുന്നു. ഒരു സാധാരണ ഓപ്പറേറ്റിംഗ് സിസ്റ്റം തടസ്സം (interruption), ബോധപൂർവ്വം ബട്ടൺ വിട്ടതായി ഗെയിം തെറ്റായി വ്യാഖ്യാനിക്കുകയായിരുന്നു. കളി അന്യായമായി അവസാനിച്ചു. കൺട്രോളുകളിലുള്ള വിശ്വാസം നഷ്ടപ്പെട്ടു.

മൂലകാരണം: ഒരു ഇവന്റ് രണ്ട് ജോലികൾ ചെയ്യുന്നു

ഈ ബഗ് വളരെ സൂക്ഷ്മമായിരുന്നുവെങ്കിലും നേരിട്ടുള്ളതായിരുന്നു. യഥാർത്ഥ ഇൻപുട്ട് ലെയറിൽ, വിൻഡോയുടെ blur ഇവന്റുമായി (blur event) നേരിട്ട് ബന്ധിപ്പിച്ചാണ് ചാടാനുള്ള ലോജിക് (jump release logic) എഴുതിയിരുന്നത്:

window.addEventListener("blur", releaseCharge);

ഇത് നോക്കുമ്പോൾ യുക്തിസഹമായി തോന്നാം. കളിക്കാരൻ ഒരു കീ അല്ലെങ്കിൽ പോയിന്റർ അമർത്തിപ്പിടിക്കുകയായിരുന്നു; ഇപ്പോൾ അത് നിലച്ചു. എന്നാൽ ഒരു blur ഇവന്റ് എന്നത് ഒരു ഇൻപുട്ട് ഇവന്റല്ല. അതൊരു വിൻഡോ മാനേജ്‌മെന്റ് സിഗ്നലാണ്. കളിക്കാരൻ ടാബുകൾ മാറുന്നതിനോ, വിൻഡോ മിനിമൈസ് ചെയ്യുന്നതിനോ, ഒരു എക്സ്റ്റേണൽ മോണിറ്ററിൽ ക്ലിക്ക് ചെയ്യുന്നതിനോ, അല്ലെങ്കിൽ ഒരു സിസ്റ്റം നോട്ടിഫിക്കേഷൻ ഫോക്കസ് എടുക്കുന്നതിനോ കാരണമാകുന്ന ബ്രൗസർ ടാബ് ഓപ്പറേറ്റിംഗ് സിസ്റ്റം ഫോക്കസ് നഷ്ടപ്പെടുമ്പോഴാണ് ഇത് സംഭവിക്കുന്നത്. ഈ പ്രവൃത്തികളൊന്നും "എനിക്ക് എന്റെ കഥാപാത്രത്തെ ചാടさせണം" എന്ന് അർത്ഥമാക്കുന്നില്ല. പകരം "ഞാൻ ഗെയിമിന് പുറത്തുള്ള മറ്റെന്തോടാണ് ഇടപഴകുന്നത്" എന്നാണ് അവ അർത്ഥമാക്കുന്നത്.

blur ഇവന്റിനെ releaseCharge-ലേക്ക് വഴിതിരിച്ചുവിട്ടതിലൂടെ, ഗെയിം രണ്ട് തികച്ചും വ്യത്യസ്തമായ കാര്യങ്ങളെ ഒന്നായി കണ്ടു: ബോധപൂർവ്വമായ ഒരു നിർത്തൽ (കളിക്കാരൻ ബട്ടൺ വിടുന്നു), ഒരു ബാഹ്യ തടസ്സം (ബ്രൗസർ ഇനി ആക്റ്റീവ് വിൻഡോ അല്ല). releaseCharge നിലവിലെ ചാർജ് അവസ്ഥയെ അടിസ്ഥാനമാക്കി ചാട്ടത്തിന്റെ ശക്തി കണക്കാക്കുകയും ഉടൻ തന്നെ വേഗത (velocity) നൽകുകയും ചെയ്യുന്നതുകൊണ്ട്, ചാർജ്ജ് ചെയ്യുന്നതിനിടയിൽ ഫോക്കസ് നഷ്ടപ്പെട്ടാൽ അത്രയും ശക്തിയോടെ കഥാപാത്രം ചാടിപ്പോകുന്നു. കളിക്കാരൻ തിരികെ വരുമ്പോൾ കഥാപാത്രം മരിച്ച നിലയിലോ അല്ലെങ്കിൽ തങ്ങൾ ഉദ്ദേശിക്കാത്ത ഒരു നീക്കത്തിലൂടെ പുരോഗതി നഷ്ടപ്പെട്ട നിലയിലോ കാണപ്പെടുന്നു.

Three.js ഡെവലപ്പർമാർ അറിയേണ്ട ബ്രൗസർ യാഥാർത്ഥ്യങ്ങൾ

Three.js നിങ്ങൾക്ക് ശക്തമായ ഒരു 3D ക്യാൻവാസ് നൽകുന്നുണ്ടെങ്കിലും, ഇൻപുട്ടുകൾ ഇപ്പോഴും DOM വഴിയാണ് വരുന്നത്. ഈ വ്യത്യാസം വളരെ പ്രധാനമാണ്. സ്പേസ് ബാർ അമർത്തിപ്പിടിക്കുന്നത് ചാടാൻ തയ്യാറെടുക്കാനാണെന്ന് ബ്രൗസറിന് സ്വതസിദ്ധമായി അറിയില്ല. ഒരു കീ അമർത്തിപ്പിടിച്ചിരിക്കുന്നു എന്ന് മാത്രമേ അതിന് അറിയാവൂ. ഫോക്കസ് ഡോക്യുമെന്റിൽ നിന്ന് മാറുമ്പോൾ, അമർത്തിപ്പിടിച്ചിരിക്കുന്ന ഓരോ കീയ്ക്കും പകരം ഒരു keyup ഇവന്റ് ബ്രൗസർ സ്വയം നിർമ്മിക്കില്ല. പകരം, വിൻഡോ ഇല്ലാതായി എന്ന് അത് നിങ്ങളെ അറിയിക്കുന്നു. ഫോക്കസ് ഇല്ലാതിരിക്കുന്നത് ഇൻപുട്ട് ഇല്ലാതിരിക്കുകയാണെന്ന് നിങ്ങളുടെ ഗെയിം ലോജിക് കരുതിയാൽ, അപ്രതീക്ഷിതമായ നീക്കങ്ങൾ (phantom actions) സംഭവിക്കും.

വില്ല് വലിക്കുന്നത്, വാഹനം വേഗത്തിലാക്കുന്നത്, ചാർജ്ജ് ചെയ്ത മന്ത്രങ്ങൾ ഉപയോഗിക്കുന്നത്, അല്ലെങ്കിൽ സ്റ്റാമിന ഉപയോഗിച്ച് ഓടുന്നത് എന്നിങ്ങനെ എല്ലായിടത്തും കാണപ്പെടുന്ന ചാർജ്-അപ്പ് മെക്കാനിക്സുകളിൽ (charge-up mechanics) ഈ വ്യത്യാസം വളരെ പ്രധാനമാണ്. സമയത്തിനനുസരിച്ച് സ്റ്റേറ്റ് (state) ആക്യുമുലേറ്റ് ചെയ്യുന്ന ഏതൊരു നീക്കവും ഇത്തരത്തിൽ തെറ്റായി വ്യാഖ്യാനിക്കപ്പെടാൻ സാധ്യതയുണ്ട്. നേറ്റീവ് ആപ്ലിക്കേഷനുകൾ ഫോക്കസ് നഷ്ടപ്പെടുമ്പോൾ സിമുലേഷൻ നിർത്തിവെക്കാറുണ്ട്. ബ്രൗസർ ഗെയിമുകൾക്കും അങ്ങനെ ചെയ്യാവുന്നതാണ്, എന്നാൽ ഗെയിം പ്രവർത്തിച്ചുകൊണ്ടിരിക്കുകയാണെങ്കിൽ പോലും, സിസ്റ്റം തടസ്സങ്ങളെ കളിക്കാരന്റെ കമാൻഡുകളിൽ നിന്ന് നിങ്ങൾ വേർതിരിക്കണം.

ഉദ്ദേശ്യത്തെ തടസ്സങ്ങളിൽ നിന്ന് വേർതിരിക്കുക

ചാർജിംഗ് സ്റ്റേറ്റിൽ നിന്നുള്ള എക്സിറ്റ് പാത്തിനെ (exit path) രണ്ട് വ്യത്യസ്ത വഴികളായി തിരിക്കുകയാണ് ഇതിനുള്ള പരിഹാരം. ഒന്ന് ബോധപൂർവ്വമായ ഇൻപുട്ടുകൾ കൈകാര്യം ചെയ്യുന്നു. മറ്റൊന്ന് യഥാർത്ഥ ലോകം ഇടപെടുന്നത് തടയാൻ സഹായിക്കുന്നു.

ബോധപൂർവ്വമായ റിലീസുകൾ (Deliberate releases)pointerup, keyup എന്നിവ ചാട്ടം നടപ്പിലാക്കുന്നു. ഇവ കളിക്കാരൻ നൽകുന്ന നേരിട്ടുള്ള സിഗ്നലുകളാണ്.

ഫോക്കസ് നഷ്ടപ്പെടുന്ന ഇവന്റുകൾ (Focus loss events)blur, pointercancel, ഡോക്യുമെന്റ് മറയ്ക്കപ്പെടുമ്പോൾ സംഭവിക്കുന്ന visibilitychange എന്നിവ ഇപ്പോൾ cancelCharge എന്ന പ്രത്യേക ഫംഗ്ഷൻ പ്രവർത്തിപ്പിക്കുന്നു.

cancelCharge എന്നത് ഒരു പരിഷ്കരിച്ച റിലീസ് അല്ല. അതൊരു ഹാർഡ് റീസെറ്റ് (hard reset) ആണ്. ഇത് ആക്യുമുലേറ്റ് ചെയ്ത ചാർജ് ഫോഴ്സിനെ പൂജ്യത്തിലേക്ക് കുറയ്ക്കുന്നു, കളിക്കാരന്റെ വിഷ്വൽ സ്കെയിൽ അതിന്റെ ഡിഫോൾട്ട് അവസ്ഥയിലേക്ക് മാറ്റുന്നു, സ്ക്രീനിലെ ചാർജ് മീറ്റർ പൂജ്യമാക്കുന്നു, കൂടാതെ ഗെയിമിനെ അതിന്റെ എയിമിംഗ് മോഡിലേക്ക് (aiming mode) തിരികെ എത്തിക്കുന്നു. ഏറ്റവും പ്രധാനമായി, ഇത് ലോഞ്ച് ട്രാജക്റ്ററി (launch trajectory) കോഡിനെ ബാധിക്കില്ല. അവിടെ വേഗത കണക്കാക്കലുകളോ ഫിസിക്സ് ഇംപൾസോ ചാട്ടമോ ഉണ്ടാവില്ല. ചാർജ് സുരക്ഷിതമായി ഇല്ലാതാകുന്നു.

പുതുക്കിയ രീതി ആശയപരമായി ഇപ്രകാരമാണ്:

window.addEventListener("blur", cancelCharge);

എന്നാൽ യഥാർത്ഥ ആർക്കിടെക്ചറൽ മാറ്റം എന്നത്, ചാർജിംഗ് എന്നത് രണ്ട് സാധ്യതകളുള്ള ഒരു സ്റ്റേറ്റ് (state) ആണെന്ന തിരിച്ചറിവാണ്. ശരിയായ രീതിയിൽ ബട്ടൺ വിടുമ്പോൾ, സ്റ്റേറ്റ് മെഷീൻ ചാർജ് ശതമാനം വിലയിരുത്തുകയും ചാട്ടത്തിന്റെ വേഗത കണക്കാക്കുകയും ചാട്ടത്തിനായുള്ള ആനിമേഷനിലേക്ക് മാറുകയും ചെയ്യുന്നു. ഒരു തടസ്സം ഉണ്ടാകുമ്പോൾ, സ്റ്റേറ്റ് മെഷീൻ അത് റദ്ദാക്കുകയും പഴയ അവസ്ഥയിലേക്ക് (idle) മടങ്ങുകയും ചെയ്യുന്നു. ഈ പാത്തുകൾ വേർതിരിച്ചു നിർത്തുന്നത് പാർശ്വഫലങ്ങൾ (side effects) ഒഴിവാക്കാൻ സഹായിക്കുന്നു.

നിങ്ങൾ pointercancel ശ്രദ്ധിക്കുകയും വേണം. പോയിന്റിംഗ് ഉപകരണത്തിൽ സിസ്റ്റം തലത്തിലുള്ള തടസ്സങ്ങൾ—ഉദാഹരണത്തിന് ടച്ച് സ്ക്രീനുകളിലെ palm rejection ജെസ്റ്ററുകൾ, ഒരു സിസ്റ്റം മെനു വിളിക്കുന്നത്, അല്ലെങ്കിൽ അസാധാരണമായ സാഹചര്യങ്ങളിൽ ഒരു പെൻ സമ്പർക്കം നഷ്ടപ്പെടുന്നത് എന്നിവ—കണ്ടെത്തുമ്പോൾ ബ്രൗസർ ഇത് ഡിസ്പാച്ച് ചെയ്യുന്നു. blur-ഉം pointercancel-ഉം ഒരുമിച്ച് ഉപയോഗിക്കുന്നത് ഡെസ്ക്ടോപ്പ് മൾട്ടിടാസ്കിംഗും മൊബൈൽ തടസ്സങ്ങളും ഒരുപോലെ പരിഹരിക്കാൻ സഹായിക്കുന്നു. visibilitychange ചേർക്കുന്നതിലൂടെ, വിൻഡോ ഒബ്‌ജക്റ്റിൽ blur സംഭവിക്കാതെ തന്നെ ഉപയോക്താവ് ടാബുകൾ മാറുന്ന സാഹചര്യം കൂടി ഉൾക്കൊള്ളാൻ സാധിക്കും; ചില ബ്രൗസറുകളിലും ഓപ്പറേറ്റിംഗ് സിസ്റ്റങ്ങളിലും ഇത് സംഭവിക്കാറുണ്ട്.

അതിർത്തി സാഹചര്യങ്ങൾ (Boundary Conditions) പരിശോധിക്കുന്നു

ഇൻപുട്ട് ബഗുകൾ പരിഹരിക്കുന്നതിന് സാധാരണ രീതികൾക്ക് (happy path) പുറത്തുള്ള പരിശോധനകൾ ആവശ്യമാണ്. ഒരു ടാബിൽ മാത്രം ശാന്തമായി ഗെയിം കളിച്ചുകൊണ്ട് ഇത്തരം പ്രശ്നങ്ങൾ കണ്ടെത്താൻ കഴിയില്ല. പുതിയ മാറ്റം ശരിയാണോ എന്ന് പരിശോധിക്കാൻ ഞാൻ രണ്ട് പ്രത്യേക സാഹചര്യങ്ങൾ പരീക്ഷിച്ചു.

ഒന്നാമതായി, ഞാൻ ഒരു ജമ്പ് (jump) ചാർജ് ചെയ്തുകൊണ്ടിരിക്കെ കീബോർഡ് ഉപയോഗിച്ച് ബ്രൗസർ ടാബുകൾ മാറ്റി ഒരു blur ഇവന്റ് നിർബന്ധപൂർവ്വം ഉണ്ടാക്കി. ഗെയിം ഉടൻ തന്നെ ചാർജിംഗ് മോഡിൽ നിന്ന് മാറി എയിമിംഗ് (aiming) മോഡിലേക്ക് മടങ്ങി. ജമ്പ് നടന്നില്ല. വേഗത (velocity) പ്രയോഗിക്കപ്പെട്ടില്ല. ചാർജ് മീറ്റർ തനിയെ ക്ലിയർ ആയി. രണ്ടാമതായി, ഞാൻ ഒരു സാധാരണ ചാർജ് ചെയ്ത ശേഷം ബോധപൂർവ്വം ബട്ടൺ റിലീസ് ചെയ്തു. ജമ്പ് മുമ്പത്തെപ്പോലെ തന്നെ കൃത്യമായ ആർക്കിലും (arc) ഫോഴ്സ് സ്കെലിംഗിലും (force scaling) നടന്നു. ഗെയിം ഫീൽ മാറ്റമില്ലാതെ തുടർന്നു; എഡ്ജ് കേസ് (edge case) മാത്രമാണ് പരിഹരിക്കപ്പെട്ടത്.

രണ്ട് രീതികളും സ്വതന്ത്രമായി നിലനിൽക്കേണ്ടതുണ്ട്. അപ്രതീക്ഷിതമായ ജമ്പുകൾ തടയുകയും എന്നാൽ ശരിയായ ജമ്പുകളെ മന്ദഗതിയിലാക്കുകയും ചെയ്യുന്ന ഒരു പരിഹാരം ശരിയായ പരിഹാരമല്ല—അതൊരു പുതിയ ബഗ് ആണ്. ബ്രൗസറിലെ അനിശ്ചിതത്വങ്ങളിൽ നിന്ന് സംരക്ഷിച്ചുകൊണ്ട് തന്നെ യഥാർത്ഥ മെക്കാനിക്സിന്റെ കൃത്യത നിലനിർത്തുക എന്നതായിരുന്നു ലക്ഷ്യം.

തുടർച്ചയായ ഇൻപുട്ടിനായുള്ള ഒരു മാതൃക

ഈ പ്രശ്നം പ്ലാറ്റ്‌ഫോമറുകളിൽ (platformers) മാത്രം ഒതുങ്ങുന്നതല്ല. തുടർച്ചയായ പ്രസ്സ് (press) ആശ്രയിക്കുന്ന ഏതൊരു Three.js ഗെയിമും ഇതിന് ഇരയാകാം. മൗസ് അമർത്തിപ്പിടിക്കുന്നത് വഴി ടെൻഷൻ കൂടുന്ന ഒരു ഫസ്റ്റ്-പേഴ്സൺ ഗ്രാപ്പിളിംഗ് ഹുക്ക് (grappling hook) അല്ലെങ്കിൽ ഒരു കീ അമർത്തിപ്പിടിക്കുന്നത് വഴി ബൂസ്റ്റ് ചാർജ് ചെയ്യുന്ന ഒരു റേസിംഗ് ഗെയിം എന്നിവയെക്കുറിച്ച് ചിന്തിക്കുക. നിങ്ങളുടെ ടെയർഡൗൺ ലോജിക് (teardown logic) ഒരു ബട്ടൺ റിലീസ് ഹാൻഡ്‌ലറിൽ (button release handler) മാത്രമാണെങ്കിൽ, ടാബ് മാറ്റം, OS നോട്ടിഫിക്കേഷനുകൾ, അല്ലെങ്കിൽ സ്ക്രീൻ ലോക്കുകൾ എന്നിവ പരിഗണിക്കാത്ത പക്ഷം, ഓപ്പറേറ്റിംഗ് സിസ്റ്റം നിങ്ങളുടെ ഗെയിം നിങ്ങൾക്കായി കളിക്കാൻ നിങ്ങൾ അനുവദിക്കുകയാണ് ചെയ്യുന്നത്.

ഇൻപുട്ട് ലെയർ മൂന്ന് വ്യക്തമായ അവസ്ഥകളോടെ (states) നിർമ്മിക്കുക എന്നതാണ് ഇതിന്റെ മാതൃക: ആക്റ്റീവ് ഇൻപുട്ട് (active input), റിലീസ്ഡ് ഇൻപുട്ട് (released input), കാൻസൽഡ് ഇൻപുട്ട് (cancelled input). ആക്റ്റീവ് ഇൻപുട്ട് ചാർജ് നിർമ്മിക്കുന്നു അല്ലെങ്കിൽ ആക്ഷൻ ആരംഭിക്കുന്നു. റിലീസ്ഡ് ഇൻപുട്ട് അത് സ്ഥിരീകരിക്കുന്നു. കാൻസൽഡ് ഇൻപുട്ട് അത് കൃത്യമായി അവസാനിപ്പിക്കുന്നു. ഒരു വിൻഡോ blur ഒരിക്കലും ഒരു റിലീസ് (release) ആയി തെറ്റിദ്ധരിക്കാൻ അനുവദിക്കരുത്. ബ്രൗസർ ഒരു ഹോസ്റ്റ് ആണ്, പ്ലെയർ അല്ല.

മനുഷ്യ സ്വഭാവം പരിഗണിക്കുക

ആളുകൾ ടാബുകൾ മാറ്റാറുണ്ട്. അവർ മെസ്സേജുകൾക്ക് മറുപടി നൽകുന്നു. അവർ രണ്ടാമത്തെ മോണിറ്ററിൽ ഒരു ഗൈഡ് നോക്കുന്നു. അവർക്ക് സ്ലാക്ക് (Slack) നോട്ടിഫിക്കേഷനുകൾ വരുന്നു. ഇവ വെറും എഡ്ജ് കേസുകളല്ല; ഒരു ബ്രൗസറിനുള്ളിലെ സാധാരണ പെരുമാറ്റങ്ങളാണ്. സാധാരണ മനുഷ്യരുടെ മൾട്ടിടാസ്കിംഗിനെ ശിക്ഷിക്കുന്ന ഒരു ബ്രൗസർ ഗെയിം ദുർബലമായി തോന്നും. ഫോക്കസ് നഷ്ടപ്പെടുന്നതിനെ ഒരു കമാൻഡ് എന്നതിലുപരി ഒരു കാൻസലേഷൻ (cancellation) ആയി പരിഗണിക്കുന്നതിലൂടെ, Solstice Leap ഇപ്പോൾ കളിക്കാർക്ക് തങ്ങൾ തയ്യാറാക്കിയ ജമ്പ് നഷ്ടപ്പെടാതെ തന്നെ ഒരു നിമിഷം മാറിനിൽക്കാൻ അനുവദിക്കുന്നു.

ഒരു blur ഇവന്റ് എന്നത് ഒരു റിലീസ് ഇവന്റ് അല്ല. ബ്രൗസർ മുറിയിൽ നിന്ന് പുറത്തുപോയി എന്ന് പറയുന്നത മാത്രമാണ് അത്. അതനുസരിച്ച് കോഡ് ചെയ്യുക, അപ്പോൾ നിങ്ങളുടെ കളിക്കാർക്ക് നിയന്ത്രണങ്ങളിൽ വിശ്വാസം വരികയും അവർക്ക് ആവശ്യമുള്ളപ്പോൾ കൃത്യമായി ജമ്പ് ചെയ്യാൻ സാധിക്കുകയും ചെയ്യും.