/** * @file Script.cpp * @brief 脚本组件存储、脚本注册表与逐帧更新实现。 * @details * 该文件负责脚本系统运行时核心流程: * - 维护脚本实例数组与 script_id 到实例下标映射; * - 维护脚本工厂注册表,支持按哈希查询创建函数; * - 在脚本回调中缓存变换修改,并在帧末批量提交到 Transform 系统。 */ #include "Script.h" #include "Entity.h" #include "Transform.h" #define USE_TRANSFORM_CACHE_MAP 1 namespace XEngine::script { namespace { /** * @brief 活跃脚本实例数组。 */ utl::vector entity_scripts; /** * @brief script_id 索引到实例数组下标的映射。 */ utl::vector id_mapping; /** * @brief script_id 代数数组。 */ utl::vector generations; /** * @brief 可复用 script_id 队列。 */ utl::deque free_ids; /** * @brief 脚本对 Transform 的延迟修改缓存。 */ utl::vector transform_cache; #if USE_TRANSFORM_CACHE_MAP std::unordered_map cache_map; #endif using script_registry = std::unordered_map; /** * @brief 获取全局脚本工厂注册表。 * @return 注册表引用。 */ script_registry& registery() { static script_registry reg; return reg; } #ifdef USE_WITH_EDITOR /** * @brief 获取编辑器脚本名集合。 * @return 脚本名数组引用。 */ utl::vector& script_names() { static utl::vector names; return names; } #endif /** * @brief 判断脚本组件是否存在且有效。 * @param id 脚本组件 ID。 * @return 存在返回 true。 */ bool exists(script_id id) { assert(id::is_valid(id)); const id::id_type index{ id::index(id) }; assert(index < generations.size() && id_mapping[index] < entity_scripts.size()); assert(generations[index] == id::generation(id)); return (generations[index] == id::generation(id) && entity_scripts[id_mapping[index]] && entity_scripts[id_mapping[index]]->is_valid()); } #if USE_TRANSFORM_CACHE_MAP /** * @brief 获取实体对应的 Transform 缓存项。 * @param entity 目标实体。 * @return 缓存项指针。 */ transform::component_cache *const get_cache_ptr(const game_entity::entity *const entity) { assert(game_entity::is_alive((*entity).get_id())); const transform::transform_id id{ (*entity).transform().get_id() }; u32 index{ u32_invalid_id }; auto pair = cache_map.try_emplace(id, id::invalid_id); if (pair.second) { index = (u32)transform_cache.size(); transform_cache.emplace_back(); transform_cache.back().id = id; cache_map[id] = index; } else { index = cache_map[id]; } assert(index < transform_cache.size()); return &transform_cache[index]; } #else /** * @brief 线性查找方式获取 Transform 缓存项。 * @param entity 目标实体。 * @return 缓存项指针。 */ transform::component_cache *const get_cache_ptr(const game_entity::entity *const entity) { assert(game_entity::is_alive((*entity).get_id())); const transform::transform_id id{ (*entity).transform().get_id() }; for (auto& cache : transform_cache) { if (cache.id == id) { return &cache; } } transform_cache.emplace_back(); transform_cache.back().id = id; return &transform_cache.back(); } #endif }// namespace namespace detail { /** * @brief 注册脚本工厂函数。 * @param tag 脚本名哈希。 * @param func 工厂函数。 * @return 注册成功返回 1。 */ u8 register_script(size_t tag, script_creator func) { bool result{ registery().insert(script_registry::value_type{tag, func}).second }; assert(result); return result; } /** * @brief 按哈希获取脚本工厂函数。 * @param tag 脚本名哈希。 * @return 脚本工厂函数。 */ script_creator get_script_creator(size_t tag) { auto script = XEngine::script::registery().find(tag); assert(script != XEngine::script::registery().end() && script->first == tag); return script->second; } #ifdef USE_WITH_EDITOR /** * @brief 记录脚本名称供编辑器使用。 * @param name 脚本类名。 * @return 写入成功返回 1。 */ u8 add_script_name(const char* name) { script_names().emplace_back(name); return true; } #endif }// namespace detail /** * @brief 创建脚本组件并实例化脚本对象。 * @param info 脚本初始化参数。 * @param entity 绑定实体。 * @return 脚本组件句柄。 */ component create(init_info info, game_entity::entity entity) { assert(entity.is_valid()); assert(info.script_creator); script_id id{}; if (free_ids.size() > id::min_deleted_elements) { id = free_ids.front(); assert(!exists(id)); free_ids.pop_front(); id = script_id{ id::new_generation(id) }; ++generations[id::index(id)]; } else { id = script_id{ (id::id_type)id_mapping.size() }; id_mapping.emplace_back(); generations.push_back(0); } assert(id::is_valid(id)); const id::id_type index{ (id::id_type)entity_scripts.size() }; entity_scripts.emplace_back(info.script_creator(entity)); assert(entity_scripts.back()->get_id() == entity.get_id()); id_mapping[id::index(id)] = index; return component{ id }; } /** * @brief 删除脚本组件并更新映射关系。 * @param c 脚本组件句柄。 */ void remove(component c) { assert(c.is_valid() && exists(c.get_id())); const script_id id{ c.get_id() }; const id::id_type index{ id::index(id) }; const script_id last_id{ entity_scripts.back()->script().get_id() }; utl::erase_unordered(entity_scripts, index); id_mapping[id::index(last_id)] = index; id_mapping[id::index(id)] = id::invalid_id; } /** * @brief 执行脚本逐帧更新并提交变换缓存。 * @param dt 帧时间步长。 */ void update(float dt) { for (auto& ptr : entity_scripts) { ptr->update(dt); } if (transform_cache.size()) { transform::update(transform_cache.data(), (u32)transform_cache.size()); transform_cache.clear(); #if USE_TRANSFORM_CACHE_MAP cache_map.clear(); #endif } } /** * @brief 缓存脚本写入的旋转变更。 * @param entity 目标实体。 * @param rotation_quaternion 旋转四元数。 */ void entity_script::set_rotation(const game_entity::entity *const entity, math::v4 rotation_quaternion) { transform::component_cache& cache{ *get_cache_ptr(entity) }; cache.flags |= transform::component_flags::rotation; cache.rotation = rotation_quaternion; } /** * @brief 缓存脚本写入的朝向变更。 * @param entity 目标实体。 * @param orientation_vector 朝向向量。 */ void entity_script::set_orientation(const game_entity::entity *const entity, math::v3 orientation_vector) { transform::component_cache& cache{ *get_cache_ptr(entity) }; cache.flags |= transform::component_flags::orientation; cache.orientation = orientation_vector; } /** * @brief 缓存脚本写入的位置变更。 * @param entity 目标实体。 * @param position 位置向量。 */ void entity_script::set_position(const game_entity::entity *const entity, math::v3 position) { transform::component_cache& cache{ *get_cache_ptr(entity) }; cache.flags |= transform::component_flags::position; cache.position = position; } /** * @brief 缓存脚本写入的缩放变更。 * @param entity 目标实体。 * @param scale 缩放向量。 */ void entity_script::set_scale(const game_entity::entity *const entity, math::v3 scale) { transform::component_cache& cache{ *get_cache_ptr(entity) }; cache.flags |= transform::component_flags::scale; cache.scale = scale; } } // namespace script #ifdef USE_WITH_EDITOR /** * @brief ATL 的 SAFEARRAY 封装头。 * @details * 该头文件提供 CComSafeArray,用于在编辑器导出路径中构造 COM 安全数组, * 以便把脚本名列表跨 DLL 边界返回给外部调用方。 */ #include /** * @brief 导出脚本名列表给编辑器端。 * @details * 返回值为 LPSAFEARRAY(SAFEARRAY*),元素类型为 BSTR。 * 内部流程: * - 从 script_names() 读取脚本名数量; * - 为空时返回 nullptr; * - 使用 CComSafeArray 分配数组; * - 逐项把 std::string 转成 BSTR 填入数组; * - 通过 Detach 转移 SAFEARRAY 所有权给调用方。 * 调用方在不再使用后应负责释放 SAFEARRAY。 */ extern "C" __declspec(dllexport) LPSAFEARRAY get_script_names() { // 读取当前已注册脚本名数量。 const u32 size{ (u32)XEngine::script::script_names().size() }; // 没有脚本时返回空指针,避免创建空 SAFEARRAY。 if (!size) return nullptr; // 创建固定长度的 BSTR 安全数组。 CComSafeArray names(size); // 将脚本名逐项转换为 BSTR 并写入数组。 for (u32 i{ 0 }; i < size; ++i) { names.SetAt(i, A2BSTR_EX(XEngine::script::script_names()[i].c_str()), false); } // 释放 CComSafeArray 对底层 SAFEARRAY 的管理并返回给调用方。 return names.Detach(); } #endif