From c4c7aed6edeff5427b93bd02c2203b7f36b26786 Mon Sep 17 00:00:00 2001 From: Trugath Date: Thu, 3 Oct 2024 18:12:33 +0100 Subject: [PATCH] Event Bus --- eventbus/defaulteventbus.gd | 43 +++++++++++++ eventbus/defaultscope.gd | 2 + eventbus/event.gd | 12 ++++ eventbus/eventbus.gd | 17 +++++ eventbus/eventbussubscription.gd | 19 ++++++ eventbus/eventresult.gd | 7 +++ eventbus/eventscope.gd | 2 + eventbus/subscription.gd | 5 ++ input_event.gd | 10 +++ main.gd | 35 +++++++++++ main.tscn | 6 +- project.godot | 4 +- spaceship.gd | 25 ++++---- tests.tscn | 6 ++ tests/test_eventbus.gd | 105 +++++++++++++++++++++++++++++++ tests/tests.gd | 8 +++ weapons_system.gd | 1 + 17 files changed, 290 insertions(+), 17 deletions(-) create mode 100644 eventbus/defaulteventbus.gd create mode 100644 eventbus/defaultscope.gd create mode 100644 eventbus/event.gd create mode 100644 eventbus/eventbus.gd create mode 100644 eventbus/eventbussubscription.gd create mode 100644 eventbus/eventresult.gd create mode 100644 eventbus/eventscope.gd create mode 100644 eventbus/subscription.gd create mode 100644 input_event.gd create mode 100644 main.gd create mode 100644 tests.tscn create mode 100644 tests/test_eventbus.gd create mode 100644 tests/tests.gd diff --git a/eventbus/defaulteventbus.gd b/eventbus/defaulteventbus.gd new file mode 100644 index 0000000..436e8fc --- /dev/null +++ b/eventbus/defaulteventbus.gd @@ -0,0 +1,43 @@ +class_name DefaultEventBus +extends EventBus + +var subscribers: Dictionary = {} + +func _init(): + subscribers = {} + +func subscribers_of(event_scope: EventScope, event_type: String) -> Array[Subscription]: + var key = [event_scope, event_type] + return subscribers.get(key, []) + +func subscribe_to(event_scope: EventScope, event_type: String, fn: Callable) -> Subscription: + var key = [event_scope, event_type] + var subscriber_set: Array[EventBusSubscription] = subscribers.get(key, [] as Array[EventBusSubscription]) + var subscription = EventBusSubscription.new(key, fn, self) + subscriber_set.append(subscription) + subscribers[key] = subscriber_set + return subscription + +func publish(event_scope: EventScope, event: Event) -> void: + var key = [event_scope, event.get_type()] + var subscriber_set: Array[EventBusSubscription] = subscribers.get(key, [] as Array[EventBusSubscription]).duplicate(true) + for subscriber in subscriber_set: + var result = subscriber.callback.call(event) + if result == EventResult.ResultType.DISPOSE_SUBSCRIPTION: + subscriber.dispose() + +func cancel_scope(event_scope: EventScope) -> void: + var keys_to_cancel: Array = [] + for key in subscribers.keys(): + if key[0] == event_scope: + keys_to_cancel.append(key) + for key in keys_to_cancel: + var subscriber_set: Array[EventBusSubscription] = subscribers[key] + for subscriber in subscriber_set: + subscriber.dispose() + +func dispose() -> void: + for subscriber_set in subscribers.values(): + for subscriber in subscriber_set: + subscriber.dispose() + subscribers.clear() diff --git a/eventbus/defaultscope.gd b/eventbus/defaultscope.gd new file mode 100644 index 0000000..dc0820d --- /dev/null +++ b/eventbus/defaultscope.gd @@ -0,0 +1,2 @@ +class_name DefaultScope +extends EventScope diff --git a/eventbus/event.gd b/eventbus/event.gd new file mode 100644 index 0000000..a96f561 --- /dev/null +++ b/eventbus/event.gd @@ -0,0 +1,12 @@ +class_name Event +extends RefCounted + +var type: String +var source: Variant + +func _init(_source: Variant = null): + source = _source + type = self.get_class() + +func get_type() -> String: + return type diff --git a/eventbus/eventbus.gd b/eventbus/eventbus.gd new file mode 100644 index 0000000..442dd52 --- /dev/null +++ b/eventbus/eventbus.gd @@ -0,0 +1,17 @@ +class_name EventBus +extends Node + +func subscribers_of(_event_scope: EventScope, _event_type: String) -> Array[Subscription]: + return Array() + +func subscribe_to(_event_scope: EventScope, _event_type: String, _fn: Callable) -> Subscription: + return Subscription.new() + +func publish(_event_scope: EventScope, _event: Event) -> void: + pass + +func cancel_scope(_event_scope: EventScope) -> void: + pass + +func dispose() -> void: + pass diff --git a/eventbus/eventbussubscription.gd b/eventbus/eventbussubscription.gd new file mode 100644 index 0000000..28f9068 --- /dev/null +++ b/eventbus/eventbussubscription.gd @@ -0,0 +1,19 @@ +class_name EventBusSubscription +extends Subscription + +var key: Array +var callback: Callable +var bus: DefaultEventBus + +func _init(_key: Array, _callback: Callable, _bus: DefaultEventBus): + key = _key + callback = _callback + bus = _bus + +func dispose() -> void: + var subscriber_set: Array[EventBusSubscription] = bus.subscribers.get(key, []) + subscriber_set.erase(self) + if subscriber_set.is_empty(): + bus.subscribers.erase(key) + else: + bus.subscribers[key] = subscriber_set diff --git a/eventbus/eventresult.gd b/eventbus/eventresult.gd new file mode 100644 index 0000000..5087cfc --- /dev/null +++ b/eventbus/eventresult.gd @@ -0,0 +1,7 @@ +class_name EventResult +extends RefCounted + +enum ResultType { + KEEP_SUBSCRIPTION, + DISPOSE_SUBSCRIPTION +} diff --git a/eventbus/eventscope.gd b/eventbus/eventscope.gd new file mode 100644 index 0000000..395b780 --- /dev/null +++ b/eventbus/eventscope.gd @@ -0,0 +1,2 @@ +class_name EventScope +extends Node diff --git a/eventbus/subscription.gd b/eventbus/subscription.gd new file mode 100644 index 0000000..3472e7f --- /dev/null +++ b/eventbus/subscription.gd @@ -0,0 +1,5 @@ +class_name Subscription +extends RefCounted + +func dispose() -> void: + pass diff --git a/input_event.gd b/input_event.gd new file mode 100644 index 0000000..a5baedf --- /dev/null +++ b/input_event.gd @@ -0,0 +1,10 @@ +class_name KeyboardInputEvent +extends Event + +var action: String +var pressed: bool + +func _init(_action: String, _pressed: bool): + action = _action + pressed = _pressed + type = "KeyboardInputEvent" diff --git a/main.gd b/main.gd new file mode 100644 index 0000000..aa8774f --- /dev/null +++ b/main.gd @@ -0,0 +1,35 @@ +extends Node + +func _init() -> void: + var node = WeaponsSystem.new() + node.set_name("WeaponsSystem") + add_child(node) + + node = DefaultEventBus.new() + node.set_name("MainEventBus") + add_child(node) + + node = EventScope.new() + node.set_name("InputEventScope") + add_child(node) + +# Called when the node enters the scene tree for the first time. +func _ready() -> void: + pass # Replace with function body. + + +# Called every frame. 'delta' is the elapsed time since the previous frame. +func _process(delta: float) -> void: + var actions = [ + "ui_fire", + "ui_up", + "ui_down", + "ui_left", + "ui_right", + ] + for action in actions: + if Input.is_action_pressed(action): + get_node("/root/Main/MainEventBus").publish(get_node("/root/Main/InputEventScope"), KeyboardInputEvent.new(action, true)) + elif Input.is_action_just_released(action): + get_node("/root/Main/MainEventBus").publish(get_node("/root/Main/InputEventScope"), KeyboardInputEvent.new(action, false)) + pass diff --git a/main.tscn b/main.tscn index e2b84ac..6cff3f0 100644 --- a/main.tscn +++ b/main.tscn @@ -1,10 +1,10 @@ [gd_scene load_steps=13 format=3 uid="uid://8ocp10j32f62"] +[ext_resource type="Script" path="res://main.gd" id="1_eedai"] [ext_resource type="Texture2D" uid="uid://y6phkg4twpdm" path="res://images/bg_space_seamless.png" id="1_rpyi5"] [ext_resource type="Texture2D" uid="uid://cran7fr1i2qou" path="res://images/spaceship-placeholder.png" id="2_f2x66"] [ext_resource type="Script" path="res://spaceship.gd" id="3_ttkgl"] [ext_resource type="Script" path="res://laser.gd" id="4_uhf7q"] -[ext_resource type="Script" path="res://weapons_system.gd" id="5_gf6oh"] [ext_resource type="Script" path="res://asteroid.gd" id="6_n4dsl"] [ext_resource type="Texture2D" uid="uid://bxgw2u7j4b634" path="res://images/laser_turret.png" id="6_qxhyw"] [ext_resource type="Texture2D" uid="uid://buirp1h6onqai" path="res://images/AsteroidBrown.png" id="7_0tjls"] @@ -22,6 +22,7 @@ colors = PackedColorArray(1, 1, 0, 1, 1, 1, 1, 0.137255) radius = 88.0057 [node name="Main" type="Node2D"] +script = ExtResource("1_eedai") [node name="background" type="Sprite2D" parent="."] position = Vector2(955, 537) @@ -70,9 +71,6 @@ offset_top = -71.0 offset_right = 104.0 offset_bottom = -20.0 -[node name="WeaponsSystem" type="Node" parent="spaceship"] -script = ExtResource("5_gf6oh") - [node name="HardPoint1" type="Node2D" parent="spaceship"] position = Vector2(0, -46) script = ExtResource("7_6cr6a") diff --git a/project.godot b/project.godot index a8a4f4b..bcda1c8 100644 --- a/project.godot +++ b/project.godot @@ -12,7 +12,7 @@ config_version=5 config/name="Star Cheese - The Final Fromage" run/main_scene="res://main.tscn" -config/features=PackedStringArray("4.2", "Forward Plus") +config/features=PackedStringArray("4.3", "Forward Plus") config/icon="res://icon.svg" [display] @@ -24,7 +24,7 @@ window/size/viewport_height=1080 ui_fire={ "deadzone": 0.5, -"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":32,"echo":false,"script":null) +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null) ] } diff --git a/spaceship.gd b/spaceship.gd index 0a67e16..c822f60 100644 --- a/spaceship.gd +++ b/spaceship.gd @@ -8,10 +8,12 @@ extends CharacterBody2D signal fire # Signal to start firing signal stop_fire # Signal to stop firing -var weapons_system - +var weapons_system: WeaponsSystem +var event_bus: EventBus func _ready(): - weapons_system = $WeaponsSystem + weapons_system = get_node("/root/Main/WeaponsSystem") + event_bus = get_node("/root/Main/MainEventBus") + event_bus.subscribe_to(get_node("/root/Main/InputEventScope"), "KeyboardInputEvent", Callable(self, "handle_keyboard_input")) func get_spaceship_orientation_vector() -> Vector2: return -global_transform.y @@ -45,13 +47,14 @@ func handle_movement(delta: float) -> void: var collision_normal = collision.get_normal() velocity = velocity.slide(collision_normal) -func handle_weapons_input() -> void: - if Input.is_action_pressed("ui_fire"): - weapons_system.fire_all() - - if Input.is_action_just_released("ui_fire"): - weapons_system.cease_fire_all() - +func handle_keyboard_input(event: KeyboardInputEvent): + if( event.action == "ui_fire" ): + if( event.pressed ): + weapons_system.fire_all() + else: + weapons_system.cease_fire_all() + + pass + func _process(delta: float) -> void: handle_movement(delta) - handle_weapons_input() diff --git a/tests.tscn b/tests.tscn new file mode 100644 index 0000000..f073280 --- /dev/null +++ b/tests.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://dxxijo7vef7or"] + +[ext_resource type="Script" path="res://tests/tests.gd" id="1_dngg8"] + +[node name="tests" type="Node"] +script = ExtResource("1_dngg8") diff --git a/tests/test_eventbus.gd b/tests/test_eventbus.gd new file mode 100644 index 0000000..9b173ac --- /dev/null +++ b/tests/test_eventbus.gd @@ -0,0 +1,105 @@ +# res://tests/test_eventbus.gd +class_name EventBusTests +extends Node + +func _ready(): + run_tests() + +func run_tests(): + test_subscribe_and_publish() + test_dispose_subscription() + test_cancel_scope() + test_multiple_subscribers() + + # Test function: Test subscribing and publishing events +func test_subscribe_and_publish() -> void: + var bus: DefaultEventBus = DefaultEventBus.new() + var scope: DefaultScope = DefaultScope.new() + var event_received: Flag = Flag.new() + + var event_handler = func(_event: Event) -> int: + event_received.value = true + return EventResult.ResultType.KEEP_SUBSCRIPTION + + bus.subscribe_to(scope, "TestEvent", event_handler) + var event: TestEvent = TestEvent.new(self) + bus.publish(scope, event) + + # Assert that the event was received + assert(event_received.value == true, "Event was not received by subscriber") + bus.dispose() + + # Test function: Test disposing of a subscription after handling an event +func test_dispose_subscription() -> void: + var bus: DefaultEventBus = DefaultEventBus.new() + var scope: DefaultScope = DefaultScope.new() + var event_received_times: Counter = Counter.new() + + var event_handler = func(_event: Event) -> int: + event_received_times.value += 1 + return EventResult.ResultType.DISPOSE_SUBSCRIPTION + + bus.subscribe_to(scope, "TestEvent", event_handler) + var event: TestEvent = TestEvent.new(self) + bus.publish(scope, event) + bus.publish(scope, event) # Publish a second time + + # Assert that the event handler was called only once + assert(event_received_times.value == 1, "Event handler should have been called only once due to disposal") + bus.dispose() + + # Test function: Test canceling an event scope +func test_cancel_scope() -> void: + var bus: DefaultEventBus = DefaultEventBus.new() + var scope: DefaultScope = DefaultScope.new() + var event_received: Flag = Flag.new() + + var event_handler = func(_event: Event) -> int: + event_received.value = true + return EventResult.ResultType.KEEP_SUBSCRIPTION + + bus.subscribe_to(scope, "TestEvent", event_handler) + bus.cancel_scope(scope) + var event: TestEvent = TestEvent.new(self) + bus.publish(scope, event) + + # Assert that the event was not received after scope cancellation + assert(event_received.value == false, "Event should not have been received after scope cancellation") + bus.dispose() + + +# Test function: Test multiple subscribers receiving the same event +func test_multiple_subscribers() -> void: + var bus: DefaultEventBus = DefaultEventBus.new() + var scope: DefaultScope = DefaultScope.new() + var event_received_by_handler1: Flag = Flag.new() + var event_received_by_handler2: Flag = Flag.new() + + var event_handler1 = func(_event: Event) -> int: + event_received_by_handler1.value = true + return EventResult.ResultType.KEEP_SUBSCRIPTION + + var event_handler2 = func(_event: Event) -> int: + event_received_by_handler2.value = true + return EventResult.ResultType.KEEP_SUBSCRIPTION + + bus.subscribe_to(scope, "TestEvent", event_handler1) + bus.subscribe_to(scope, "TestEvent", event_handler2) + var event: TestEvent = TestEvent.new(self) + bus.publish(scope, event) + + # Assert that both handlers received the event + assert(event_received_by_handler1.value == true, "First handler did not receive event") + assert(event_received_by_handler2.value == true, "Second handler did not receive event") + bus.dispose() + +class Flag: + var value = false + +class Counter: + var value = 0 + +class TestEvent extends Event: + func _init(_source: Variant = null) -> void: + super(_source) + type = "TestEvent" diff --git a/tests/tests.gd b/tests/tests.gd new file mode 100644 index 0000000..d53b6a2 --- /dev/null +++ b/tests/tests.gd @@ -0,0 +1,8 @@ +extends Node + +func _ready() -> void: + EventBusTests.run_tests() + get_tree().quit() + +func _process(_delta: float) -> void: + pass diff --git a/weapons_system.gd b/weapons_system.gd index 4050259..23e70e2 100644 --- a/weapons_system.gd +++ b/weapons_system.gd @@ -1,3 +1,4 @@ +class_name WeaponsSystem extends Node var weapons = []