{"id":33012991,"url":"https://github.com/TheWalruzz/godot-sx","last_synced_at":"2025-11-18T11:02:26.312Z","repository":{"id":149366997,"uuid":"621324335","full_name":"TheWalruzz/godot-sx","owner":"TheWalruzz","description":"Signal Extensions for Godot 4","archived":false,"fork":false,"pushed_at":"2025-03-05T06:42:58.000Z","size":130,"stargazers_count":79,"open_issues_count":1,"forks_count":4,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-04-10T01:13:23.558Z","etag":null,"topics":["gdscript","godot","godot-engine","godot-engine-4","rx"],"latest_commit_sha":null,"homepage":"","language":"GDScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/TheWalruzz.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-03-30T12:43:28.000Z","updated_at":"2025-04-05T21:46:40.000Z","dependencies_parsed_at":null,"dependency_job_id":"943a9993-3706-4b39-a4aa-7f5971b6128d","html_url":"https://github.com/TheWalruzz/godot-sx","commit_stats":null,"previous_names":[],"tags_count":25,"template":false,"template_full_name":null,"purl":"pkg:github/TheWalruzz/godot-sx","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TheWalruzz%2Fgodot-sx","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TheWalruzz%2Fgodot-sx/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TheWalruzz%2Fgodot-sx/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TheWalruzz%2Fgodot-sx/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/TheWalruzz","download_url":"https://codeload.github.com/TheWalruzz/godot-sx/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TheWalruzz%2Fgodot-sx/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":285051560,"owners_count":27106642,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-11-18T02:00:05.759Z","response_time":61,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["gdscript","godot","godot-engine","godot-engine-4","rx"],"created_at":"2025-11-13T17:00:27.800Z","updated_at":"2025-11-18T11:02:26.306Z","avatar_url":"https://github.com/TheWalruzz.png","language":"GDScript","funding_links":[],"categories":["Plugins and scripts"],"sub_categories":["XR"],"readme":"# GodotSx - Signal Extensions for Godot 4\n\nHave you ever wanted to do more things with Godot's signals? Maybe merge them or filter them Rx-style?\nThen Signal Extensions (or Sx for short) are for you. This simple and lightweight library allows you to do basic operations on regular Signals\nand treat them like reactive-ish streams. \n\n*DISCLAIMER: This addon is NOT an Rx implementation. Therefore, it does not, nor will it ever, implement all the functionality and operators of Rx.\nIf you need a proper Rx implementation for Godot 4, see this excellent project: [GodotRx](https://github.com/Neroware/GodotRx) that Sx is largely inspired by.*\n\n## Motivation\nThe main goal of this library is to allow for more advanced signal handling in Godot.\n\nGodot's built-in signal system is robust, but could really profit from reactive approach, which works pretty well with games. Especially when a lot of components interact with each other.\n\nThose kind of manipulations could be done by an Rx framework (like GodotRx mentioned above), but this means including a package, that due to the sheer scale of mechanisms needed for pure Rx to work, is really big and bloated for use cases you might actually encounter in your code.\n\nInstead of including a big and complex Rx solution, why not extend the existing mechanism?\n\nSx allows for signal manipulation that feels like Rx, without all the overhead.\n\n## Installation\n* Copy `addons/signal_extensions` directory to the `/addons/` directory in your project\n* Enable `SignalExtensions` plugin in Project Settings -\u003e Plugins\n* You're done! You now have access to Sx singleton!\n\n\n## Basic usage\nIn order to perform operations on signals, they have to be converted to SxSignal:\n\t\n```gdscript\nsignal my_signal\n\nvar my_wrapped_signal := Sx.from(my_signal)\n```\n\n### Subscription\nTo subscribe to emissions, use `.subscribe(Callable)`:\n\t\n```gdscript\nsignal my_signal\n\nSx.from(my_signal).subscribe(func(): print(\"Got it!\"))\nmy_signal.emit()\n\n# result:\n# \tGot it!\n```\n\nMuch like when connecting to native signals, some flags can be passed to control the signal connection. See: enum Object.ConnectFlags.\nPlease note that using *CONNECT_ONE_SHOT* might break the subscription system of GodotSx. Use *.first()* operator instead.\n\n```gdscript\nsignal my_signal\n\n# this will defer emissions to idle frame, instead of sending them immediately.\nSx.from(my_signal).subscribe(func(): pass, CONNECT_DEFERRED)\n```\n\n### Filtering and mapping\nYou can filter and map emitted items much like in regular Rx implementations:\n\n```gdscript\nsignal value_changed(int)\n\n# multiply only positive numbers by 2\nSx.from(value_changed) \\\n\t.filter(func(value: int): return value \u003e 0) \\\n\t.map(func(value: int): return value * 2) \\\n\t.subscribe(func(value: int): print(value))\n\nvalue_changed.emit(-2)\nvalue_changed.emit(-1)\nvalue_changed.emit(0)\nvalue_changed.emit(1)\nvalue_changed.emit(2)\n\n# result:\n# \t2\n# \t4\n```\n\nSx supports signals with up to 6 arguments and they can also be filtered and mapped:\n\n```gdscript\nsignal multi_values(int, int2)\n\n# when mapping multiple values, array must be returned from lambda\nSx.from(multi_values) \\\n\t.filter(func(value1: int, value2: int): return value2 \u003e value1) \\\n\t.map(func(value1: int, value2: int): return [value2, value1]) \\\n\t.subscribe(func(value1: int, value2: int): print(value1, \" \", value2))\n\nmulti_values.emit(2, 1)\t\nmulti_values.emit(3, 10)\n\n# result:\n#\t10 3\n```\n\nNumber of arguments down the chain can be freely changed using map:\n\n```gdscript\nsignal int_values(int)\n\nSx.from(int_values) \\\n\t.map(func(value: int): return [value, value * 2]) \\\n\t.subscribe(func(value1: int, value2: int): print(value1, \" \", value2))\n\t\nint_values.emit(3)\n\n# result:\n#\t3 6\n```\n\n### Merging signals\nMultiple Godot signals can be merged into one using `Sx.merge_from()`:\n\n```gdscript\nsignal signal1(int)\nsignal signal2(int)\n\nSx.merge_from([signal1, signal2]).subscribe(func(value: int): print(value))\nsignal1.emit(1)\nsignal2.emit(2)\n\n# result:\n#\t1\n#\t2\n```\n\nMultiple SxSignals can also be merged easily using `Sx.merge()`:\n\n```gdscript\nsignal signal1(int)\nsignal signal2(int)\n\nSx.merge([\n\tSx.from(signal1).map(func(value: int): return value * 2),\n\tSx.from(signal2).map(func(value: int): return value * 3)\n]).subscribe(func(value: int): print(value))\nsignal1.emit(1)\nsignal2.emit(2)\n\n# result:\n#\t2\n#\t6\n```\n\nAlternatively, you can use `merge` operator:\n\n```gdscript\nsignal signal1(int)\nsignal signal2(int)\n\nSx.from(signal1).map(func(value: int): return value * 2).merge([\n\tSx.from(signal2).map(func(value: int): return value * 3)\n]).subscribe(func(value: int): print(value))\nsignal1.emit(1)\nsignal2.emit(2)\n\n# result:\n#\t2\n#\t6\n```\n\n### Timers\nTo simplify creation of periodic interval timers (without the hassle of creating and managing a Timer node yourself),\nyou can use `Sx.interval_timer()` to create a SxSignal that will periodically emit items.\n\n```gdscript\nSx.interval_timer(1.0).subscribe(func(): print(\"Tick!\"))\n# prints 'Tick!' every second\n```\n\n`interval_timer()` also accepts optional parameters to control the process mode and process callback of the timer.\nTo see more information about what they do, check Godot docs about Node and Timer respectively.\n\n```gdscript\nSx.interval_timer(\n\t1.0,\n\tNode.PROCESS_MODE_ALWAYS,\n\tTimer.TIMER_PROCESS_PHYSICS\n).subscribe(func(): print(\"Tick!\"))\n```\n\nThis Timer will automatically destroy itself once all the subscriptions are disposed.\n\nCreation of one-shot timers this way is not supported, but you can just do:\n\n```gdscript\nSx.from(get_tree().create_timer(1.0).timeout).subscribe(func(): print(\"Timeout!\"))\n\n# or:\n\nSx.interval_timer(1.0).first().subscribe(func(): print(\"Timeout!\"))\n```\n\n### Debouncing and throttling\nSx now also provides `debounce()` and `throttle()` operators, which either debounce or throttle the emission of signals based on provided time duration.\n\n```gdscript\nvar text_edit: TextEdit\n\nSx.from(text_edit.text_changed).debounce(0.25).subscribe(func(): print(text_edit.text)) # text will be printed to console only after 0.25 seconds elapsed since last keystroke.\nSx.from(text_edit.text_changed).throttle(0.25).subscribe(func(): print(text_edit.text)) # text will be printed every 0.25 seconds when typing continuously.\n```\n\n\n### Scan operator\nSx allows for scanning and buffering incoming values inside a stateful operator. This operator behaves similarly to `reduce()` in functional programming.\n\n```gdscript\nsignal numbers(value: int)\n\nSx.from(numbers).scan(\n\tfunc(acc: int, value: int):\n\t\treturn acc + value,\n\t0\n).subscribe(func(value: int): print(value))\n\nnumbers.emit(3)\nnumbers.emit(2)\nnumbers.emit(7)\n\n# result:\n#\t3\n#\t5\n#\t12\n```\n\nThis can also be useful if you want to collect previous emissions:\n```gdscript\nsignal numbers(value: int)\n\nSx.from(numbers).scan(\n\tfunc(acc: Array[int], value: int):\n\t\tacc.append(value)\n\t\treturn acc,\n\t[]\n).subscribe(func(value: Array[int]): print(value))\n\nnumbers.emit(3)\nnumbers.emit(2)\nnumbers.emit(7)\n\n# result:\n#\t[3]\n#\t[3, 2]\n#\t[3, 2, 7]\n```\n\nDue to the way the reducing function works, while multiple signal arguments will be passed to the function after the accumulator, this function should return one value.\nSubsequent operator after `scan` will receive ONE argument.\n\n### On complete callback\nWhen you're subscribing, you can set an optional callback that will be fired when the signal completes (either naturally, or when signal is disposed).\n\n```gdscript\nsignal numbers(int)\n\nSx.from(numbers).first().subscribe(\n\tfunc(value: int): print(value),\n\t0, # no connect flags\n\tfunc(): print(\"Completed\")\n)\nnumbers.emit(5)\n\n# result:\n#\t5\n#\tCompleted\n```\n\nOr:\n\t\n```gdscript\nsignal numbers(int)\n\nvar disposable := Sx.from(numbers).subscribe(\n\tfunc(value: int): print(value),\n\t0, # no connect flags\n\tfunc(): print(\"Completed\")\n)\nnumbers.emit(5)\ndisposable.dispose()\n\n# result:\n#\t5\n#\tCompleted\n```\n\n### Subscription disposal\nSignals can dispose themselves (and disconnect from signals) when some of operators are used. These include:\n* take_while\n* take\n* element_at\n* first\n\nWhen they finish their emissions, they dispose themselves according to the operator used.\n\n```gdscript\nsignal numbers(value)\n\n\nSx.from(numbers).take_while(func(value: int): return value \u003c 0) \\\n\t.subscribe(\n\t\tfunc(value: int): print(value),\n\t\t0, # no connect flags\n\t\tfunc(): print(\"Completed\")\n)\nnumbers.emit(-2)\nnumbers.emit(-1)\nnumbers.emit(0)\nnumbers.emit(1)\nnumbers.emit(2)\n\n# result:\n#\t-2\n#\t-1\n#\tCompleted\n```\n\n```gdscript\nsignal numbers(value)\n\n\nSx.from(numbers).first().subscribe(\n\t\tfunc(value: int): print(value),\n\t\t0, # no connect flags\n\t\tfunc(): print(\"Completed\")\n)\nnumbers.emit(-2)\nnumbers.emit(-1)\nnumbers.emit(0)\nnumbers.emit(1)\nnumbers.emit(2)\n\n# result:\n#\t-2\n#\tCompleted\n```\n\nYou might want to dispose SxSignals manually, or automatically when the subscribing Node exits the tree (both of which are highly recommended to make sure no accidental memory leaks occur).\n`subscribe()` method returns a SxDisposable object which allows for:\n* manual subscription disposal (and subsequent disconnection from signal) using `dispose()`\n* automatic disposal when Node is exitting the scene tree using `dispose_with(Node)`\n* adding disposable to SxCompositeDisposable using `dispose_with(SxCompositeDisposable)`\n\n```gdscript\nextends Node\n\nfunc _ready() -\u003e void:\n\tSx.from(some_other_node.my_signal).subscribe(func(): pass).dispose_with(self)\n```\n\nComposite disposable:\n\t\n```gdscript\nsignal test_signal\n\nvar composite_disposable := SxCompositeDisposable.new()\nSx.from(test_signal).subscribe(func(): print(\"First subscription\")).dispose_with(composite_disposable)\nSx.from(test_signal).subscribe(func(): print(\"Second subscription\")).dispose_with(composite_disposable)\ncomposite_disposable.dispose()\n```\n\nDisposables can also be added to a SxCompositeDisposable directly:\n\n```gdscript\nsignal some_signal\n\nvar composite_disposable := SxCompositeDisposable.new()\nvar disposable := Sx.from(some_signal).subscribe(func(): pass)\ncomposite_disposable.append(disposable)\ncomposite_disposable.dispose()\n```\n\n### Reactive properties\nSometimes, you need to store some values and react when they change. For this reason, Sx provides it's own implementation of Signal-based values, much like ReactiveProperties in GodotRx and UniRx.\n\n#### SxProperty\n```gdscript\nvar property := SxProperty.new(10)\nproperty.as_signal().subscribe(func(value: int): print(value))\nproperty.value = 15\n\n# result:\n#\t10\n#\t15\n```\n\nYou can also directly access the underlying signal:\n\n```gdscript\nproperty.value_changed.connect(func(value: int): print(value))\n```\n\nAlso, in case you don't want the initial emission when subscribing to SxProperty, you can pass false to `as_signal()`:\n\t\n```gdscript\nvar property := SxProperty.new(10)\nproperty.as_signal(false).subscribe(func(value: int): print(value))\nproperty.value = 15\n\n# result:\n#\t15\n```\n\n#### SxSignalProperty\nIn case you want to remember last emission from a signal, while keeping the ability to automatically update the value and notify subscribers,\nthe SxSignalProperty is for you. \nThis special type of SxProperty, receives a SxSignal and optional `initial_value`.\nIt will subscribe to the passed signal and assign the payload to the internal variable whenever emission occurs.\n\n```gdscript\nsignal number(value: int)\n\nvar property: SxProperty\n\nproperty = SxSignalProperty.new(\n\tSx.from(number).map(\n\t\tfunc(value: int):\n\t\t\treturn value * 2\n\t),\n\t0\n)\n\nprint(property.value)\nnumber.emit(2)\nprint(property.value)\n\n# result:\n#\t0\n#\t4\n```\n\nOf course, SxSignalProperty behaves just like SxProperty, so you can subscribe to that property using `as_signal()`:\n\n```gdscript\nsignal number(value: int)\n\nvar property: SxProperty\n\nproperty = SxSignalProperty.new(\n\tSx.from(number).map(\n\t\tfunc(value: int):\n\t\t\treturn value * 2\n\t),\n\t0\n)\n\nproperty.as_signal().subscribe(\n\tfunc(value: int):\n\t\tprint(value)\n)\n\nnumber.emit(2)\n\n# result:\n#\t0\n#\t4\n```\n\nSetting the value with `value` will work and will notify all subscribers, however that value will be replaced as soon as new emission occurs.\n\n#### SxArrayProperty and SxDictionaryProperty\nThere are also wrappers around Array and Dictionary, called SxArrayProperty and SxDictionaryProperty, but they're more complex.\nFor starters, all operations that result in count of items change, should be processed by the Sx wrapper, but everything else, \nlike filtering, and mapping items is allowed only through special getter *.value* which returns the underlying Array or Dictionary.\n\n```gdscript\nvar array := SxArrayProperty.new()\narray.as_signal().subscribe(func(type: SxArrayProperty.Event, current_array: Array, payload: Variant):\n\tprint(type, current_array, payload)\n)\narray.append(2)\n\n# result:\n#\tSxArrayProperty.Event.UPDATED_LIST [] []\n#\tSxArrayProperty.Event.UPDATED [2] 2\n#\tSxArrayProperty.Event.COUNT_CHANGED [2] 1\n```\n\n```gdscript\nvar dict := SxDictionaryProperty.new()\ndict.as_signal().subscribe(func(type: SxDictionaryProperty.Event, current: Dictionary, payload: Variant):\n\tprint(type, current, payload)\n)\ndict.set_value(\"test\", 2)\n\n# result:\n#\tSxDictionaryProperty.Event.UPDATED_LIST {} {}\n#\tSxDictionaryProperty.Event.UPDATED {\"test\":2} \"test\"\n#\tSxDictionaryProperty.Event.COUNT_CHANGED {\"test\":2} 1\n```\n\nWhen getting the underlying value, use *.value* or *.get_index()*/*.get_value()*\n\n```gdscript\nvar array := SxArrayProperty.new([1])\nprint(array.value[0])\nprint(array.get_index(0))\n\n# result:\n#\t1\n#\t1\n```\n\nTo observe specific events:\n\t\n```gdscript\nvar array := SxArrayProperty.new([1])\narray.observe(SxArrayProperty.Event.UPDATED).subscribe(func(current_array: Array, payload: Variant):\n\tprint(current_array, payload)\n)\n```\n\nBoth *.as_signal()* and *.observe()* can take an optional bool argument *emit_initial_value* which can be set to false.\nDoing so will not emit the current state when subscribing:\n\n```gdscript\nvar dict := SxDictionaryProperty.new()\ndict.as_signal(false).subscribe(func(type: SxDictionaryProperty.Event, current: Dictionary, payload: Variant):\n\tprint(type, current, payload)\n)\ndict.set_value(\"test\", 2)\n\n# result:\n#\tSxDictionaryProperty.Event.UPDATED {\"test\":2} \"test\"\n#\tSxDictionaryProperty.Event.COUNT_CHANGED {\"test\":2} 1\n```\n\nBoth SxArrayProperty and SxDictionaryProperty implement custom iterators, so they can iterated on in for loops:\n\n```gdscript\nvar array := SxArrayProperty.new([10, 20, 30])\nfor item in array:\n\tprint(item)\n\t\n# result:\n#\t10\n#\t20\n#\t30\n```\n\n```gdscript\nvar dict := SxDictionaryProperty.new({test1 = 1, test2 = 2})\nfor key in dict:\n\tprint(key)\n\t\n# result:\n#\ttest1\n#\ttest2\n```\n\n### All available operators\n* debounce\n* delay\n* element_at\n* filter\n* first\n* map\n* merge\n* merge_from\n* scan\n* skip\n* skip_while\n* start_with\n* take\n* take_while\n* throttle\n\nPlease note that full implementation of all Rx operators is NOT a goal of this library.\nIf you have a more complex problem that cannot be solved with Sx, then use GodotRx instead.\n\n# Unit tests\nThis library uses gdUnit4 as the unit test framework, but it is not provided in this repo. In order to run the tests, it needs to be installed manually (more information here: https://mikeschulze.github.io/gdUnit4/first_steps/install/). After that, tests can be run from the editor.\n\n# License\nDistributed under the [MIT License](https://github.com/TheWalruzz/godot-sx/blob/main/LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FTheWalruzz%2Fgodot-sx","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FTheWalruzz%2Fgodot-sx","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FTheWalruzz%2Fgodot-sx/lists"}