├── mvc.png ├── project ├── icon.png ├── default_env.tres ├── mvc-scene │ ├── view │ │ ├── user_attribute_view.gd │ │ └── user_attribute_view.tscn │ ├── handler │ │ ├── award_handler.gd │ │ └── battle_handler.gd │ ├── command │ │ ├── load_game_command.gd │ │ └── save_game_command.gd │ ├── proxy │ │ └── user_attribute_proxy.gd │ └── scene │ │ ├── main.gd │ │ └── main.tscn ├── mvc │ ├── event.gd │ ├── command.gd │ ├── event_center.gd │ ├── handler.gd │ ├── proxy.gd │ ├── test.gd │ └── app.gd ├── project.godot └── icon.png.import ├── games └── sokoban │ └── screenshot.jpg ├── .gitignore ├── LICENSE └── README.md /mvc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baiXfeng/godot-mvc/HEAD/mvc.png -------------------------------------------------------------------------------- /project/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baiXfeng/godot-mvc/HEAD/project/icon.png -------------------------------------------------------------------------------- /games/sokoban/screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baiXfeng/godot-mvc/HEAD/games/sokoban/screenshot.jpg -------------------------------------------------------------------------------- /project/default_env.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Environment" load_steps=2 format=3 uid="uid://chg83hqdfoyiq"] 2 | 3 | [sub_resource type="Sky" id="1"] 4 | 5 | [resource] 6 | background_mode = 2 7 | sky = SubResource("1") 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Godot 4+ specific ignores 2 | .godot/ 3 | 4 | # Godot-specific ignores 5 | .import/ 6 | export.cfg 7 | export_presets.cfg 8 | 9 | # Imported translations (automatically generated from CSV files) 10 | *.translation 11 | 12 | # Mono-specific ignores 13 | .mono/ 14 | data_*/ 15 | mono_crash.*.json 16 | -------------------------------------------------------------------------------- /project/mvc-scene/view/user_attribute_view.gd: -------------------------------------------------------------------------------- 1 | extends HBoxContainer 2 | 3 | @onready var gold = $gold 4 | @onready var hp = $HP 5 | 6 | func set_proxy(p: user_attribute_proxy): 7 | p.connect("on_gold_changed", _on_gold_changed) 8 | p.connect("on_hp_changed", _on_hp_changed) 9 | p.update_ui() 10 | 11 | func _on_gold_changed(value: int): 12 | gold.text = "GOLD: %d" % value 13 | 14 | func _on_hp_changed(value: int): 15 | hp.text = "HP: %d" % value 16 | 17 | -------------------------------------------------------------------------------- /project/mvc-scene/view/user_attribute_view.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://bds0qt5nmpsbs"] 2 | 3 | [ext_resource type="Script" path="res://mvc-scene/view/user_attribute_view.gd" id="1"] 4 | 5 | [node name="user_attribute_view" type="HBoxContainer"] 6 | offset_right = 40.0 7 | offset_bottom = 40.0 8 | script = ExtResource("1") 9 | 10 | [node name="gold" type="Label" parent="."] 11 | custom_minimum_size = Vector2(100, 30) 12 | layout_mode = 2 13 | text = "GOLD: 0" 14 | 15 | [node name="HP" type="Label" parent="."] 16 | custom_minimum_size = Vector2(100, 30) 17 | layout_mode = 2 18 | text = "HP: 0" 19 | -------------------------------------------------------------------------------- /project/mvc-scene/handler/award_handler.gd: -------------------------------------------------------------------------------- 1 | extends MVCHnadler 2 | 3 | # override 4 | func _on_enter(a: MVCApp): 5 | # 监听奖励金币 6 | a.add_callable("on_award_gold", _on_award_gold) 7 | 8 | # override 9 | func _on_exit(a: MVCApp): 10 | # 解除奖励金币监听 11 | a.remove_callable("on_award_gold", _on_award_gold) 12 | 13 | # 模拟奖励金币 14 | func _on_award_gold(e: MVCEvent): 15 | # 获取玩家属性 16 | var user_attr: user_attribute_proxy = get_proxy("user_attr") 17 | 18 | # 模拟奖励金币 19 | var gold: int = randi() % 100 + 1 20 | 21 | # 获得金币 22 | user_attr.add_gold( gold ) 23 | 24 | printt("金币奖励:", gold, "当前总金币:", user_attr.get_gold()) 25 | 26 | -------------------------------------------------------------------------------- /project/mvc/event.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name MVCEvent 3 | 4 | var name: String: get = _get_name, set = _set_name 5 | var data : get = _get_data, set = _set_data 6 | 7 | # ============================================================================== 8 | # private 9 | var _name: String 10 | var _data 11 | 12 | func _init(n, d): 13 | _name = n 14 | _data = d 15 | 16 | func _set_name(v): 17 | pass 18 | 19 | func _get_name() -> String: 20 | return _name 21 | 22 | func _set_data(v): 23 | pass 24 | 25 | func _get_data(): 26 | return _data 27 | 28 | func _to_string(): 29 | return "MVCEvent(\"%s\", %s)" % [_name, _data] 30 | 31 | -------------------------------------------------------------------------------- /project/mvc/command.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name MVCCommand 3 | 4 | var _cmd_list: Array 5 | var _app: WeakRef 6 | 7 | func add_command(cmd): 8 | _cmd_list.append(cmd) 9 | 10 | func execute(e: MVCEvent): 11 | for cmd in _cmd_list: 12 | cmd._set_app(app()) 13 | cmd.execute(e) 14 | _on_execute(e) 15 | 16 | func app() -> MVCApp: 17 | return _app.get_ref() 18 | 19 | func get_proxy(name: String) -> MVCProxy: 20 | return app().get_proxy(name) 21 | 22 | # ============================================================================== 23 | # override 24 | func _on_execute(e: MVCEvent): 25 | pass 26 | 27 | func _set_app(a: MVCApp): 28 | _app = weakref(a) 29 | 30 | -------------------------------------------------------------------------------- /project/mvc-scene/command/load_game_command.gd: -------------------------------------------------------------------------------- 1 | extends MVCCommand 2 | 3 | # override 4 | func _on_execute(e: MVCEvent): 5 | 6 | # 获取玩家属性 7 | var user_attr: user_attribute_proxy = get_proxy("user_attr") 8 | 9 | # 读取玩家数据 10 | var data: Dictionary = _read_file("game.save") 11 | if not data.is_empty(): 12 | user_attr.load( data ) 13 | 14 | # 打印玩家序列化数据 15 | printt("玩家读档:", data) 16 | 17 | func _read_file(filename: String) -> Dictionary: 18 | var file_path = "user://" + filename 19 | var file = FileAccess.open(file_path, FileAccess.READ) 20 | if file.get_open_error() != OK: 21 | return {} 22 | var dict: Dictionary = JSON.parse_string( file.get_as_text() ) 23 | return dict if dict != null else {} 24 | 25 | -------------------------------------------------------------------------------- /project/mvc-scene/command/save_game_command.gd: -------------------------------------------------------------------------------- 1 | extends MVCCommand 2 | 3 | # override 4 | func _on_execute(e: MVCEvent): 5 | 6 | # 获取玩家属性 7 | var user_attr: user_attribute_proxy = get_proxy("user_attr") 8 | 9 | # 序列化玩家数据 10 | var data: Dictionary 11 | user_attr.save( data ) 12 | 13 | # 保存到磁盘 14 | _write_file(data, "game.save") 15 | 16 | # 打印玩家序列化数据 17 | printt("玩家存档:", data) 18 | 19 | func _write_file(data: Dictionary, filename: String) -> bool: 20 | var data_text = JSON.stringify(data) 21 | var file_path = "user://" + filename 22 | var file = FileAccess.open(file_path, FileAccess.WRITE) 23 | if file.get_open_error() != OK: 24 | return false 25 | file.store_string(data_text) 26 | return true 27 | 28 | -------------------------------------------------------------------------------- /project/project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=5 10 | 11 | [application] 12 | 13 | config/name="mvc-project" 14 | run/main_scene="res://mvc-scene/scene/main.tscn" 15 | config/features=PackedStringArray("4.3") 16 | config/icon="res://icon.png" 17 | 18 | [display] 19 | 20 | window/dpi/allow_hidpi=false 21 | 22 | [gui] 23 | 24 | common/drop_mouse_on_gui_input_disabled=true 25 | 26 | [physics] 27 | 28 | common/enable_pause_aware_picking=true 29 | 30 | [rendering] 31 | 32 | environment/defaults/default_environment="res://default_env.tres" 33 | -------------------------------------------------------------------------------- /project/icon.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://sjw481h3rj84" 6 | path="res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://icon.png" 14 | dest_files=["res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /project/mvc-scene/proxy/user_attribute_proxy.gd: -------------------------------------------------------------------------------- 1 | extends MVCProxy 2 | class_name user_attribute_proxy 3 | 4 | # 金币 5 | var _gold: int 6 | # 血量 7 | var _hp: int 8 | 9 | # 金币变更信号 10 | signal on_gold_changed(value) 11 | # 血量变更信号 12 | signal on_hp_changed(value) 13 | 14 | # 更新UI 15 | func update_ui(): 16 | on_gold_changed.emit(_gold) 17 | on_hp_changed.emit(_hp) 18 | 19 | # 增加金币 20 | func add_gold(value: int): 21 | _gold += value 22 | on_gold_changed.emit(_gold) 23 | 24 | # 获取金币 25 | func get_gold() -> int: 26 | return _gold 27 | 28 | # 增加血量 29 | func add_hp(value: int): 30 | _hp += value 31 | if _hp <= 0: 32 | _hp = 0 33 | on_hp_changed.emit(_hp) 34 | 35 | # 获取血量 36 | func get_hp() -> int: 37 | return _hp 38 | 39 | # override 40 | func _on_save(dict: Dictionary): 41 | dict["hp"] = _hp 42 | dict["gold"] = _gold 43 | 44 | # override 45 | func _on_load(dict: Dictionary): 46 | if dict.has("hp"): 47 | _hp = dict["hp"] as int 48 | if dict.has("gold"): 49 | _gold = dict["gold"] as int 50 | 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 baifeng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /project/mvc/event_center.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name MVCEventCenter 3 | 4 | var _event_dict: Dictionary 5 | 6 | func add_callable(name: String, c: Callable) -> bool: 7 | return _get_event_listener(name).add(c) 8 | 9 | func remove_callable(name: String, c: Callable) -> bool: 10 | return _get_event_listener(name).remove(c) 11 | 12 | func notify(name: String, value): 13 | send( MVCEvent.new(name, value) ) 14 | 15 | func send(e: MVCEvent): 16 | _get_event_listener(e.name).receive(e) 17 | 18 | func clear(): 19 | _event_dict.clear() 20 | 21 | func _get_event_listener(name: String) -> _listener: 22 | if not _event_dict.has(name): 23 | _event_dict[name] = _listener.new() 24 | return _event_dict[name] 25 | 26 | # implement listener 27 | class _listener extends RefCounted: 28 | signal _impl(e: MVCEvent) 29 | func add(c: Callable) -> bool: 30 | if _impl.is_connected(c): 31 | return false 32 | _impl.connect(c) 33 | return true 34 | func remove(c: Callable) -> bool: 35 | if not _impl.is_connected(c): 36 | return false 37 | _impl.disconnect(c) 38 | return true 39 | func receive(e: MVCEvent): 40 | _impl.emit(e) 41 | 42 | -------------------------------------------------------------------------------- /project/mvc-scene/handler/battle_handler.gd: -------------------------------------------------------------------------------- 1 | extends MVCHnadler 2 | 3 | # override 4 | func _on_enter(a: MVCApp): 5 | # 监听战斗模拟事件 6 | a.add_callable("on_battle_simulation", _on_battle_simulation) 7 | # 监听恢复血量技能事件 8 | a.add_callable("on_skill_recover_hp", _on_skill_recover_hp) 9 | 10 | # override 11 | func _on_exit(a: MVCApp): 12 | # 解除战斗模拟事件监听 13 | a.remove_callable("on_battle_simulation", _on_battle_simulation) 14 | # 解除恢复血量技能事件监听 15 | a.remove_callable("on_skill_recover_hp", _on_battle_simulation) 16 | 17 | # 处理战斗模拟事件 18 | func _on_battle_simulation(e: MVCEvent): 19 | # 获取玩家属性 20 | var user_attr: user_attribute_proxy = get_proxy("user_attr") 21 | 22 | # 模拟伤害 23 | var damage: int = -( randi() % 100 + 10 ) 24 | 25 | # 进行伤害 26 | user_attr.add_hp( damage ) 27 | 28 | printt("战斗伤害:", damage, "当前血量:", user_attr.get_hp()) 29 | 30 | # 处理恢复血量技能 31 | func _on_skill_recover_hp(e: MVCEvent): 32 | # 获取玩家属性 33 | var user_attr: user_attribute_proxy = get_proxy("user_attr") 34 | 35 | # 模拟恢复 36 | var recover_hp: int = 50 37 | 38 | # 进行恢复 39 | user_attr.add_hp( recover_hp ) 40 | 41 | printt("恢复血量:", recover_hp, "当前总血量:", user_attr.get_hp()) 42 | 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Godot MVC 2 | Lightweight mvc framework written with gdscript. 3 | 4 | ![](mvc.png) 5 | 6 | # Features 7 | 8 | - Lightweight and non-intrusive. 9 | - Independent of Node components. 10 | - Support serialization and deserialization. 11 | - Easy to use. 12 | 13 | # How To Use 14 | 15 | - Copy the 'mvc' directory to any location within your Godot project. 16 | - Begin your MVC coding journey with the following code: 17 | 18 | ```gdscript 19 | 20 | # create mvc app 21 | var app = MVCApp.new() 22 | 23 | # add proxy (model proxy) 24 | app.add_proxy("p1", MVCProxy.new(1)) 25 | app.add_proxy("p2", MVCProxy.new(2)) 26 | app.add_proxy("p3", MVCProxy.new(3)) 27 | 28 | # add handler (controller) 29 | app.add_handler("h1", MVCHnadler.new()) 30 | app.add_handler("h2", MVCHnadler.new()) 31 | app.add_handler("h3", MVCHnadler.new()) 32 | 33 | # add command 34 | app.add_command("my_command", MVCCommand) 35 | 36 | # send notification 37 | app.notify("my_notification", with_param) 38 | app.notify("my_command", with_param) 39 | 40 | # get proxy 41 | var p1: MVCProxy = app.get_proxy("p1") 42 | print( p1.data() ) 43 | 44 | ``` 45 | 46 | # Game made with this framework 47 | 48 | ### Godot Sokoban 49 | 50 | ![screenshot](games/sokoban/screenshot.jpg) 51 | 52 | #### Source Code 53 | [https://godotengine.org/asset-library/asset/3390](https://godotengine.org/asset-library/asset/3390) -------------------------------------------------------------------------------- /project/mvc/handler.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | class_name MVCHandler 3 | 4 | func name() -> String: 5 | return _name 6 | 7 | func app() -> MVCApp: 8 | return _app.get_ref() 9 | 10 | func get_proxy(name: String) -> MVCProxy: 11 | return app().get_proxy(name) 12 | 13 | func on_enter(a: MVCApp): 14 | if a.debug_print: 15 | print("handler <%s:%s> on_enter." % [app().name(), _name]) 16 | _on_enter(a) 17 | 18 | func on_exit(a: MVCApp): 19 | if a.debug_print: 20 | print("handler <%s:%s> on_exit." % [app().name(), _name]) 21 | _on_exit(a) 22 | queue_free() 23 | 24 | func notify(event_name: String, value = null): 25 | app().notify(event_name, value) 26 | 27 | func send(e: MVCEvent): 28 | app().send(e) 29 | 30 | # ============================================================================== 31 | # private 32 | var _app: WeakRef 33 | var _name: String 34 | 35 | # override 36 | func _on_enter(a: MVCApp): 37 | pass 38 | 39 | # override 40 | func _on_exit(a: MVCApp): 41 | pass 42 | 43 | # ============================================================================== 44 | # private function 45 | func _init(parent: Node = null): 46 | if parent: 47 | parent.add_child(self) 48 | 49 | func _set_name(n: String): 50 | _name = n 51 | 52 | func _set_app(a: MVCApp): 53 | _app = weakref(a) 54 | 55 | func _to_string() -> String: 56 | return "handler:%s" % _name 57 | 58 | -------------------------------------------------------------------------------- /project/mvc-scene/scene/main.gd: -------------------------------------------------------------------------------- 1 | extends Node2D 2 | 3 | @onready var _view = $user_attribute_view 4 | 5 | var _app: MVCApp = MVCApp.new() # 创建mvc上下文 6 | 7 | func _ready(): 8 | # 初始化应用 9 | _init_app() 10 | # 初始化视图 11 | _init_view() 12 | 13 | func _init_app(): 14 | # 启用 mvc 信息打印 15 | _app.debug_print = true 16 | 17 | # 注册数据代理 18 | _app.add_proxy("user_attr", user_attribute_proxy.new()) 19 | 20 | # 注册战斗事件处理器 21 | _app.add_handler("battle_handler", load("res://mvc-scene/handler/battle_handler.gd").new()) 22 | # 注册奖励事件处理器 23 | _app.add_handler("award_handler", load("res://mvc-scene/handler/award_handler.gd").new()) 24 | 25 | # 注册存档命令 26 | _app.add_command("save_game_command", load("res://mvc-scene/command/save_game_command.gd")) 27 | _app.add_command("load_game_command", load("res://mvc-scene/command/load_game_command.gd")) 28 | 29 | # 读取存档 30 | _app.notify("load_game_command") 31 | 32 | func _init_view(): 33 | # 获取用户数据 34 | var user_attr: user_attribute_proxy = _app.get_proxy("user_attr") 35 | # 用户数据绑定视图 36 | _view.set_proxy( user_attr ) 37 | 38 | func _on_AddGold_pressed(): 39 | # 奖励金币事件 40 | _app.notify("on_award_gold") 41 | 42 | func _on_AddHP_pressed(): 43 | # 技能回血事件 44 | _app.notify("on_skill_recover_hp") 45 | 46 | func _on_Damage_pressed(): 47 | # 战斗伤害模拟 48 | _app.notify("on_battle_simulation") 49 | 50 | func _on_SaveGame_pressed(): 51 | # 存档命令 52 | _app.notify("save_game_command") 53 | -------------------------------------------------------------------------------- /project/mvc-scene/scene/main.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://dtqbrk84v6in6"] 2 | 3 | [ext_resource type="Script" path="res://mvc-scene/scene/main.gd" id="1"] 4 | [ext_resource type="PackedScene" uid="uid://bds0qt5nmpsbs" path="res://mvc-scene/view/user_attribute_view.tscn" id="2"] 5 | 6 | [node name="Main" type="Node2D"] 7 | script = ExtResource("1") 8 | 9 | [node name="user_attribute_view" parent="." instance=ExtResource("2")] 10 | 11 | [node name="AddGold" type="Button" parent="."] 12 | offset_left = 20.0 13 | offset_top = 50.0 14 | offset_right = 100.0 15 | offset_bottom = 90.0 16 | text = "AwardGold" 17 | 18 | [node name="AddHP" type="Button" parent="."] 19 | offset_left = 130.0 20 | offset_top = 50.0 21 | offset_right = 210.0 22 | offset_bottom = 90.0 23 | text = "RecoverHP" 24 | 25 | [node name="Damage" type="Button" parent="."] 26 | offset_left = 240.0 27 | offset_top = 50.0 28 | offset_right = 320.0 29 | offset_bottom = 90.0 30 | text = "Damage" 31 | 32 | [node name="SaveGame" type="Button" parent="."] 33 | offset_left = 350.0 34 | offset_top = 50.0 35 | offset_right = 430.0 36 | offset_bottom = 90.0 37 | text = "SaveGame" 38 | 39 | [connection signal="pressed" from="AddGold" to="." method="_on_AddGold_pressed"] 40 | [connection signal="pressed" from="AddHP" to="." method="_on_AddHP_pressed"] 41 | [connection signal="pressed" from="Damage" to="." method="_on_Damage_pressed"] 42 | [connection signal="pressed" from="SaveGame" to="." method="_on_SaveGame_pressed"] 43 | -------------------------------------------------------------------------------- /project/mvc/proxy.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name MVCProxy 3 | 4 | signal on_data_changed(sender: MVCProxy, data) 5 | 6 | func name() -> String: 7 | return _name 8 | 9 | func data(): 10 | return _data 11 | 12 | func set_data(d): 13 | _data = d 14 | on_data_changed.emit(self, _data) 15 | 16 | func app() -> MVCApp: 17 | return _app.get_ref() 18 | 19 | func on_enter(a: MVCApp): 20 | if a.debug_print: 21 | print("proxy <%s:%s> on_enter." % [app().name(), _name]) 22 | _on_enter(a) 23 | 24 | func on_exit(a: MVCApp): 25 | if a.debug_print: 26 | print("proxy <%s:%s> on_exit." % [app().name(), _name]) 27 | _on_exit(a) 28 | 29 | func save(dict: Dictionary): 30 | _on_save(dict) 31 | 32 | func load(dict: Dictionary): 33 | _on_load(dict) 34 | 35 | func notify(event_name: String, value = null): 36 | app().notify(event_name, value) 37 | 38 | func send(e: MVCEvent): 39 | app().send(e) 40 | 41 | # ============================================================================== 42 | # private 43 | var _app: WeakRef 44 | var _name: String 45 | var _data 46 | 47 | func _init(data = null): 48 | _data = data 49 | 50 | # override 51 | func _on_enter(a: MVCApp): 52 | pass 53 | 54 | # override 55 | func _on_exit(a: MVCApp): 56 | pass 57 | 58 | # override 59 | func _on_save(dict: Dictionary): 60 | pass 61 | 62 | # override 63 | func _on_load(dict: Dictionary): 64 | pass 65 | 66 | func _set_name(n: String): 67 | _name = n 68 | 69 | func _set_app(a: MVCApp): 70 | _app = weakref(a) 71 | 72 | func _to_string() -> String: 73 | return "proxy:%s:%s" % [_name, _data] 74 | 75 | -------------------------------------------------------------------------------- /project/mvc/test.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name MVCTest 3 | 4 | var _app: MVCApp = MVCApp.new("MVCApp_test") 5 | 6 | func _init(): 7 | _app.debug_print = true 8 | _test_proxy() 9 | _test_handler() 10 | _test_command() 11 | _test_event() 12 | 13 | func _test_proxy(): 14 | _app.add_proxy("p1", MVCProxy.new(1)) 15 | _app.add_proxy("p2", MVCProxy.new(2)) 16 | _app.add_proxy("p2", MVCProxy.new(2)) 17 | _app.add_proxy("p3", MVCProxy.new(3)) 18 | _app.remove_proxy("p1") 19 | 20 | printt("get proxy", _app.get_proxy("p1"), "has proxy", _app.has_proxy("p3")) 21 | print("") 22 | 23 | func _test_handler(): 24 | _app.add_handler("h1", MVCHandler.new()) 25 | _app.add_handler("h2", MVCHandler.new()) 26 | _app.add_handler("h3", MVCHandler.new()) 27 | _app.remove_handler("h3") 28 | 29 | printt("get handler", _app.get_handler("h1"), "has handler", _app.has_handler("h3")) 30 | print("") 31 | 32 | class _cmd extends MVCCommand: 33 | func _init(): 34 | print("test command init.") 35 | func _on_execute(e: MVCEvent): 36 | print("test command execute.") 37 | 38 | func _test_command(): 39 | _app.add_command("test_cmd_1", _cmd) 40 | _app.add_command("test_cmd_2", _cmd) 41 | 42 | _app.notify("test_cmd_2") 43 | 44 | printt("has command", _app.has_command("test_cmd_1")) 45 | 46 | _app.remove_command("test_cmd_1") 47 | _app.notify("test_cmd_1") 48 | 49 | printt("has command", _app.has_command("test_cmd_1")) 50 | print("") 51 | 52 | class _event_handler extends MVCHandler: 53 | # override 54 | func _on_enter(a: MVCApp): 55 | a.add_callable("test", _on_event) 56 | # override 57 | func _on_exit(a: MVCApp): 58 | a.remove_callable("test", _on_event) 59 | func _on_event(e: MVCEvent): 60 | print("handler <%s> on event <%s> with <%s>." % [name(), e.name, e.data]) 61 | 62 | func _test_event(): 63 | var handler: MVCHandler = _app.get_handler("h1") 64 | handler.notify("test_cmd_2") 65 | handler.send(MVCEvent.new("test_cmd_2", null)) 66 | 67 | print("") 68 | 69 | _app.add_handler("event_handler", _event_handler.new()) 70 | _app.notify("test", "test event handler.") 71 | _app.remove_handler("event_handler") 72 | _app.notify("test", "test event handler.") 73 | 74 | print("") 75 | 76 | -------------------------------------------------------------------------------- /project/mvc/app.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name MVCApp 3 | 4 | var debug_print: bool 5 | 6 | var _name: String 7 | var _proxy_pool: Dictionary 8 | var _handler_pool: Dictionary 9 | var _command_pool: Dictionary 10 | var _event_pool: MVCEventCenter = MVCEventCenter.new() 11 | 12 | func _init(name: String = "MVCApp"): 13 | _name = name 14 | 15 | func name() -> String: 16 | return _name 17 | 18 | func clear(): 19 | remove_all_commands() 20 | remove_all_handlers() 21 | remove_all_proxies() 22 | 23 | func add_proxy(name: String, proxy: MVCProxy = MVCProxy.new()): 24 | remove_proxy(name) 25 | _proxy_pool[name] = proxy 26 | proxy._set_name(name) 27 | proxy._set_app(self) 28 | proxy.on_enter(self) 29 | 30 | func remove_proxy(name: String) -> bool: 31 | if _proxy_pool.has(name): 32 | var proxy = _proxy_pool[name] 33 | proxy.on_exit(self) 34 | return _proxy_pool.erase(name) 35 | 36 | func remove_all_proxies(): 37 | var keys = _proxy_pool.keys() 38 | for k in keys: 39 | remove_proxy(k) 40 | 41 | func has_proxy(name: String) -> bool: 42 | return _proxy_pool.has(name) 43 | 44 | func get_proxy(name: String) -> MVCProxy: 45 | if _proxy_pool.has(name): 46 | return _proxy_pool[name] 47 | return null 48 | 49 | func add_handler(name: String, handler): 50 | remove_handler(name) 51 | _handler_pool[name] = handler 52 | handler._set_name(name) 53 | handler._set_app(self) 54 | handler.on_enter(self) 55 | 56 | func remove_handler(name: String) -> bool: 57 | if _handler_pool.has(name): 58 | var handler = _handler_pool[name] 59 | handler.on_exit(self) 60 | return _handler_pool.erase(name) 61 | 62 | func remove_all_handlers(): 63 | var keys = _handler_pool.keys() 64 | for k in keys: 65 | remove_handler(k) 66 | 67 | func has_handler(name: String): 68 | return _handler_pool.has(name) 69 | 70 | func get_handler(name: String): 71 | if _handler_pool.has(name): 72 | return _handler_pool[name] 73 | return null 74 | 75 | class _command_shell extends RefCounted: 76 | var _debug_print: bool 77 | var _class: Resource 78 | var _app_name: String 79 | var _app: WeakRef 80 | func _init(r: Resource, debug_print: bool = false): 81 | _class = r 82 | _debug_print = debug_print 83 | func _register(a: MVCApp, name: String): 84 | _app_name = a.name() 85 | _app = weakref(a) 86 | a.add_callable(name, _on_event) 87 | func _unregister(a: MVCApp, name: String): 88 | a.remove_callable(name, _on_event) 89 | _app = null 90 | func _on_event(e: MVCEvent): 91 | if _debug_print: 92 | print("command <%s:%s> execute." % [_app_name, e.name]) 93 | var cmd = _class.new() 94 | cmd._set_app(_app.get_ref()) 95 | cmd.execute(e) 96 | 97 | func add_command(name: String, cmdres: Resource) -> bool: 98 | if cmdres == null: 99 | print("add command <%s:%s> fail: Resource is null." % [_name, name]) 100 | return false 101 | remove_command(name) 102 | var shell = _command_shell.new(cmdres, debug_print) 103 | _command_pool[name] = shell 104 | shell._register(self, name) 105 | if debug_print: 106 | print("command <%s:%s> add to MVCApp." % [_name, name]) 107 | return true 108 | 109 | func remove_command(name: String) -> bool: 110 | if _command_pool.has(name): 111 | var shell = _command_pool[name] 112 | shell._unregister(self, name) 113 | if debug_print: 114 | print("command <%s:%s> remove from MVCApp." % [_name, name]) 115 | return _command_pool.erase(name) 116 | 117 | func remove_all_commands() -> bool: 118 | var keys = _command_pool.keys() 119 | for name in keys: 120 | remove_command(name) 121 | return true 122 | 123 | func has_command(name: String): 124 | return _command_pool.has(name) 125 | 126 | func add_callable(name: String, c: Callable): 127 | _event_pool.add_callable(name, c) 128 | 129 | func remove_callable(name: String, c: Callable): 130 | _event_pool.remove_callable(name, c) 131 | 132 | func notify(event_name: String, value = null): 133 | _event_pool.notify(event_name, value) 134 | 135 | func send(e: MVCEvent): 136 | _event_pool.send(e) 137 | 138 | --------------------------------------------------------------------------------