资讯动态

将非卡片对象接入 godot-card-game-framework 的 ScriptingEngine:scriptables 扩展契约详解

发布时间:2026/10/14 17:20:05 来源:尧图企业网站定制
游戏开发UI组件【免费下载链接】godot-card-game-frameworkA framework which comes with prepared scenes and classes to kickstart your card game, as well as a powerful scripting engine to use to provide full rules enforcement.项目地址https://gitcode.com/gh_mirrors/go/godot-card-game-framework点击查看免费下载godot-card-game-framework卡牌游戏框架内置了一个名为 ScriptingEngineScEng的规则执行引擎它把复杂的棋盘交互逻辑编码为普通的 JSON 字典由引擎解释执行从而实现完整的规则强制full rules enforcement。本框架的 ScEng 在设计上并不只服务于卡牌任何对象都可以接入它的能力。读完本文你将掌握让一张普通卡牌之外的对象例如自定义组件、生物、棋盘元素参与脚本触发的完整契约——scriptables 分组、canonical_name变量、execute_scripts()、get_state_exec()与retrieve_scripts()五项要求各自对应的源码实现以及信号驱动的自动触发链路。一、ScriptingEngine 基本工作原理从 SCRIPTING_ENGINE.md 的原始描述来看ScEng 的定位是允许通过一个简单的 JSON 字典把对棋盘board的复杂交互与操作编码进游戏对象中。在实现层面引擎的入口类 ScriptingEngine 的头部注释解释了其工作方式自动化基于 ScriptTask脚本任务对象每个任务通过调用被操纵对象已有的函数来完成一次非常具体的棋盘状态变更因此每个任务函数实际上提供了一套“文本形式的 API”。一个典型的脚本定义是仓库自带的演示集 SetScripts_Demo1.gd结构为三层嵌套字典卡片名 → 触发器trigger→ 执行状态state→ 任务数组const scripts : { Test Card 1: { manual: { board: [ { name: rotate_card, subject: self, degrees: 90 }, ], hand: [ { name: spawn_card, card_name: Spawn Card, board_position: Vector2(500, 200), } ] }, }, }其中name指定任务类型如rotate_card、move_card_to_container、mod_counter、nested_scriptsubject指定作用对象self、target、index等其余键是任务参数。任务的全部标准键定义集中在 ScriptProperties.gd即代码中的SP常量表中。引擎执行时的两条关键机制值得先了解因为它们直接决定了后面execute_scripts()契约的含义费用预演cost dry-runScriptingEngine 维护can_all_costs_be_paid与all_tasks_completed两个标志以CFInt.RunType.COST_CHECK方式先对标记为is_cost的任务做一次“干跑”确认费用可支付后才执行正式运行else 分支若费用无法支付且任务定义了is_else则以CFInt.RunType.ELSE执行“否则”分支任务。主执行循环见 execute()它根据run_type过滤任务COST_CHECK 只跑is_cost/needs_subject任务ELSE 只跑is_else任务为每个任务设置临时计数器/属性修正然后依次交给ScriptTask处理。每个ScriptTask在 prime() 中完成“可选任务玩家确认 → 目标查找_find_subjects→ 发出primed信号”的流程。二、接入契约总览SCRIPTING_ENGINE.md 给出的接入步骤原文有五条把对象加入scriptables 分组在对象脚本中定义canonical_name 变量并赋值为人类可读的对象名为对象添加execute_scripts()方法为对象添加get_state_exec()方法为对象添加retrieve_scripts()方法。下面逐条结合仓库源码说明每条要求被哪些代码路径依赖、Card 类的参考实现长什么样。1. 加入 scriptables 分组ScEng 有两条核心代码路径会遍历scriptables分组这也正是分组存在的意义路径 A信号驱动的自动触发。卡牌框架用 CFControl.gd 中的内部类SignalPropagator收集所有已知卡牌信号known_card_signals包含card_rotated、card_flipped、card_moved_to_board、card_moved_to_pile、card_attached、card_properties_modified等十一个信号。每当任意卡牌发出这些信号之一_on_signal_received()会依次调用# src/core/CFControl.gd (SignalPropagator._on_signal_received) for card in cfc.get_tree().get_nodes_in_group(cards): card.execute_scripts(trigger_card, trigger, details) # If we need other objects than cards to trigger scripts via signals # add them to the scriptables group ang ensure they have # an execute_scripts function for card in cfc.get_tree().get_nodes_in_group(scriptables): card.execute_scripts(trigger_card, trigger, details)注意第二条循环——cards分组与scriptables分组是并列的两组。任何想让非卡片对象响应“卡牌被移动/翻转/附挂”这类自动触发的对象都必须自己加入scriptables分组才会被这条广播链路覆盖。路径 B修改量alterant引擎。当某任务需要被场上其他对象的“修正脚本”调整数值时CFScriptUtils.get_altered_value() 会同时扫描两个分组var scriptables_array : Array \ cfc.get_tree().get_nodes_in_group(cards)\ cfc.get_tree().get_nodes_in_group(scriptables) for obj in scriptables_array: var scripts obj.retrieve_scripts(SP.KEY_ALTERANTS) var state_scripts scripts.get(obj.get_state_exec(), [])从这里可以直接看出scriptables分组中的每个对象必须实现retrieve_scripts()和get_state_exec()否则这条循环会直接报错——这就是契约第 4、5 条要求不可省略的原因。该函数还实现了修正值的符号保护规则修正不能让正变负、负变正与 alterant 结果缓存。2. 定义 canonical_name 变量canonical_name是对象在规则系统里的“规范名”Card 类的定义见 CardTemplate.gdvar canonical_name : String setget set_card_name, get_card_name它的三个实际用途均可在源码中验证玩家对话框确认弹窗、整数输入弹窗都使用它作为标题/主语如 CFUtils.confirm() 的调用点 CardTemplate.execute_scripts() 与 ScriptingEngine 中的integer_dialog.prep(script.owner.canonical_name, minimum, maximum)效果过滤与目标解析ScriptTask 与 ScriptAlter 在生成日志/对话时读取owner.canonical_name确定性排序CFUtils.sort_scriptables_by_name() 以canonical_name为主键回退到旧的card_name对 scriptable 对象排序保证同一局游戏中修正量叠加顺序稳定。该函数要求列表中每个对象都有canonical_name属性这也是文档要求该变量“人类可读”的隐含动机。对于非卡片对象你只需在脚本中声明var canonical_name : String并在初始化时赋值为可辨识的名称即可不需要复刻 Card 的 setget 逻辑那是为了同步节点名与标签显示。3. 实现 execute_scripts() 方法文档建议“直接复制 Card 的execute_scripts()再修改”。Card 的参考实现位于 CardTemplate.gd其执行管线可拆解为以下步骤非卡片对象应保持一致的语义前置守卫游戏暂停时cfc.game_paused或棋盘不存在时cfc.NMAP中没有board直接返回避免在卡牌展示界面等场景误触发取脚本var card_scripts retrieve_scripts(trigger)按当前触发器取出脚本字典触发器过滤SP.filter_trigger(card_scripts, trigger_card, self, trigger_details)检查脚本定义中的触发条件trigger 名 details 匹配不匹配则清空按状态取任务state_scripts card_scripts.get(get_state_exec(), card_scripts.get(all, []))——先查对象当前状态对应的任务列表查不到就回退到all状态可选确认CFUtils.confirm(...)处理is_optional类确认弹窗若玩家放弃则state_scripts []多选分支若状态下的脚本是字典而非数组则弹出CardChoices让玩家二选一参见 src/core/CardChoices.gd两段式引擎运行sceng cfc.scripting_engine.new(state_scripts, self, trigger_card, trigger_details) sceng.execute(CFInt.RunType.COST_CHECK) # 费用干跑 if not sceng.all_tasks_completed: yield(sceng, tasks_completed) if sceng.can_all_costs_be_paid and not only_cost_check: sceng.execute() # 正式执行 ... elif not sceng.can_all_costs_be_paid and not only_cost_check: sceng.execute(CFInt.ELSE) # 费用不足走 else 分支 ... emit_signal(scripts_executed, self, sceng, trigger)注意该方法返回ScriptingEngine实例注释说明故意不做静态类型标注因为会引发解析期循环依赖。only_cost_check参数支持外部只跑一次费用检查而不实际执行效果——这在实现“玩家能否支付此费用”类逻辑时会用到。另外Card 在 CardTemplate.gd 的_ready()中通过cfc.signal_propagator.connect_new_card(self)把自己接入了信号广播。非卡片对象若要接收卡牌信号同样可以直接调用该连接方法或由框架在 SignalPropagator._on_signal_received() 的scriptables分组循环中统一覆盖后者只需分组 execute_scripts()。SignalPropagator还会额外发出自己的signal_received信号见 CHANGELOG.md 相关条目允许不在scriptables分组的对象也从一个中心位置钩住卡牌触发事件——这是分组机制之外的第二条订阅通道。4. 实现 get_state_exec() 方法get_state_exec()的作用是根据对象当前所处位置返回一个用于查找脚本的状态字符串。Card 的参考实现在 CardTemplate.gdfunc get_state_exec() - String: var state_exec : NONE match state: CardState.ON_PLAY_BOARD, CardState.FOCUSED_ON_BOARD, CardState.DROPPING_TO_BOARD: if is_faceup: # 只有面朝上的牌才能在棋盘上执行脚本 state_exec board CardState.IN_HAND, CardState.FOCUSED_IN_HAND, CardState.REORGANIZING, CardState.PUSHED_ASIDE: state_exec hand CardState.IN_POPUP, CardState.FOCUSED_IN_POPUP, CardState.IN_PILE, CardState.VIEWED_IN_PILE: state_exec pile CardState.MOVING_TO_CONTAINER: if get_parent().is_in_group(hands): state_exec hand else: state_exec pile return(state_exec)返回值即脚本字典第二层键board/hand/pile/NONE。注释明确说明该函数按state枚举而非父节点名判断因为父节点会变化。非卡片对象没有 Card 的state枚举需要自行定义等价的状态模型例如“在棋盘上/在弹窗中”并把返回值与脚本定义中使用的状态键对齐返回NONE表示该状态下不执行任何脚本。5. 实现 retrieve_scripts() 方法retrieve_scripts(trigger)负责“从对象存储脚本的任何位置”取出指定触发器下的脚本字典。Card 的参考实现在 CardTemplate.gd有两条取值路径且实例级优先func retrieve_scripts(trigger: String) - Dictionary: var found_scripts: Dictionary # 若脚本直接定义在该 Card 实例上运行时可被单独修改优先使用 if not scripts.empty() and not scripts.get(trigger, {}).empty(): found_scripts scripts.get(trigger, {}).duplicate(true) else: # 否则回退到全局脚本定义文件cfc.set_scripts中的定义 found_scripts cfc.set_scripts.get(canonical_name, {}).get(trigger, {}).duplicate(true) return(found_scripts)第一层scripts是挂在实例自身上的运行时字典允许在运行时单独修改某一张牌的行为而不影响同名其他牌第二层cfc.set_scripts是全局脚本定义表由卡牌集的 SetScripts 文件如 SetScripts_Demo1.gd 中get_scripts()暴露的字典装载索引键正是canonical_name。非卡片对象通常只有一条取值路径自己的存储但同样应保证给定一个 trigger返回{状态键: [任务字典, ...]}或空字典且用duplicate(true)深拷贝防止引擎执行时尤其repeat会就地改写任务污染原始定义。三、对象如何成为脚本“主体”get_all_scriptables()加入scriptables分组让对象能被触发器扫描到要让对象还能被任务当作作用对象subject例如某任务遍历场上所有可脚本化对象则涉及板面board的查询入口 BoardTemplate.get_all_scriptables()# Overridable function which returns all objects on the table which can # be used as subjects by the scripting engine. func get_all_scriptables() - Array: return(get_all_cards())这是一个可覆写的钩子默认返回板上全部 Card自定义 Board 可以覆写它把生物、组件等其他 scriptable 节点一并返回。引擎中boardseek类的主语解析正是走这条路ScriptObject.gd 中可以看到cfc.NMAP.board.get_all_scriptables()被用于生成目标列表随后以CFUtils.sort_scriptables_by_name排序CHANGELOG.md 也记录了该变更“ScriptEngine 的 boardseek 现在使用新的 board 方法get_all_scriptables()可以被扩展以收集任何类型的对象”。同理Pile 在按名排序时同样依赖该工具函数。四、综合示例一个最小 scriptable 对象骨架按上述契约一个可接入 ScEng 的非卡片对象大致长这样示例骨架键名与分组名均与仓库实现一致extends Node var canonical_name : String My Scriptable Object # 从自己的存储脚本定义文件、属性等读取脚本 var scripts : {} func _ready(): add_to_group(scriptables) # 若需要直接接收卡牌信号可调用 # cfc.signal_propagator.connect_new_card(self) # 由 SignalPropagator 的分组循环自动调用手动交互时应自行调用 func execute_scripts(trigger_card, trigger : manual, trigger_details : {}): var card_scripts retrieve_scripts(trigger) if not SP.filter_trigger(card_scripts, trigger_card, self, trigger_details): return var state_scripts card_scripts.get(get_state_exec(), card_scripts.get(all, [])) if len(state_scripts) 0: return var sceng cfc.scripting_engine.new(state_scripts, self, trigger_card, trigger_details) sceng.execute(CFInt.RunType.COST_CHECK) if not sceng.all_tasks_completed: yield(sceng, tasks_completed) if sceng.can_all_costs_be_paid: sceng.execute() if not sceng.all_tasks_completed: yield(sceng, tasks_completed) # 返回对象当前所处状态对应的脚本状态键 func get_state_exec() - String: return board func retrieve_scripts(trigger: String) - Dictionary: return scripts.get(trigger, {}).duplicate(true)要点回顾分组保证被信号广播与 alterant 扫描覆盖canonical_name保证对话框、过滤与排序可用三个方法保证SignalPropagator、CFScriptUtils与BoardTemplate中所有对 scriptable 对象的统一调用约定成立。若对象还要被任务作为 subject 选取别忘了覆写所在 Board 的get_all_scriptables()。五、验证与测试仓库自带 GUT 测试对信号接入做了直接断言test_scripting_engine_signals.gd 遍历cfc.signal_propagator.known_card_signals确认每张测试卡牌都已连接到signal_propagator脚本引擎行为有大量集成测试可作参照位于 tests/integration/如 test_scripting_engine_general.gd、test_scripting_engine_subjects.gd、test_scripting_engine_costs.gd、test_scripting_engine_alterants.gd演示卡牌集 SetScripts_Demo1.gd / SetScripts_Demo2.gd 展示了manual触发器、nested_script、needs_selection选择参数、is_cost等键的真实用法可作为编写脚本定义时的现成样例SCRIPTING_ENGINE.md 原文建议先查阅项目 wiki 中的 ScriptDefinitions 基础说明任务键、过滤器的完整清单以 wiki 与 ScriptProperties.gd 源码为准。六、接入检查清单步骤要求仓库中的验证点1对象加入scriptables分组CFControl.gd 的分组广播、CFScriptUtils.gd 的 alterant 扫描2定义canonical_name字符串变量CFUtils.gd 排序、CardTemplate.gd 参考定义3实现execute_scripts()参考 Card 版本CardTemplate.gd 参考实现4实现get_state_exec()返回状态键CardTemplate.gd 参考实现5实现retrieve_scripts(trigger)返回脚本字典CardTemplate.gd 参考实现可选覆写 Board 的get_all_scriptables()使其可被作为 subjectBoardTemplate.gd、ScriptObject.gd遵循以上契约后非卡片对象即可与卡牌一样被卡牌信号自动触发、被费用/确认/多选流程覆盖、参与修改量叠加并作为任务主体被 ScEng 解析——这正是框架“规则逻辑数据化、执行引擎统一化”设计意图的完整体现。赞分享游戏开发UI组件【免费下载链接】godot-card-game-frameworkA framework which comes with prepared scenes and classes to kickstart your card game, as well as a powerful scripting engine to use to provide full rules enforcement.项目地址https://gitcode.com/gh_mirrors/go/godot-card-game-framework点击查看免费下载相关推荐【亲测免费】 Godot Card Game Framework 安装和配置指南Godot Card Game Framework 安装和配置指南 1. 项目基础介绍和主要编程语言 项目基础介绍 Godot Card Game Framew游戏开发UI组件godot-card-game-framework从安装部署到脚本引擎的卡牌游戏框架实战指南godot card game framework从安装部署到脚本引擎的卡牌游戏框架实战指南 godot card game framework下文简称 C游戏开发UI组件Godot Card Game Framework 安装指南从 src 目录拷贝到可升级的卡牌游戏框架Godot Card Game Framework 安装指南从 src 目录拷贝到可升级的卡牌游戏框架 Godot Card Game Framework下游戏开发UI组件创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑