Event Bus

This commit is contained in:
Elliot Stirling 2024-10-03 18:12:33 +01:00
parent aa424b1783
commit 7542472833
17 changed files with 290 additions and 17 deletions

View File

@ -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()

2
eventbus/defaultscope.gd Normal file
View File

@ -0,0 +1,2 @@
class_name DefaultScope
extends EventScope

12
eventbus/event.gd Normal file
View File

@ -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

17
eventbus/eventbus.gd Normal file
View File

@ -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

View File

@ -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

7
eventbus/eventresult.gd Normal file
View File

@ -0,0 +1,7 @@
class_name EventResult
extends RefCounted
enum ResultType {
KEEP_SUBSCRIPTION,
DISPOSE_SUBSCRIPTION
}

2
eventbus/eventscope.gd Normal file
View File

@ -0,0 +1,2 @@
class_name EventScope
extends Node

5
eventbus/subscription.gd Normal file
View File

@ -0,0 +1,5 @@
class_name Subscription
extends RefCounted
func dispose() -> void:
pass

10
input_event.gd Normal file
View File

@ -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"

35
main.gd Normal file
View File

@ -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

View File

@ -1,10 +1,10 @@
[gd_scene load_steps=13 format=3 uid="uid://8ocp10j32f62"] [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://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="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://spaceship.gd" id="3_ttkgl"]
[ext_resource type="Script" path="res://laser.gd" id="4_uhf7q"] [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="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://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"] [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 radius = 88.0057
[node name="Main" type="Node2D"] [node name="Main" type="Node2D"]
script = ExtResource("1_eedai")
[node name="background" type="Sprite2D" parent="."] [node name="background" type="Sprite2D" parent="."]
position = Vector2(955, 537) position = Vector2(955, 537)
@ -70,9 +71,6 @@ offset_top = -71.0
offset_right = 104.0 offset_right = 104.0
offset_bottom = -20.0 offset_bottom = -20.0
[node name="WeaponsSystem" type="Node" parent="spaceship"]
script = ExtResource("5_gf6oh")
[node name="HardPoint1" type="Node2D" parent="spaceship"] [node name="HardPoint1" type="Node2D" parent="spaceship"]
position = Vector2(0, -46) position = Vector2(0, -46)
script = ExtResource("7_6cr6a") script = ExtResource("7_6cr6a")

View File

@ -12,7 +12,7 @@ config_version=5
config/name="Star Cheese - The Final Fromage" config/name="Star Cheese - The Final Fromage"
run/main_scene="res://main.tscn" 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" config/icon="res://icon.svg"
[display] [display]
@ -24,7 +24,7 @@ window/size/viewport_height=1080
ui_fire={ ui_fire={
"deadzone": 0.5, "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)
] ]
} }

View File

@ -8,10 +8,12 @@ extends CharacterBody2D
signal fire # Signal to start firing signal fire # Signal to start firing
signal stop_fire # Signal to stop firing signal stop_fire # Signal to stop firing
var weapons_system var weapons_system: WeaponsSystem
var event_bus: EventBus
func _ready(): 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: func get_spaceship_orientation_vector() -> Vector2:
return -global_transform.y return -global_transform.y
@ -45,13 +47,14 @@ func handle_movement(delta: float) -> void:
var collision_normal = collision.get_normal() var collision_normal = collision.get_normal()
velocity = velocity.slide(collision_normal) velocity = velocity.slide(collision_normal)
func handle_weapons_input() -> void: func handle_keyboard_input(event: KeyboardInputEvent):
if Input.is_action_pressed("ui_fire"): if( event.action == "ui_fire" ):
weapons_system.fire_all() if( event.pressed ):
weapons_system.fire_all()
else:
weapons_system.cease_fire_all()
if Input.is_action_just_released("ui_fire"): pass
weapons_system.cease_fire_all()
func _process(delta: float) -> void: func _process(delta: float) -> void:
handle_movement(delta) handle_movement(delta)
handle_weapons_input()

6
tests.tscn Normal file
View File

@ -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")

105
tests/test_eventbus.gd Normal file
View File

@ -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"

8
tests/tests.gd Normal file
View File

@ -0,0 +1,8 @@
extends Node
func _ready() -> void:
EventBusTests.run_tests()
get_tree().quit()
func _process(_delta: float) -> void:
pass

View File

@ -1,3 +1,4 @@
class_name WeaponsSystem
extends Node extends Node
var weapons = [] var weapons = []