popcar2 / GodotOS

A Fake Operating System Interface made in Godot!
GNU Affero General Public License v3.0
1.09k stars 55 forks source link

Unfocused window responds to Input #18

Open PJ-568 opened 7 months ago

PJ-568 commented 7 months ago

Issue

All windows receive and handle (keyboard) input when more than one window is opened.

Plan

Create an InputEvent manager which register the windows (or other node) that is focused and pass InputEvent to it.

Draft

Below is some random copys of similar things form my project, please ignore diff.


extends Node
class_name InputManager

@export var camera:Camera3D

@export var initialObject:Node var currentObject:Node = initialObject var allObjects:Array = []

func registerObject(object:Node) -> void: ## Register and switch to the object that needs to receive input events if !allObjects.has(object): if getParentObject(object): getParentObject(object).exit() object.registerObject.connect(registerObject) object.removeObject.connect(removeObject) object.enter() allObjects.append(object) currentObject = object

func removeObject(object:Node) -> void: ## If a parent object exists, recursively unregister the object and its child nodes and switch to the parent object var parentObject := getParentObject(object) if allObjects.has(object) and parentObject: object.registerObject.disconnect(registerObject) object.removeObject.disconnect(removeObject) allObjects.erase(object) var childObject := getChildObject(object) if childObject: removeObject(childObject) parentObject.enter() currentObject = parentObject object.exit()

func getChildObject(object:Node) -> Node: if allObjects.size() > 1: if allObjects.has(object) and allObjects.find(object) < allObjects.size() - 1: return allObjects[allObjects.find(object) + 1] return null

func getParentObject(object:Node) -> Node: if allObjects.size() > 1: if allObjects.has(object) and allObjects.find(object) > 0: return allObjects[allObjects.find(object) - 1] return null

func _ready() -> void:

Initialize objects

if initialObject:
    registerObject(initialObject)
else:
    push_warning(self.name, ": Initial object not set")
# Capture mouse pointer
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
# Initialize camera settings
camera.fov = Sett.field_of_view
camera.current = true

func _input(event:InputEvent): if currentObject: currentObject.handleInputEvent(event)

func _unhandled_input(event:InputEvent) -> void:

Press "Back" to pause and release mouse capture.

if Input.is_action_just_pressed("back") or Input.is_action_just_pressed("ui_cancel"):
    Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
    Global.getUIManager().openUI("PauseUI")
    Global.pause()
if currentObject:
    currentObject.remainingInputEvent(event)

func _process(delta:float) -> void: if currentObject: camera.global_transform = currentObject.camera_position.global_transform currentObject.updateFrame(delta)

func _physics_process(delta:float): if currentObject: currentObject.physicsUpdate(delta)

popcar2 commented 7 months ago

Thanks for looking into this. Most windows do separate keyboard input, but some games don't because they directly use Input in physics process instead of the _input functions. I'm not 100% sure if there's a way to stop a node from receiving input via the Input singleton, even with that script.

HVukman commented 7 months ago

Couldn't you send the signal focus_exited() when the focus is exited. Then some boolean variable could block input.

popcar2 commented 7 months ago

Then some boolean variable could block input.

I tried to do this in game_window.gd using node.set_process_input(false), but it turns out that only disables the _input() function and doesn't stop it from using the Input singleton, which means games will probably use input anyways in process functions. As far as I know there isn't really a catch-all way to disable input.

PJ-568 commented 7 months ago

Then some boolean variable could block input.

I tried to do this in game_window.gd using node.set_process_input(false), but it turns out that only disables the _input() function and doesn't stop it from using the Input singleton, which means games will probably use input anyways in process functions. As far as I know there isn't really a catch-all way to disable input.


My solution

My solution is to split _process(delta:float) (or _physics_process(delta:float)) into two parts:

  1. The functions which uses Input.
  2. The functions which does not use Input.

The 2nd part will run only if it is focused. And everything works just fine.

Example

Something like this:

extends StateMachine
class_name CompatibleStateMachine

func _ready():
    for child_node in get_children():
        if child_node is State:
            all_states[child_node.name] = child_node
            child_node.state_changed.connect(on_child_state_changed)

    if initial_state:
        initial_state.enter()
        current_state = initial_state

### 1st part ###

func _input(event: InputEvent):
    if current_state:
        current_state.handle_input_event(event)

func _unhandled_input(event: InputEvent):
    if current_state:
        current_state.handle_remaining_input_event(event)

func _process(delta: float):
    if current_state:
        current_state.update_frame(delta)

func _physics_process(delta: float):
    if current_state:
        current_state.update_physics(delta)

### 2nd part ###

func handle_input_event(event: InputEvent):
    if current_state:
        current_state.handle_input_event(event)

func handle_remaining_input_event(event: InputEvent):
    if current_state:
        current_state.handle_remaining_input_event(event)

func update_per_frame(delta: float):
    if current_state:
        current_state.update_per_frame(delta)

func update_per_physics(delta: float):
    if current_state:
        current_state.update_per_physics(delta)

Upon is a state machine which is designed to be compatible with InputManager I metioned.

Sorry that I can not share all the code since the project is confidential for now.

PJ-568 commented 7 months ago

State example of walk:

extends CompatibleState
class_name PlayerState

@onready var machine = $".."
@onready var player = $"../.."

func enter():
    pass

func exit():
    pass

func handle_input_event(event: InputEvent):
    pass

func handle_remaining_input_event(event: InputEvent):
    pass

func update_per_frame(delta: float):
    pass

func update_per_physics(delta: float):
    if player.is_on_floor():
        elif player.direction:
            if Input.is_action_pressed("Sprint"):
                machine.change_state.emit(self, "Run")
            else:
                machine.change_state.emit(self, "Walk")
        else:
            machine.change_state.emit(self, "Idle")
    else:
        machine.change_state.emit(self, "Air")

func handle_input_event(event: InputEvent):
    pass

func handle_remaining_input_event(event: InputEvent):
    pass

func update_per_frame(delta: float):
    pass

func update_per_physics(delta: float):
    if player.is_on_floor():
        if Input.is_action_pressed("Down"):
            machine.change_state.emit(self, "Down")

extends PlayerState

func enter():
    player.appearance.walk_run(player.velocity.length())

func exit():
    player.acceleration = player.walk_acceleration

func update_per_frame(delta: float):
    origin.handle_camera_shake(delta)

func update_per_physics(delta: float):
    player.acceleration = lerp(player.acceleration, player.walk_acceleration, delta * player.acceleration_change_rate)
    if player.direction:
        player.velocity += (player.direction * player.acceleration * delta)
        player.appearance.walk_run(player.velocity.length())

    if player.is_on_floor():
        if Input.is_action_pressed("Down"):
            if player.velocity.length() > player.slide_start_threshold:
                machine.change_state.emit(self, "Slide")
            else:
                machine.change_state.emit(self, "Crouch")
        elif Input.is_action_just_released("Up"):
            machine.change_state.emit(self, "Jump")

Hope these help. :-)