ਖਿਡਾਰੀ ਕਿਸੇ ਤਕਨੀਕੀ ਕਾਰਨ ਆਪਣੀ ਰਨ (run) ਹਾਰਨਾ ਨਫ਼ਰਤ ਕਰਦੇ ਹਨ। ਪਲੇਟਫਾਰਮ ਸਾਫ਼ ਸੀ, ਟਾਈਮਿੰਗ ਸਹੀ ਸੀ, ਅਤੇ ਫਿਰ ਗੇਮ ਨੇ ਉਹਨਾਂ ਨੂੰ ਇਸ ਲਈ ਨਹੀਂ ਮਾਰਿਆ ਕਿਉਂਕਿ ਉਹਨਾਂ ਨੇ ਕੋਈ ਗਲਤੀ ਕੀਤੀ ਸੀ, ਸਗੋਂ ਇਸ ਲਈ ਕਿਉਂਕਿ ਬ੍ਰਾਊਜ਼ਰ ਟੈਬ ਨੇ ਫੋਕਸ (focus) ਗੁਆ ਦਿੱਤਾ ਸੀ।
ਮੈਂ ਇਹ Solstice Leap ਵਿੱਚ ਖੁਦ ਦੇਖਿਆ, ਜੋ ਕਿ ਇੱਕ Three.js ਆਰਕੇਡ ਗੇਮ ਹੈ ਜਿਸਨੂੰ ਮੈਂ ਇੱਕ ਸਿੰਗਲ ਸੰਤੁਸ਼ਟੀਜਨਕ ਮਕੈਨਿਕ (mechanic) ਦੇ ਆਲੇ-ਦੁਆਲੇ ਬਣਾਇਆ ਸੀ: ਜੰਪ (jump) ਨੂੰ ਚਾਰਜ ਕਰਨ ਲਈ ਇੱਕ ਬਟਨ ਨੂੰ ਦਬਾ ਕੇ ਰੱਖੋ, ਫਿਰ ਪਾੜਿਆਂ (gaps) ਦੇ ਪਾਰ ਲਾਂਚ ਕਰਨ ਲਈ ਇਸਨੂੰ ਛੱਡ ਦਿਓ। ਪਲੇਅ-ਟੈਸਟਿੰਗ ਦੌਰਾਨ, ਮੈਂ ਇੱਕ ਪਰੇਸ਼ਾਨ ਕਰਨ ਵਾਲਾ ਪੈਟਰਨ ਦੇਖਿਆ। ਜੇਕਰ ਕੋਈ ਮੈਸੇਜ ਦਾ ਜਵਾਬ ਦੇਣ ਲਈ Alt-Tab ਕਰਦਾ ਸੀ ਜਾਂ ਜਦੋਂ ਉਹਨਾਂ ਦਾ ਚਾਰਜ ਵਧ ਰਿਹਾ ਸੀ ਤਾਂ ਕਿਸੇ ਹੋਰ ਟੈਬ 'ਤੇ ਕਲਿੱਕ ਕਰਦਾ ਸੀ, ਤਾਂ ਵਿੰਡੋ ਵਾਪਸ ਕਲਿੱਕ ਹੁੰਦੇ ਹੀ ਕਿਰਦਾਰ ਖਾਲੀ (void) ਵਿੱਚ ਜਾ ਡਿੱਗਦਾ ਸੀ—ਜਾਂ ਕਦੇ-ਕਦੇ ਫੋਕਸ ਗੁਆਉਣ 'ਤੇ ਹੀ ਤੁਰੰਤ। ਗੇਮ ਨੇ ਇੱਕ ਰੁਟੀਨ ਓਪਰੇਟਿੰਗ ਸਿਸਟਮ ਇੰਟਰਪਸ਼ਨ (interruption) ਨੂੰ ਜਾਣਬੁੱਝ ਕੇ ਬਟਨ ਛੱਡਣ ਵਜੋਂ ਲਿਆ ਸੀ। ਰਨ ਗ਼ੈਰ-ਵਾਜਬ ਤਰੀਕੇ ਨਾਲ ਖਤਮ ਹੋ ਜਾਂਦੀਆਂ ਸਨ। ਕੰਟਰੋਲਸ 'ਤੇ ਭਰੋਸਾ ਘਟਣ ਲੱਗ ਪਿਆ।
ਮੂਲ ਕਾਰਨ: ਇੱਕ ਈਵੈਂਟ (Event) ਦੋ ਕੰਮ ਕਰ ਰਿਹਾ ਹੈ
ਬੱਗ (bug) ਬਾਰੀਕ ਸੀ ਪਰ ਸਿੱਧਾ ਸੀ। ਅਸਲ ਇਨਪੁਟ ਲੇਅਰ ਵਿੱਚ, ਕੋਡ ਨੇ ਜੰਪ ਰਿਲੀਜ਼ ਲੌਜਿਕ (jump release logic) ਨੂੰ ਸਿੱਧਾ ਵਿੰਡੋ ਦੇ blur event ਨਾਲ ਜੋੜ ਦਿੱਤਾ ਸੀ:
window.addEventListener("blur", releaseCharge);
ਜੇਕਰ ਤੁਸੀਂ ਧਿਆਨ ਨਾਲ ਦੇਖੋ ਤਾਂ ਇਹ ਜਾਇਜ਼ ਲੱਗਦਾ ਹੈ। ਖਿਡਾਰੀ ਇੱਕ ਕੀ (key) ਜਾਂ ਪੁਆਇੰਟਰ (pointer) ਨੂੰ ਦਬਾ ਕੇ ਰੱਖ ਰਿਹਾ ਸੀ; ਹੁਣ ਕੁਝ ਰੁਕ ਗਿਆ। ਪਰ blur event ਕੋਈ input event ਨਹੀਂ ਹੈ। ਇਹ ਇੱਕ ਵਿੰਡੋ ਮੈਨੇਜਮੈਂਟ ਸਿਗਨਲ ਹੈ। ਇਹ ਉਦੋਂ ਫਾਇਰ ਹੁੰਦਾ ਹੈ ਜਦੋਂ ਬ੍ਰਾਊਜ਼ਰ ਟੈਬ ਓਪਰੇਟਿੰਗ ਸਿਸਟਮ ਫੋਕਸ ਗੁਆ ਲੈਂਦਾ ਹੈ, ਜੋ ਕਿ ਉਦੋਂ ਹੋ ਸਕਦਾ ਹੈ ਜਦੋਂ ਖਿਡਾਰੀ ਟੈਬ ਬਦਲਦਾ ਹੈ, ਵਿੰਡੋ ਨੂੰ ਮਿਨੀਮਾਈਜ਼ ਕਰਦਾ ਹੈ, ਕਿਸੇ ਬਾਹਰੀ ਮਾਨੀਟਰ 'ਤੇ ਕਲਿੱਕ ਕਰਦਾ ਹੈ, ਜਾਂ ਜਦੋਂ ਕੋਈ ਸਿਸਟਮ ਨੋਟੀਫਿਕੇਸ਼ਨ ਫੋਕਸ ਖੋਹ ਲੈਂਦਾ ਹੈ। ਇਹਨਾਂ ਵਿੱਚੋਂ ਕੋਈ ਵੀ ਐਕਸ਼ਨ ਇਹ ਨਹੀਂ ਦਰਸਾਉਂਦਾ ਕਿ "ਮੈਂ ਆਪਣੇ ਕਿਰਦਾਰ ਨੂੰ ਲਾਂਚ ਕਰਨਾ ਚਾਹੁੰਦਾ ਹਾਂ।" ਇਹਨਾਂ ਦਾ ਮਤਲਬ ਹੈ "ਮੈਂ ਗੇਮ ਤੋਂ ਬਾਹਰ ਕਿਸੇ ਚੀਜ਼ ਨਾਲ ਇੰਟਰੈਕਟ ਕਰ ਰਿਹਾ ਹਾਂ।"
blur ਨੂੰ releaseCharge ਵਿੱਚ ਰੂਟ ਕਰਕੇ, ਗੇਮ ਨੇ ਦੋ ਬਿਲਕੁਲ ਵੱਖਰੇ ਸੰਕਲਪਾਂ ਨੂੰ ਮਿਲਾ ਦਿੱਤਾ: ਇੱਕ ਜਾਣਬੁੱਝ ਕੇ ਰੁਕਣਾ (ਖਿਡਾਰੀ ਬਟਨ ਛੱਡ ਦਿੰਦਾ ਹੈ) ਅਤੇ ਇੱਕ ਬਾਹਰੀ ਵਿਘਨ (ਬ੍ਰਾਊਜ਼ਰ ਹੁਣ ਐਕਟਿਵ ਵਿੰਡੋ ਨਹੀਂ ਹੈ)। ਕਿਉਂਕਿ releaseCharge ਮੌਜੂਦਾ ਚਾਰਜ ਸਟੇਟ ਦੇ ਅਧਾਰ 'ਤੇ ਜੰਪ ਫੋਰਸ ਦੀ ਗਣਨਾ ਕਰਦਾ ਸੀ ਅਤੇ ਤੁਰੰਤ ਵੇਲੋਸਿਟੀ (velocity) ਲਾਗੂ ਕਰਦਾ ਸੀ, ਚਾਰਜ ਦੇ ਵਿਚਕਾਰ ਕਿਸੇ ਵੀ ਫੋਕਸ ਦੇ ਗੁਆਉਣ ਨਾਲ ਜਿੰਨੀ ਵੀ ਸ਼ਕਤੀ ਇਕੱਠੀ ਹੋ ਚੁੱਕੀ ਸੀ, ਉਸ ਨਾਲ ਲਾਂਚ ਟ੍ਰਿਗਰ ਹੋ ਜਾਂਦਾ ਸੀ। ਖਿਡਾਰੀ ਵਾਪਸ ਆ ਕੇ ਦੇਖਦਾ ਸੀ ਕਿ ਉਸਦਾ ਕਿਰਦਾਰ ਮਰ ਚੁੱਕਾ ਹੈ ਜਾਂ ਉਸਦੀ ਪ੍ਰਗਤੀ ਉਸ ਅੰਦੋਲਨ (move) ਕਾਰਨ ਖਰਾਬ ਹੋ ਗਈ ਹੈ ਜਿਸਦੀ ਉਸਨੇ ਕਦੇ ਇਜਾਜ਼ਤ ਨਹੀਂ ਦਿੱਤੀ ਸੀ।
Three.js ਡਿਵੈਲਪਰਾਂ ਲਈ ਬ੍ਰਾਊਜ਼ਰ ਦੀਆਂ ਅਸਲੀਅਤਾਂ
Three.js ਤੁਹਾਨੂੰ ਇੱਕ ਸ਼ਕਤੀਸ਼ਾਲੀ 3D ਕੈਨਵਸ (canvas) ਦਿੰਦਾ ਹੈ, ਪਰ ਇਨਪੁਟ ਅਜੇ ਵੀ DOM ਰਾਹੀਂ ਵਹਿੰਦਾ ਹੈ। ਉਹ ਵੰਡ ਮਹੱਤਵਪੂਰਨ ਹੈ। ਬ੍ਰਾਊਜ਼ਰ ਨੂੰ ਕੁਦਰਤੀ ਤੌਰ 'ਤੇ ਇਹ ਨਹੀਂ ਪਤਾ ਹੁੰਦਾ ਕਿ ਸਪੇਸਬਾਰ (spacebar) ਨੂੰ ਦਬਾ ਕੇ ਰੱਖਣ ਨਾਲ ਜੰਪ ਚਾਰਜ ਹੁੰਦਾ ਹੈ। ਇਸਨੂੰ ਸਿਰਫ਼ ਇਹ ਪਤਾ ਹੁੰਦਾ ਹੈ ਕਿ ਇੱਕ ਕੀ (key) ਦਬਾਈ ਗਈ ਹੈ। ਜਦੋਂ ਫੋਕਸ ਡੌਕੂਮੈਂਟ ਤੋਂ ਬਾਹਰ ਜਾਂਦਾ ਹੈ, ਤਾਂ ਬ੍ਰਾਊਜ਼ਰ ਹਰ ਦਬਾਈ ਹੋਈ ਕੀ ਲਈ ਆਪਣੇ ਆਪ keyup ਨਹੀਂ ਬਣਾਉਂਦਾ। ਇਸ ਦੀ ਬਜਾਏ, ਇਹ ਤੁਹਾਨੂੰ ਦੱਸਦਾ ਹੈ ਕਿ ਵਿੰਡੋ ਚਲੀ ਗਈ ਹੈ। ਜੇਕਰ ਤੁਹਾਡਾ ਗੇਮ ਲੌਜਿਕ ਇਹ ਮੰਨਦਾ ਹੈ ਕਿ ਫੋਕਸ ਦੀ ਗੈਰ-ਹਾਜ਼ਰੀ ਦਾ ਮਤਲਬ ਇਨਪੁਟ ਦੀ ਗੈਰ-ਹਾਜ਼ਰੀ ਹੈ, ਤਾਂ ਤੁਹਾਨੂੰ ਫੈਂਟਮ ਐਕਸ਼ਨ (phantom actions) ਮਿਲਦੇ ਹਨ।
ਇਹ ਅੰਤਰ ਖਾਸ ਤੌਰ 'ਤੇ ਚਾਰਜ-ਅੱਪ ਮਕੈਨਿਕਸ ਲਈ ਮਹੱਤਵਪੂਰਨ ਹੈ, ਜੋ ਕਿ ਹਰ ਜਗ੍ਹਾ ਦਿਖਾਈ ਦਿੰਦੇ ਹਨ: ਤੀਰ ਚਲਾਉਣਾ, ਵਾਹਨ ਦੀ ਰਫ਼ਤਾਰ ਵਧਾਉਣਾ, ਚਾਰਜਡ ਸਪੈਲ (spell) ਕੱਢਣਾ, ਜਾਂ ਸਟੈਮੀਨਾ ਵਿੰਡ-ਅੱਪ (stamina wind-up) ਨਾਲ ਦੌੜਨਾ। ਕੋਈ ਵੀ ਲਗਾਤਾਰ ਐਕਸ਼ਨ ਜੋ ਸਮੇਂ
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.
