ತಾಂತ್ರಿಕ ಕಾರಣಗಳಿಂದಾಗಿ ಆಟದ ಪ್ರಗತಿಯನ್ನು ಕಳೆದುಕೊಳ್ಳುವುದು ಆಟಗಾರರಿಗೆ ಇಷ್ಟವಿಲ್ಲ. ವೇದಿಕೆಯು ಸ್ಪಷ್ಟವಾಗಿತ್ತು, ಸಮಯವೂ ಸರಿಯಾಗಿತ್ತು, ಆದರೆ ಅವರು ತಪ್ಪು ಮಾಡಿದ್ದರಿಂದಲ್ಲ, ಬದಲಾಗಿ ಬ್ರೌಸರ್ ಟ್ಯಾಬ್ ಫೋಕಸ್ (focus) ಕಳೆದುಕೊಂಡಿದ್ದರಿಂದ ಆಟವು ಅವರನ್ನು ಸೋಲಿಸಿತು.
ನಾನು ಇದನ್ನು Solstice Leap ಎಂಬ Three.js ಆರ್ಕೇಡ್ ಗೇಮ್ನಲ್ಲಿ ನೇರವಾಗಿ ಕಂಡೆ. ನಾನು ಇದನ್ನು ಒಂದು ತೃಪ್ತಿಕರ ಮೆಕ್ಯಾನಿಕ್ನ ಸುತ್ತ ನಿರ್ಮಿಸಿದ್ದೆ: ಜಂಪ್ ಮಾಡಲು ಚಾರ್ಜ್ ಮಾಡಲು ಒಂದು ಬಟನ್ ಅನ್ನು ಹಿಡಿದುಕೊಳ್ಳುವುದು, ನಂತರ ಅಂತರವನ್ನು ದಾಟಲು ಅದನ್ನು ಬಿಡುವುದು. ಪ್ಲೇ-ಟೆಸ್ಟ್ಗಳ ಸಮಯದಲ್ಲಿ, ನಾನು ಒಂದು ಕಿರಿಕಿರಿ ಉಂಟುಮಾಡುವ ಮಾದರಿಯನ್ನು ಗಮನಿಸಿದೆ. ಯಾರಾದರೂ ಮೆಸೇಜ್ಗೆ ಉತ್ತರಿಸಲು Alt-Tab ಮಾಡಿದರೆ ಅಥವಾ ಚಾರ್ಜ್ ಆಗುತ್ತಿರುವಾಗ ಇನ್ನೊಂದು ಟ್ಯಾಬ್ ಅನ್ನು ಕ್ಲಿಕ್ ಮಾಡಿದರೆ, ವಿಂಡೋ ಮತ್ತೆ ಕ್ಲಿಕ್ ಮಾಡಿದ ತಕ್ಷಣ ಅಥವಾ ಫೋಕಸ್ ಕಳೆದುಕೊಂಡ ತಕ್ಷಣವೇ ಕ್ಯಾರೆಕ್ಟರ್ ಶೂನ್ಯಕ್ಕೆ (void) ಧುಮುಕುತ್ತಿತ್ತು. ಆಪರೇಟಿಂಗ್ ಸಿಸ್ಟಮ್ನ ಸಾಮಾನ್ಯ ಅಡಚಣೆಯನ್ನು ಆಟವು ಉದ್ದೇಶಪೂರ್ವಕವಾಗಿ ಬಟನ್ ಬಿಡುಗಡೆ ಮಾಡಿದನೆಂದು ತಪ್ಪಾಗಿ ಅರ್ಥೈಸಿಕೊಂಡಿತ್ತು. ಇದರಿಂದ ಆಟವು ಅನ್ಯಾಯವಾಗಿ ಕೊನೆಗೊಳ್ಳುತ್ತಿತ್ತು ಮತ್ತು ಕಂಟ್ರೋಲ್ಗಳ ಮೇಲಿನ ನಂಬಿಕೆ ಕುಸಿಯುತ್ತಿತ್ತು.
ಮೂಲ ಕಾರಣ: ಒಂದು ಇವೆಂಟ್ ಎರಡು ಕೆಲಸಗಳನ್ನು ಮಾಡುವುದು
ಈ ಬಗ್ ಸೂಕ್ಷ್ಮವಾಗಿತ್ತು ಆದರೆ ನೇರ ಪರಿಣಾಮ ಬೀರುತ್ತಿತ್ತು. ಮೂಲ ಇನ್ಪುಟ್ ಲೇಯರ್ನಲ್ಲಿ, ಕೋಡ್ ಜಂಪ್ ರಿಲೀಸ್ ಲಾಜಿಕ್ ಅನ್ನು ನೇರವಾಗಿ ವಿಂಡೋನ blur ಇವೆಂಟ್ಗೆ ಜೋಡಿಸಿತ್ತು:
window.addEventListener("blur", releaseCharge);
ಮೇಲ್ನೋಟಕ್ಕೆ ಇದು ಸರಿಯಾಗಿ ಕಾಣಿಸಬಹುದು. ಆಟಗಾರ ಕೀ ಅಥವಾ ಪಾಯಿಂಟರ್ ಅನ್ನು ಹಿಡಿದುಕೊಂಡಿದ್ದ; ಈಗ ಏನೋ ಒಂದು ನಿಂತಿದೆ. ಆದರೆ blur ಇವೆಂಟ್ ಎಂಬುದು ಇನ್ಪುಟ್ ಇವೆಂಟ್ ಅಲ್ಲ. ಅದು ವಿಂಡೋ ಮ್ಯಾನೇಜ್ಮೆಂಟ್ ಸಿಗ್ನಲ್ ಆಗಿದೆ. ಬ್ರೌಸರ್ ಟ್ಯಾಬ್ ಆಪರೇಟಿಂಗ್ ಸಿಸ್ಟಮ್ ಫೋಕಸ್ ಕಳೆದುಕೊಂಡಾಗ ಇದು ಕಾರ್ಯಗತಗೊಳ್ಳುತ್ತದೆ. ಆಟಗಾರ ಟ್ಯಾಬ್ಗಳನ್ನು ಬದಲಾಯಿಸಿದಾಗ, ವಿಂಡೋವನ್ನು ಮಿನಿಮೈಸ್ ಮಾಡಿದಾಗ, ಎಕ್ಸ್ಟರ್ನಲ್ ಮಾನಿಟರ್ ಅನ್ನು ಕ್ಲಿಕ್ ಮಾಡಿದಾಗ ಅಥವಾ ಸಿಸ್ಟಮ್ ನೋಟಿಫಿಕೇಶನ್ ಬಂದಾಗ ಇದು ಸಂಭವಿಸಬಹುದು. ಈ ಯಾವುದೇ ಕ್ರಮಗಳು "ನಾನು ನನ್ನ ಕ್ಯಾರೆಕ್ಟರ್ ಅನ್ನು ಲಾಂಚ್ ಮಾಡಲು ಬಯಸುತ್ತೇನೆ" ಎಂದರ್ಥವಲ್ಲ. ಅವುಗಳ ಅರ್ಥ "ನಾನು ಆಟದ ಹೊರಗಿನ ಯಾವುದೋ ವಿಷಯದೊಂದಿಗೆ ಸಂವಹನ ನಡೆಸುತ್ತಿದ್ದೇನೆ" ಎಂದು.
blur ಅನ್ನು releaseCharge ಗೆ ಕಳುಹಿಸುವ ಮೂಲಕ, ಆಟವು ಎರಡು ಸಂಪೂರ್ಣವಾಗಿ ವಿಭಿನ್ನ ಪರಿಕಲ್ಪನೆಗಳನ್ನು ಬೆರೆಸಿತು: ಒಂದು ಉದ್ದೇಶಪೂರ್ವಕ ನಿಲುಗಡೆ (ಆಟಗಾರ ಬಟನ್ ಬಿಡುವುದು) ಮತ್ತು ಇನ್ನೊಂದು ಬಾಹ್ಯ ಅಡಚಣೆ (ಬ್ರೌಸರ್ ಈಗ ಸಕ್ರಿಯ ವಿಂಡೋ ಅಲ್ಲ). releaseCharge ಪ್ರಸ್ತುತ ಚಾರ್ಜ್ ಸ್ಥಿತಿಯ ಆಧಾರದ ಮೇಲೆ ಜಂಪ್ ಫೋರ್ಸ್ ಅನ್ನು ಲೆಕ್ಕಹಾಕಿ ತಕ್ಷಣವೇ ವೇಗವನ್ನು (velocity) ಅನ್ವಯಿಸುವುದರಿಂದ, ಚಾರ್ಜ್ ಆಗುತ್ತಿರುವ ಮಧ್ಯದಲ್ಲಿ ಫೋಕಸ್ ಕಳೆದುಕೊಂಡರೆ, ಅಷ್ಟರ ಮಟ್ಟಿಗೆ ಸಂಗ್ರಹವಾದ ಶಕ್ತಿಯೊಂದಿಗೆ ಲಾಂಚ್ ಆಗುವ ಪ್ರಕ್ರಿಯೆ ಪ್ರಾರಂಭವಾಗುತ್ತಿತ್ತು. ಆಟಗಾರ ಹಿಂತಿರುಗಿದಾಗ ಅವರ ಕ್ಯಾರೆಕ್ಟರ್ ಸತ್ತುಹೋಗಿರುತ್ತಿತ್ತು ಅಥವಾ ಅವರು ಅನುಮೋದಿಸದ ಚಲನೆಯಿಂದಾಗಿ ಅವರ ಪ್ರಗತಿ ಹಾಳಾಗಿರುತ್ತಿತ್ತು.
Three.js ಡೆವಲಪರ್ಗಳಿಗಾಗಿ ಬ್ರೌಸರ್ ವಾಸ್ತವಗಳು
Three.js ನಿಮಗೆ ಶಕ್ತಿಯುತವಾದ 3D ಕ್ಯಾನ್ವಾಸ್ ನೀಡುತ್ತದೆ, ಆದರೆ ಇನ್ಪುಟ್ ಇನ್ನೂ DOM ಮೂಲಕವೇ ಹರಿಯುತ್ತದೆ. ಈ ವ್ಯತ್ಯಾಸ ಬಹಳ ಮುಖ್ಯ. ಸ್ಪೇಸ್ಬಾರ್ ಅನ್ನು ಹಿಡಿದುಕೊಳ್ಳುವುದು ಜಂಪ್ ಚಾರ್ಜ್ ಮಾಡುತ್ತದೆ ಎಂದು ಬ್ರೌಸರ್ ಸ್ವತಃ ತಿಳಿಯುವುದಿಲ್ಲ. ಕೇವಲ ಒಂದು ಕೀ ಒತ್ತಲ್ಪಟ್ಟಿದೆ ಎಂದು ಮಾತ್ರ ಅದಕ್ಕೆ ತಿಳಿದಿರುತ್ತದೆ. ಫೋಕಸ್ ಡಾಕ್ಯುಮೆಂಟ್ನಿಂದ ಹೊರಟಾಗ, ಬ್ರೌಸರ್ ಹಿಡಿದುಕೊಂಡಿರುವ ಪ್ರತಿಯೊಂದು ಕೀಗಾಗಿ ಸ್ವಯಂಚಾಲಿತವಾಗಿ keyup ಅನ್ನು ಸೃಷ್ಟಿಸುವುದಿಲ್ಲ. ಬದಲಾಗಿ, ವಿಂಡೋ ಇಲ್ಲ ಎಂದು ಅದು ನಿಮಗೆ ತಿಳಿಸುತ್ತದೆ. ನಿಮ್ಮ ಗೇಮ್ ಲಾಜಿಕ್ ಫೋಕಸ್ ಇಲ್ಲದಿರುವುದೇ ಇನ್ಪುಟ್ ಇಲ್ಲ ಎಂದರ್ಥ ಎಂದು ಭಾವಿಸಿದರೆ, ನೀವು ಫ್ಯಾಂಟಮ್ ಆಕ್ಷನ್ಗಳನ್ನು (phantom actions) ಎದುರಿಸುತ್ತೀರಿ.
ಬಿಲ್ಲು ಎಳೆಯುವುದು, ವಾಹನವನ್ನು ವೇಗಗೊಳಿಸುವುದು, ಚಾರ್ಜ್ಡ್ ಸ್ಪೆಲ್ (spell) ಪ್ರಯೋಗಿಸುವುದು ಅಥವಾ ಸ್ಟ್ಯಾಮಿನಾ ಬಳಸಿ ಓಡುವುದು - ಇಂತಹ ಚಾರ್ಜ್-ಅಪ್ ಮೆಕ್ಯಾನಿಕ್ಗಳಿಗೆ ಈ ವ್ಯತ್ಯಾಸವು ಬಹಳ ಮುಖ್ಯವಾಗಿದೆ. ಸಮಯದೊಂದಿಗೆ ಸ್ಥಿತಿಯನ್ನು (state) ಸಂಗ್ರಹಿಸುವ ಯಾವುದೇ ನಿರಂತರ ಕ್ರಿಯೆಯು ಇದೇ ರೀತಿಯ ತಪ್ಪು ವ್ಯಾಖ್ಯಾನಕ್ಕೆ ಒಳಗಾಗುವ ಸಾಧ್ಯತೆ ಇರುತ್ತದೆ. ನೇಟಿವ್ ಅಪ್ಲಿಕೇಶನ್ಗಳು ಸಾಮಾನ್ಯವಾಗಿ ಫೋಕಸ್ ಕಳೆದುಕೊಂಡಾಗ ಇಡೀ ಸಿಮ್ಯುಲೇಶನ್ ಅನ್ನು ಸ್ಥಗಿತಗೊಳಿಸುತ್ತವೆ. ಬ್ರೌಸರ್ ಗೇಮ್ಗಳು ಕೂಡ ಹಾಗೆಯೇ ಮಾಡಬಹುದು, ಆದರೆ ನೀವು ಆಟವನ್ನು ಮುಂದುವರಿಸಿದರೂ ಸಹ, ಸಿಸ್ಟಮ್ ಅಡಚಣೆಗಳನ್ನು ಮತ್ತು ಆಟಗಾರನ ಕಮಾಂಡ್ಗಳನ್ನು ನೀವು ಪ್ರತ್ಯೇಕಿಸಬೇಕು.
ಉದ್ದೇಶ ಮತ್ತು ಅಡಚಣೆಯನ್ನು ಪ್ರತ್ಯೇಕಿಸುವುದು
ಈ ಸಮಸ್ಯೆಯನ್ನು ಸರಿಪಡಿಸಲು ಚಾರ್ಜಿಂಗ್ ಸ್ಥಿತಿಯಿಂದ ಹೊರಬರುವ ಹಾದಿಯನ್ನು ಎರಡು ವಿಭಿನ್ನ ಮಾರ್ಗಗಳಾಗಿ ವಿಂಗಡಿಸಬೇಕಾಯಿತು. ಒಂದು ಮಾರ್ಗವು ಉದ್ದೇಶಪೂರ್ವಕ ಇನ್ಪುಟ್ ಅನ್ನು ನಿರ್ವಹಿಸುತ್ತದೆ. ಇನ್ನೊಂದು ಮಾರ್ಗವು ನೈಜ ಪ್ರಪಂಚದ ಅಡಚಣೆಗಳು ಸಂಭವಿಸಿದಾಗ ಗೇಮ್ ಸ್ಥಿತಿಯನ್ನು ಕಾಪಾಡಿಕೊಳ್ಳುತ್ತದೆ.
ಉದ್ದೇಶಪೂರ್ವಕ ಬಿಡುಗಡೆಗಳು—pointerup ಮತ್ತು keyup—ಇನ್ನೂ ಸಹ ಜಂಪ್ ಅನ್ನು ಕಾರ್ಯಗತಗೊಳಿಸುತ್ತವೆ. ಇವು ಆಟಗಾರನು ಚಲಿಸಲು ನೀಡುವ ನೇರ ಸಂಕೇತಗಳು.
ಫೋಕಸ್ ಕಳೆದುಕೊಳ್ಳುವ ಇವೆಂಟ್ಗಳು—blur, pointercancel, ಮತ್ತು ಡಾಕ್ಯುಮೆಂಟ್ ಅಡಗಿದಾಗ ಸಂಭವಿಸುವ visibilitychange—ಈಗ cancelCharge ಎಂಬ ಪ್ರತ್ಯೇಕ ಫಂಕ್ಷನ್ ಅನ್ನು ಕಾರ್ಯಗತಗೊಳಿಸುತ್ತವೆ.
cancelCharge ಎಂಬುದು ಮಾರ್ಪಡಿಸಿದ ರಿಲೀಸ್ ಅಲ್ಲ. ಇದು ಒಂದು ಹಾರ್ಡ್ ರಿಸೆಟ್ (hard reset). ಇದು ಸಂಗ್ರಹವಾದ ಚಾರ್ಜ್ ಶಕ್ತಿಯನ್ನು ಶೂನ್ಯಕ್ಕೆ ತರುತ್ತದೆ, ಆಟಗಾರನ ದೃಶ್ಯ ಸ್ಕೇಲ್ ಅನ್ನು ಅದರ ಡಿಫಾಲ್ಟ್ ಐಡಲ್ ಸ್ಥಿತಿಗೆ ಮರಳಿಸುತ್ತದೆ, ಸ್ಕ್ರೀನ್ ಮೇಲಿನ ಚಾರ್ಜ್ ಮೀಟರ್ ಅನ್ನು ಶೂನ್ಯಗೊಳಿಸುತ್ತದೆ ಮತ್ತು ಗೇಮ್ ಅನ್ನು ಅದರ ಏಮಿಂಗ್ ಮೋಡ್ಗೆ (aiming mode) ಮರಳಿಸುತ್ತದೆ. ಎಲ್ಲಕ್ಕಿಂತ ಮುಖ್ಯವಾಗಿ, ಇದು ಲಾಂಚ್ ಟ್ರ್ಯಾಜೆಕ್ಟರಿ (launch trajectory) ಕೋಡ್ ಅನ್ನು ಮುಟ್ಟುವುದಿಲ್ಲ. ಅಲ್ಲಿ ಯಾವುದೇ ವೇಗದ ಲೆಕ್ಕಾಚಾರ, ಫಿಸಿಕ್ಸ್ ಇಂಪಲ್ಸ್ ಅಥವಾ ಜಂಪ್ ಇರುವುದಿಲ್ಲ. ಚಾರ್ಜ್ ಸುರಕ್ಷಿತವಾಗಿ ಅಳಿಸಿಹೋಗುತ್ತದೆ.
ಅಪ್ಡೇಟ್ ಮಾಡಿದ ವಿನ್ಯಾಸವು ಸೈದ್ಧಾಂತಿಕವಾಗಿ ಹೀಗಿರುತ್ತದೆ:
window.addEventListener("blur", cancelCharge);
ಆದರೆ ನಿಜವಾದ ಆರ್ಕಿಟೆಕ್ಚರಲ್ ಬದಲಾವಣೆಯೆಂದರೆ, ಚಾರ್ಜಿಂಗ್ ಎಂಬುದು ಈಗ ಎರಡು ಸಂಭವನೀಯ ನಿರ್ಗಮನಗಳನ್ನು ಹೊಂದಿರುವ ಒಂದು ಸ್ಥಿತಿ (state) ಎಂಬ ಅರಿವು. ಸರಿಯಾದ ಬಿಡುಗಡೆಯ ಸಂದರ್ಭದಲ್ಲಿ, ಸ್ಟೇಟ್ ಮೆಷಿನ್ (state machine) ಚಾರ್ಜ್ ಶೇಕಡಾವನ್ನು ಮೌಲ್ಯಮಾಪನ ಮಾಡುತ್ತದೆ, ಜಂಪ್ ವೇಗವನ್ನು ಲೆಕ್ಕಹಾಕುತ್ತದೆ ಮತ್ತು ಲೀಪ್ ಅನಿಮೇಷನ್ಗೆ ಬದಲಾಗುತ್ತದೆ. ಅಡಚಣೆಯ ಸಂದರ್ಭದಲ್ಲಿ, ಸ್ಟೇಟ್ ಮೆಷಿನ್ ಅದನ್ನು ರದ್ದುಗೊಳಿಸಿ ಐಡಲ್ ಸ್ಥಿತಿಗೆ ಮರಳುತ್ತದೆ. ಈ ಮಾರ್ಗಗಳನ್ನು ಪ್ರತ್ಯೇಕವಾಗಿ ಇರಿಸುವುದು ಪಾರ್ಶ್ವ ಪರಿಣಾಮಗಳನ್ನು (side effects) ತಡೆಯುತ್ತದೆ.
You should also listen for pointercancel. The browser dispatches this when it detects a system-level interruption on the pointing device—things like a palm rejection gesture on touchscreens, a system menu invocation, or a pen losing contact under unusual conditions. Pairing blur with pointercancel covers both desktop multitasking and mobile interruptions. Adding visibilitychange catches the scenario where a user switches tabs without necessarily firing blur on the window object itself, which can happen in some browser and OS combinations.
Testing the Boundary Conditions
Fixing input bugs demands testing outside the happy path. No one finds these issues by calmly playing the game in a single tab. To verify the new behavior, I ran two specific scenarios.
First, I started charging a jump and then forced a blur event by switching browser tabs using the keyboard. The game immediately dropped out of charging mode and returned to aiming. No jump fired. No velocity applied. The charge meter cleared itself. Second, I performed a normal charge and released the button intentionally. The jump executed exactly as it had before, with the same arc and force scaling. The game feel remained intact; only the edge case was patched.
Both paths had to remain independent. A fix that prevents accidental jumps but dulls legitimate ones is not a fix—it is a different bug. Preserving the crispness of the original mechanic while hardening it against browser chaos was the goal.
A Pattern for Sustained Input
This problem extends far beyond platformers. Any Three.js game that relies on a continuous press is exposed. Consider a first-person grappling hook where holding the mouse builds tension, or a racing game where a held key charges a boost. If your teardown logic lives only in a button release handler, and you do not account for tab switching, OS notifications, or screen locks, you are allowing the operating system to play your game for you.
The broader pattern is to build your input layer with three explicit states: active input, released input, and cancelled input. Active input builds the charge or initiates the action. Released input commits it. Cancelled input kills it cleanly. Never let a window blur masquerade as a release. The browser is a host, not a player.
Keep Human Behavior in Mind
People switch tabs. They answer direct messages. They look up a guide on their second monitor. They get work Slack pings. These are not edge cases; they are standard behavior inside a browser. A browser game that punishes normal human multitasking feels fragile. By treating focus loss as a cancellation rather than a command, Solstice Leap now lets players step away for a second without sacrificing a carefully set up jump.
A blur event is not a release event. It is simply the browser saying it stepped out of the room. Code accordingly, and your players will trust the controls enough to take the leap when they actually mean to.
