ખેલાડીઓને ટેકનિકલ કારણોસર પોતાનો રન (run) ગુમાવવો ગમતો નથી. પ્લેટફોર્મ સ્પષ્ટ હતું, સમય પણ સાચો હતો, અને પછી ગેમમાં તેઓ એટલા માટે હારી ગયા નહીં કે તેમણે કોઈ ભૂલ કરી, પરંતુ એટલા માટે કારણ કે બ્રાઉઝર ટેબનું ફોકસ (focus) ગુમાવ્યું હતું.

મેં આનો અનુભવ Solstice Leap માં રૂબરૂ કર્યો હતો, જે એક Three.js આર્કેડ ગેમ છે જે મેં એક સંતોષકારક મિકેનિકની આસપાસ બનાવી હતી: જમ્પ ચાર્જ કરવા માટે બટન દબાવી રાખો, અને પછી અંતર કાપવા માટે તેને છોડો. પ્લેટેસ્ટ દરમિયાન, મેં એક હેરાન કરતું પેટર્ન જોયું. જો કોઈ મેસેજનો જવાબ આપવા માટે Alt-Tab કરે અથવા જ્યારે ચાર્જ થઈ રહ્યો હોય ત્યારે બીજા ટેબ પર ક્લિક કરે, ત્યારે વિન્ડો પર પાછા ક્લિક કરતાની સાથે જ પાત્ર (character) ખાલી જગ્યામાં ફેંકાઈ જતું—અથવા ક્યારેક ફોકસ ગુમાવતાની સાથે જ. ગેમે ઓપરેટિંગ સિસ્ટમના સામાન્ય વિક્ષેપને જાણીજોઈને બટન છોડવા તરીકે ગણતરી કરી લીધી હતી. રન અન્યાયી રીતે પૂરા થઈ જતા હતા. કંટ્રોલ પરનો વિશ્વાસ ઘટી રહ્યો હતો.

મૂળ કારણ: એક જ ઇવેન્ટ બે કામ કરી રહી હતી

આ બગ (bug) સૂક્ષ્મ હતો પણ સીધો હતો. મૂળ ઇનપુટ લેયરમાં, કોડ જમ્પ રિલીઝ લોજિકને સીધું વિન્ડોના blur ઇવેન્ટ સાથે જોડી દેતો હતો:

window.addEventListener("blur", releaseCharge);

જો તમે ઉપરછલ્લી રીતે જુઓ તો આ યોગ્ય લાગે છે. ખેલાડી કી (key) અથવા પોઇન્ટર દબાવી રહ્યો હતો; હવે કંઈક અટકી ગયું. પરંતુ blur ઇવેન્ટ એ ઇનપુટ ઇવેન્ટ નથી. તે વિન્ડો મેનેજમેન્ટ સિગ્નલ છે. જ્યારે બ્રાઉઝર ટેબ ઓપરેટિંગ સિસ્ટમનું ફોકસ ગુમાવે છે ત્યારે તે ફાયર થાય છે, જે ત્યારે થઈ શકે છે જ્યારે ખેલાડી ટેબ બદલે છે, વિન્ડો મિનિમાઇઝ કરે છે, એક્સટર્નલ મોનિટર પર ક્લિક કરે છે, અથવા જ્યારે સિસ્ટમ નોટિફિકેશન ફોકસ લઈ લે છે. આમાંથી કોઈ પણ ક્રિયાનો અર્થ એ નથી કે "મારે મારા પાત્રને લોન્ચ કરવું છે." તેનો અર્થ એ છે કે "હું ગેમની બહારની કોઈ વસ્તુ સાથે વાતચીત કરી રહ્યો છું."

releaseCharge માં blur ને રૂટ કરીને, ગેમે બે તદ્દન અલગ ખ્યાલોને ભેગા કરી દીધા હતા: એક જાણીજોઈને કરેલો સ્ટોપ (ખેલાડી બટન છોડી દે છે) અને એક બાહ્ય વિક્ષેપ (બ્રાઉઝર હવે એક્ટિવ વિન્ડો નથી). કારણ કે releaseCharge વર્તમાન ચાર્જ સ્ટેટના આધારે જમ્પ ફોર્સની ગણતરી કરતું હતું અને તરત જ વેલોસિટી (velocity) લાગુ કરતું હતું, તેથી ચાર્જ દરમિયાન કોઈપણ ફોકસ લોસથી જે કંઈ પણ શક્તિ એકઠી થઈ હોય તેનાથી લોન્ચ થઈ જતું હતું. ખેલાડી જ્યારે પાછા આવે ત્યારે તેમને તેમનું પાત્ર મૃત અથવા તેમની પ્રગતિ એવા મૂવથી બરબાદ થયેલી જોવા મળતી જે તેમણે ક્યારેય આપ્યું નહોતું.

Three.js ડેવલપર્સ માટે બ્રાઉઝરની વાસ્તવિકતાઓ

Three.js તમને શક્તિશાળી 3D કેનવાસ આપે છે, પરંતુ ઇનપુટ હજુ પણ DOM દ્વારા વહે છે. તે વિભાજન મહત્વનું છે. બ્રાઉઝરને કુદરતી રીતે ખબર નથી હોતી કે સ્પેસબાર દબાવી રાખવાથી જમ્પ ચાર્જ થાય છે. તેને ફક્ત એટલું જ ખબર છે કે કી દબાવવામાં આવી છે. જ્યારે ફોકસ ડોક્યુમેન્ટ છોડી દે છે, ત્યારે બ્રાઉઝર દરેક દબાવેલી કી માટે આપમેળે keyup બનાવતું નથી. તેના બદલે, તે તમને જણાવે છે કે વિન્ડો જતી રહી છે. જો તમારું ગેમ લોજિક એવું માને છે કે ફોકસની ગેરહાજરી એટલે ઇનપુટની ગેરહાજરી, તો તમને ફેન્ટમ (phantom) એક્શન્સ જોવા મળશે.

આ તફાવત ખાસ કરીને ચાર્જ-અપ મિકેનિક્સ માટે મહત્વપૂર્ણ છે, જે બધે જ જોવા મળે છે: ધનુષ ખેંચવું, વાહન રિવ (rev) કરવું, ચાર્જ કરેલ સ્પેલ (spell) ફેંકવો, અથવા સ્ટેમિના વિન્ડ-અપ સાથે દોડવું. કોઈપણ સતત ક્રિયા જે સમય જતાં સ્ટેટ એકત્રિત કરે છે તે આ જ પ્રકારના ખોટા અર્થઘટન માટે સંવેદનશીલ છે. નેટિવ એપ્લિકેશન્સ ઘણીવાર ફોકસ લોસ પર આખી સિમ્યુલેશનને પોઝ (pause) કરી દે છે. બ્રાઉઝર ગેમ્સ પણ આવું કરી શકે છે, પરંતુ જો તમે તેને ચાલુ રાખો છો, તો પણ તમારે સિસ્ટમ ઇન્ટરપ્ટ્સને પ્લેયર કમાન્ડ્સથી અલગ કરવા જ પડશે.

ઈરાદાને વિક્ષેપથી અલગ પાડવા

આ સુધારા માટે ચાર્જિંગ સ્ટેટમાંથી બહાર નીકળવાના માર્ગને બે અલગ લેન (lanes) માં વહેંચવો જરૂરી હતો. એક લેન જાણીજોઈને આપેલા ઇનપુટને હેન્ડલ કરે છે. બીજી લેન જ્યારે વાસ્તવિક દુનિયા વચ્ચે આવે ત્યારે લાઈફ સપોર્ટ હેન્ડલ કરે છે.

જાણીજોઈને રિલીઝ કરવાpointerup અને keyup—હજુ પણ જમ્પ એક્ઝિક્યુટ કરે છે. આ ખેલાડીના આગળ વધવા માટેના સીધા સિગ્નલ છે.

ફોકસ લોસ ઇવેન્ટ્સblur, pointercancel, અને visibilitychange જ્યારે ડોક્યુમેન્ટ છુપાઈ જાય છે—હવે cancelCharge નામનું અલગ ફંક્શન ટ્રિગર કરે છે.

cancelCharge એ સુધારેલો રિલીઝ નથી. તે એક હાર્ડ રિસેટ (hard reset) છે. તે એકત્રિત થયેલ ચાર્જ ફોર્સને શૂન્ય કરી દે છે, પ્લેયરના વિઝ્યુઅલ સ્કેલને તેની ડિફોલ્ટ આઈડલ (idle) સ્ટેટમાં પાછો લાવે છે, સ્ક્રીન પરના ચાર્જ મીટરને ઝીરો કરે છે, અને ગેમને તેના એઇમિંગ મોડમાં પાછી લાવે છે. સૌથી મહત્વની વાત એ છે કે, તે લોન્ચ ટ્રેજેક્ટરી કોડને સ્પર્શતું નથી. તેમાં કોઈ વેલોસિટી કેલ્ક્યુલેશન નથી, કોઈ ફિઝિક્સ ઈમ્પલ્સ નથી, અને કોઈ લીપ (leap) નથી. ચાર્જ સુરક્ષિત રીતે અદૃશ્ય થઈ જાય છે.

અપડેટ કરેલું વાયરિંગ ખ્યાતકીય રીતે આવું દેખાય છે:

window.addEventListener("blur", cancelCharge);

પરંતુ સાચો આર્કિટેક્ચરલ ફેરફાર એ સ્વીકૃતિ છે કે ચાર્જિંગ હવે બે સંભવિત એક્ઝિટ (exit) ધરાવતી એક સ્ટેટ છે. યોગ્ય રિલીઝ પર, સ્ટેટ મશીન ચાર્જ ટકાવારીનું મૂલ્યાંકન કરે છે, જમ્પ વેલોસિટીની ગણતરી કરે છે, અને લીપ એનિમેશનમાં ટ્રાન્ઝિશન કરે છે. ઇન્ટરપ્ટ (વિક્ષેપ) પર, સ્ટેટ મશીન અટકી જાય છે અને આઈડલ સ્ટેટમાં પાછું ફરે છે. આ માર્ગોને અલગ રાખવાથી સાઈડ ઈફેક્ટ્સ (side effects) અટકાવી શકાય છે.

તમારે pointercancel માટે પણ સાંભળવું (listen) જોઈએ. જ્યારે બ્રાઉઝર પોઇન્ટિંગ ડિવાઇસ પર સિસ્ટમ-લેવલનું વિક્ષેપ અનુભવે છે—જેમ કે ટચસ્ક્રીન પર પામ રિજેક્શન જેસ્ચર, સિસ્ટમ મેનૂનું ઇવોકેશન, અથવા અસામાન્ય પરિસ્થિતિઓમાં પેનનો સંપર્ક તૂટી જવો—ત્યારે બ્રાઉઝર આ ઇવેન્ટ મોકલે છે. blur ને pointercancel સાથે જોડવાથી ડેસ્કટોપ મલ્ટિટાસ્કિંગ અને મોબાઈલ વિક્ષેપો બંને આવરી લેવાય છે. visibilitychange ઉમેરવાથી તે પરિસ્થિતિ પકડાઈ જાય છે જ્યાં વપરાશકર્તા વિન્ડો ઓબ્જેક્ટ પર blur ફાયર કર્યા વિના ટેબ બદલે છે, જે કેટલાક બ્રાઉઝર અને OS કોમ્બિનેશનમાં થઈ શકે છે.

બાઉન્ડ્રી કન્ડિશન્સનું પરીક્ષણ (Testing the Boundary Conditions)

ઇનપુટ બગ્સને સુધારવા માટે 'happy path' ની બહાર પરીક્ષણ કરવું જરૂરી છે. કોઈ પણ વ્યક્તિ એક સિંગલ ટેબમાં શાંતિથી ગેમ રમીને આ સમસ્યાઓ શોધી શકતું નથી. નવા વર્તનને ચકાસવા માટે, મેં બે ચોક્કસ પરિસ્થિતિઓ ચલાવી.

પ્રથમ, મેં જમ્પ ચાર્જ કરવાનું શરૂ કર્યું અને પછી કીબોર્ડનો ઉપયોગ કરીને બ્રાઉઝર ટેબ બદલીને blur ઇવેન્ટ ફરજિયાત કરી. ગેમ તરત જ ચાર્જિંગ મોડમાંથી બહાર આવી ગઈ અને એઇમિંગ (aiming) પર પાછી આવી ગઈ. કોઈ જમ્પ ફાયર થયું નહીં. કોઈ વેલોસિટી (velocity) લાગુ થઈ નહીં. ચાર્જ મીટર જાતે જ ખાલી થઈ ગયું. બીજું, મેં સામાન્ય ચાર્જ કર્યું અને જાણીજોઈને બટન છોડી દીધું. જમ્પ બરાબર પહેલાની જેમ જ એ જ આર્ક (arc) અને ફોર્સ સ્કેલિંગ સાથે એક્ઝિક્યુટ થયું. ગેમનો અનુભવ (game feel) અકબંધ રહ્યો; ફક્ત એજ કેસ (edge case) ને પેચ કરવામાં આવ્યો હતો.

બંને પાથ સ્વતંત્ર રહેવા જોઈએ. એવો સુધારો જે અકસ્માતજન્ય જમ્પને અટકાવે છે પરંતુ યોગ્ય જમ્પને નબળો પાડે છે તે સુધારો નથી—તે એક અલગ બગ છે. બ્રાઉઝરની અરાજકતા (chaos) સામે તેને મજબૂત બનાવવાની સાથે મૂળ મિકેનિકની સ્પષ્ટતા જાળવી રાખવી એ અમારો ધ્યેય હતો.

સતત ઇનપુટ માટે એક પેટર્ન (A Pattern for Sustained Input)

આ સમસ્યા પ્લેટફોર્મર્સથી ઘણું આગળ વિસ્તરેલી છે. કોઈપણ Three.js ગેમ જે સતત પ્રેસ પર આધારિત છે તે આનાથી પ્રભાવિત થઈ શકે છે. પ્રથમ-વ્યક્તિ (first-person) ગ્રેપલિંગ હૂક વિશે વિચારો જ્યાં માઉસ પકડી રાખવાથી ટેન્શન વધે છે, અથવા રેસિંગ ગેમ વિશે જ્યાં કી (key) દબાવી રાખવાથી બૂસ્ટ ચાર્જ થાય છે. જો તમારું teardown logic ફક્ત બટન રિલીઝ હેન્ડલરમાં જ હોય, અને તમે ટેબ સ્વિચિંગ, OS નોટિફિકેશન અથવા સ્ક્રીન લોકનું ધ્યાન ન રાખો, તો તમે ઓપરેટિંગ સિસ્ટમને તમારા વતી તમારી ગેમ રમવા દેતા છો.

વ્યાપક પેટર્ન એ છે કે તમારા ઇનપુટ લેયરને ત્રણ સ્પષ્ટ સ્ટેટ્સ સાથે બનાવો: active input, released input, અને cancelled input. Active input ચાર્જ બનાવે છે અથવા એક્શન શરૂ કરે છે. Released input તેને કમિટ કરે છે. Cancelled input તેને ચોખ્ખી રીતે સમાપ્ત કરે છે. વિન્ડો blur ને ક્યારેય રિલીઝ તરીકે ઓળખવા ન દો. બ્રાઉઝર એક હોસ્ટ છે, પ્લેયર નથી.

માનવ વર્તનને ધ્યાનમાં રાખો (Keep Human Behavior in Mind)

લોકો ટેબ બદલે છે. તેઓ ડાયરેક્ટ મેસેજનો જવાબ આપે છે. તેઓ તેમના બીજા મોનિટર પર ગાઈડ જુએ છે. તેમને કામના Slack પિંગ