సాంకేతిక కారణాల వల్ల ఆటలో ఓడిపోవడం ఆటగాళ్లకు అస్సలు నచ్చదు. ప్లాట్ఫామ్ స్పష్టంగా ఉంది, టైమింగ్ కూడా సరిగ్గా ఉంది, కానీ వారు తప్పు చేయడం వల్ల కాకుండా, బ్రౌజర్ ట్యాబ్ ఫోకస్ (focus) కోల్పోవడం వల్ల గేమ్ వారిని ఓడించింది.
నేను దీనిని Solstice Leap అనే Three.js ఆర్కేడ్ గేమ్లో ప్రత్యక్షంగా చూశాను. నేను దీనిని ఒకే ఒక సంతృప్తికరమైన మెకానిక్ చుట్టూ నిర్మించాను: దూకడానికి (jump) బటన్ను నొక్కి పట్టుకుని ఛార్జ్ చేయాలి, ఆపై దానిని వదిలేయాలి. ప్లేటెస్ట్ల సమయంలో, నేను ఒక విసిగించే విషయాన్ని గమనించాను. ఎవరైనా మెసేజ్కు రిప్లై ఇవ్వడానికి Alt-Tab నొక్కినా లేదా ఛార్జింగ్ జరుగుతున్నప్పుడు వేరే ట్యాబ్పై క్లిక్ చేసినా, విండో మళ్ళీ క్లిక్ చేయగానే లేదా ఫోకస్ కోల్పోయిన వెంటనే క్యారెక్టర్ శూన్యంలోకి దూకేసేది. ఒక సాధారణ ఆపరేటింగ్ సిస్టమ్ అంతరాయాన్ని (interruption), బటన్ను కావాలని వదిలేసినట్లుగా గేమ్ అర్థం చేసుకుంది. దీనివల్ల ఆటలు అన్యాయంగా ముగిసిపోయేవి. కంట్రోల్స్పై నమ్మకం తగ్గిపోయేది.
మూల కారణం: ఒకే ఈవెంట్ రెండు పనులు చేయడం
ఆ బగ్ చాలా సూక్ష్మంగా ఉన్నప్పటికీ, నేరుగా ప్రభావం చూపింది. అసలు ఇన్పుట్ లేయర్లో, జంప్ రిలీజ్ లాజిక్ను నేరుగా విండో యొక్క blur ఈవెంట్కు అనుసంధానించడం జరిగింది:
window.addEventListener("blur", releaseCharge);
పైకి చూస్తే ఇది సరైనదే అనిపించవచ్చు. ప్లేయర్ ఒక కీ లేదా పాయింటర్ను నొక్కి పట్టుకున్నారు; ఇప్పుడు ఏదో ఆగిపోయింది. కానీ blur ఈవెంట్ అనేది ఇన్పుట్ ఈవెంట్ కాదు. అది విండో మేనేజ్మెంట్ సిగ్నల్. ప్లేయర్ ట్యాబ్లు మార్చినప్పుడు, విండోను మినిమైజ్ చేసినప్పుడు, ఎక్స్టర్నల్ మానిటర్పై క్లిక్ చేసినప్పుడు లేదా సిస్టమ్ నోటిఫికేషన్ ఫోకస్ను తీసుకున్నప్పుడు బ్రౌజర్ ట్యాబ్ ఆపరేటింగ్ సిస్టమ్ ఫోకస్ను కోల్పోతే ఇది జరుగుతుంది. ఈ చర్యలలో ఏదీ "నేను నా క్యారెక్టర్ను లాంచ్ చేయాలనుకుంటున్నాను" అని అర్థం కాదు. అవి "నేను గేమ్ వెలుపల ఏదో ఒక దానితో ఇంటరాక్ట్ అవుతున్నాను" అని అర్థం చెబుతాయి.
blur ఈవెంట్ను releaseCharge కి అనుసంధానించడం ద్వారా, గేమ్ రెండు పూర్తిగా భిన్నమైన అంశాలను కలిపివేసింది: కావాలని చేసిన ఆప dừng (ప్లేయర్ బటన్ను వదిలేయడం) మరియు బాహ్య అంతరాయం (బ్రౌజర్ ఇక యాక్టివ్ విండో కాదు). releaseCharge ప్రస్తుత ఛార్జ్ స్టేట్ ఆధారంగా జంప్ ఫోర్స్ను లెక్కించి వెంటనే వెలాసిటీని (velocity) అప్లై చేస్తుంది కాబట్టి, ఛార్జింగ్ మధ్యలో ఫోకస్ కోల్పోతే, అప్పటివరకు ఎంత పవర్ పేరుకుపోయిందో దానితోనే లాంచ్ అయిపోయేది. ప్లేయర్ తిరిగి వచ్చేసరికి వారి క్యారెక్టర్ చనిపోయి ఉండటం లేదా వారు చేయని మూవ్ వల్ల వారి ప్రోగ్రెస్ దెబ్బతినడం జరుగుతుంది.
Three.js డెవలపర్ల కోసం బ్రౌజర్ వాస్తవాలు
Three.js మీకు శక్తివంతమైన 3D కాన్వాస్ను ఇస్తుంది, కానీ ఇన్పుట్ ఇంకా DOM ద్వారానే ప్రవహిస్తుంది. ఆ విభజన చాలా ముఖ్యం. స్పేస్బార్ను నొక్కి పట్టుకోవడం వల్ల జంప్ ఛార్జ్ అవుతుందని బ్రౌజర్కు సహజంగా తెలియదు. ఒక కీ నొక్కి పట్టుకున్నారని మాత్రమే దానికి తెలుస్తుంది. ఫోకస్ డాక్యుమెంట్ నుండి వెళ్ళినప్పుడు, బ్రౌజర్ పట్టుకున్న ప్రతి కీ కోసం ఆటోమేటిక్గా keyup ఈవెంట్ను సృష్టించదు. బదులుగా, విండో వెళ్ళిపోయిందని మాత్రమే చెబుతుంది. మీ గేమ్ లాజిక్ ఫోకస్ లేకపోవడాన్ని ఇన్పుట్ లేకపోవడంగా భావిస్తే, మీరు ఫాంటమ్ యాక్షన్స్ (phantom actions) ఎదుర్కోవాల్సి వస్తుంది.
ఈ తేడా ఛార్జ్-అప్ మెకానిక్స్కు చాలా ముఖ్యం, ఇవి ప్రతిచోటా కనిపిస్తాయి: విల్లును లాగడం, వాహనాన్ని వేగవంతం చేయడం, ఛార్జ్డ్ స్పెల్ వేయడం లేదా స్టామినాతో పరుగెత్తడం వంటివి. కాలక్రమేణా స్టేట్ను (state) పేరుకుపోయే ఏ నిరంతర చర్య అయినా ఇదే విధమైన తప్పుగా అర్థం చేసుకోబడే ప్రమాదం ఉంది. నేటివ్ అప్లికేషన్లు తరచుగా ఫోకస్ కోల్పోయినప్పుడు మొత్తం సిమ్యులేషన్ను నిలిపివేస్తాయి. బ్రౌజర్ గేమ్లు కూడా అలాగే చేయవచ్చు, కానీ మీరు గేమ్ను రన్ చేస్తూనే ఉన్నా, సిస్టమ్ అంతరాయాలను ప్లేయర్ కమాండ్ల నుండి వేరు చేయాలి.
ఉద్దేశాన్ని (Intention) అంతరాయం (Interruption) నుండి వేరు చేయడం
ఈ సమస్యను పరిష్కరించడానికి ఛార్జింగ్ స్టేట్ నుండి బయటకు వచ్చే మార్గాన్ని రెండు వేర్వేరు మార్గాలుగా విభజించాల్సి వచ్చింది. ఒకటి కావాలని చేసే ఇన్పుట్ను హ్యాండిల్ చేస్తుంది. మరొకటి నిజ ప్రపంచం జోక్యం చేసుకున్నప్పుడు పరిస్థితిని హ్యాండిల్ చేస్తుంది.
Deliberate releases—pointerup మరియు keyup—ఇంకా జంప్ను ఎగ్జిక్యూట్ చేస్తాయి. ఇవి ప్లేయర్ ముందుకు వెళ్లడానికి ఇచ్చే ప్రత్యక్ష సంకేతాలు.
Focus loss events—blur, pointercancel, మరియు డాక్యుమెంట్ హైడ్ అయినప్పుడు visibilitychange—ఇప్పుడు cancelCharge అనే వేరే ఫంక్షన్ను ట్రిగ్గర్ చేస్తాయి.
cancelCharge అనేది కేవలం మార్చబడిన రిలీజ్ కాదు. ఇది ఒక హార్డ్ రీసెట్ (hard reset). ఇది పేరుకుపోయిన ఛార్జ్ ఫోర్స్ను సున్నాకి తగ్గిస్తుంది, ప్లేయర్ యొక్క విజువల్ స్కేల్ను డిఫాల్ట్ ఐడిల్ స్టేట్కు మారుస్తుంది, స్క్రీన్పై ఉన్న ఛార్జ్ మీటర్ను సున్నా చేస్తుంది మరియు గేమ్ను తిరిగి ఎయిమింగ్ మోడ్లోకి తీసుకువస్తుంది. అన్నింటికంటే ముఖ్యంగా, ఇది లాంచ్ ట్రాజెక్టరీ కోడ్ను తాకదు. ఇందులో వెలాసిటీ కాలిక్యులేషన్, ఫిజిక్స్ ఇంపల్స్ లేదా లీప్ ఉండవు. ఛార్జ్ సురక్షితంగా ఆవిరైపోతుంది.
అప్డేట్ చేసిన విధానం భావనపరంగా ఇలా ఉంటుంది:
window.addEventListener("blur", cancelCharge);
కానీ అసలైన ఆర్కిటెక్చరల్ మార్పు ఏమిటంటే, ఛార్జింగ్ అనేది ఇప్పుడు రెండు సాధ్యమయ్యే ఎగ్జిట్ పాయింట్లు ఉన్న ఒక స్టేట్ అని గుర్తించడం. సరైన రిలీజ్ జరిగినప్పుడు, స్టేట్ మెషీన్ ఛార్జ్ శాతాన్ని అంచనా వేస్తుంది, జంప్ వెలాసిటీని లెక్కిస్తుంది మరియు లీప్ యానిమేషన్లోకి మారుతుంది. అంతరాయం కలిగినప్పుడు, స్టేట్ మెషీన్ ఆగిపోయి తిరిగి ఐడిల్ స్టేట్కు చేరుకుంటుంది. ఈ మార్గాలను వేరుగా ఉంచడం వల్ల సైడ్ ఎఫెక్ట్స్ (side effects) రాకుండా ఉంటాయి.
మీరు pointercancel కోసం కూడా వినాలి. పాయింటింగ్ పరికరంలో సిస్టమ్-లెవల్ అంతరాయాన్ని బ్రౌజర్ గుర్తించినప్పుడు ఇది డిస్పాచ్ అవుతుంది—ఉదాహరణకు టచ్స్క్రీన్లపై పామ్ రిజెక్షన్ జెస్టర్, సిస్టమ్ మెనూ ఇన్వోకేషన్, లేదా అసాధారణ పరిస్థితుల్లో పెన్ కాంటాక్ట్ కోల్పోవడం వంటివి. blurని pointercancelతో జత చేయడం వల్ల డెస్క్టాప్ మల్టీటాస్కింగ్ మరియు మొబైల్ అంతరాయాలు రెండింటినీ కవర్ చేయవచ్చు. visibilitychangeని జోడించడం వల్ల, విండో ఆబ్జెక్ట్పై blur ఈవెంట్ ట్రిగ్గర్ కాకుండానే యూజర్ ట్యాబ్లను మార్చే సందర్భాలను కూడా పట్టుకోవచ్చు, ఇది కొన్ని బ్రౌజర్ మరియు OS కాంబినేషన్లలో జరుగుతుంది.
బౌండరీ కండిషన్ల పరీక్ష
ఇన్పుట్ బగ్లను సరిచేయడానికి 'happy path' వెలుపల పరీక్షించడం అవసరం. ఒకే ట్యాబ్లో ప్రశాంతంగా గేమ్ ఆడుతూ ఎవరూ ఈ సమస్యలను కనుగొనలేరు. కొత్త ప్రవర్తనను ధృవీకరించడానికి, నేను రెండు నిర్దిష్ట సినారియోలను నిర్వహించాను.
మొదట, నేను జంప్ను ఛార్జ్ చేయడం ప్రారంభించి, కీబోర్డ్ ఉపయోగించి బ్రౌజర్ ట్యాబ్లను మార్చడం ద్వారా బలవంతంగా blur ఈవెంట్ను కలిగించాను. గేమ్ వెంటనే ఛార్జింగ్ మోడ్ నుండి బయటకు వచ్చి aiming మోడ్కు తిరిగి వచ్చింది. జంప్ జరగలేదు. వేలాసిటీ (velocity) అప్లై కాలేదు. ఛార్జ్ మీటర్ తనంతట తాను క్లియర్ అయిపోయింది. రెండవదిగా, నేను సాధారణ ఛార్జ్ను చేసి, కావాలనే బటన్ను వదిలేశాను. జంప్ మునుపటిలాగే అదే arc మరియు force scalingతో ఖచ్చితంగా అమలు చేయబడింది. గేమ్ ఫీల్ మారలేదు; కేవలం ఎడ్జ్ కేస్ (edge case) మాత్రమే పరిష్కరించబడింది.
రెండు మార్గాలు స్వతంత్రంగా ఉండాలి. అనుకోకుండా జరిగే జంప్లను నిరోధిస్తూనే, సరైన జంప్లను బలహీనపరిచే పరిష్కారం పరిష్కారం కాదు—అది మరొక బగ్. బ్రౌజర్ గందరగోళానికి వ్యతిరేకంగా మెకానిక్ను పటిష్టం చేస్తూనే, అసలు మెకానిక్ యొక్క స్పష్టతను కాపాడటమే లక్ష్యం.
నిరంతర ఇన్పుట్ కోసం ఒక ప్యాటర్న్
ఈ సమస్య ప్లాట్ఫార్మర్లకే పరిమితం కాదు. నిరంతర ప్రెస్పై ఆధారపడే ఏ Three.js గేమ్ అయినా దీనికి గురవుతుంది. మౌస్ను పట్టుకోవడం వల్ల టెన్షన్ పెరిగే ఫస్ట్-పర్సన్ గ్రాప్లింగ్ హుక్ లేదా కీని పట్టుకోవడం వల్ల బూస్ట్ ఛార్జ్ అయ్యే రేసింగ్ గేమ్ గురించి ఆలోచించండి. మీ teardown logic కేవలం బటన్ రిలీజ్ హ్యాండ్లర్లో మాత్రమే ఉండి, మీరు ట్యాబ్ స్విచ్చింగ్, OS నోటిఫికేషన్లు లేదా స్క్రీన్ లాక్లను పరిగణనలోకి తీసుకోకపోతే, మీరు మీ గేమ్ మీ తరపున ఆపరేటింగ్ సిస్టమ్ ఆడేలా చేస్తున్నారు.
విస్తృతమైన ప్యాటర్న్ ఏమిటంటే, మీ ఇన్పుట్ లేయర్ను మూడు స్పష్టమైన స్టేట్లతో నిర్మించడం: active input, released input, మరియు cancelled input. Active input ఛార్జ్ను నిర్మిస్తుంది లేదా చర్యను ప్రారంభిస్తుంది. Released input దానిని ఖరారు చేస్తుంది. Cancelled input దానిని శుభ్రంగా ఆపివేస్తుంది. విండో blurను ఎప్పుడూ రిలీజ్ (release) లాగా భావించనివ్వకండి. బ్రౌజర్ అనేది హోస్ట్ మాత్రమే, ప్లేయర్ కాదు.
మానవ ప్రవర్తనను దృష్టిలో ఉంచుకోండి
ప్రజలు ట్యాబ్లను మారుస్తారు. వారు డైరెక్ట్ మెసేజ్లకు సమాధానం ఇస్తారు. వారు తమ రెండవ మానిటర్లో గైడ్ను చూస్తారు. వారికి పనికి సంబంధించిన Slack నోటిఫికేషన్లు వస్తాయి. ఇవి ఎడ్జ్ కేసులు కావు; ఇవి బ్రౌజర్లో సాధారణ ప్రవర్తనలు. సాధారణ మానవ మల్టీటాస్కింగ్ను శిక్షించే బ్రౌజర్ గేమ్ బలహీనంగా అనిపిస్తుంది. ఫోకస్ కోల్పోవడాన్ని కమాండ్ లాగా కాకుండా క్యాన్సిలేషన్ (cancellation) లాగా పరిగణించడం ద్వారా, Solstice Leap ఇప్పుడు ఆటగాళ్లు జాగ్రత్తగా సెట్ చేసిన జంప్ను కోల్పోకుండానే ఒక క్షణం పక్కకు వెళ్లడానికి అనుమతిస్తుంది.
blur ఈవెంట్ అనేది రిలీజ్ ఈవెంట్ కాదు. బ్రౌజర్ గది నుండి బయటకు వెళ్ళిందని చెప్పడం మాత్రమే అది. దానికి అనుగుణంగా కోడ్ రాయండి, అప్పుడు మీ ఆటగాళ్లు నిజంగా జంప్ చేయాలనుకున్నప్పుడు కంట్రోల్స్ను నమ్మకంగా ఉపయోగించగలరు.
