Files
DX12/Engine/Components/Script.cpp
2026-03-19 18:27:49 +08:00

368 lines
8.8 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @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<detail::script_ptr> entity_scripts;
/**
* @brief script_id 索引到实例数组下标的映射。
*/
utl::vector<id::id_type> id_mapping;
/**
* @brief script_id 代数数组。
*/
utl::vector<id::generation_type> generations;
/**
* @brief 可复用 script_id 队列。
*/
utl::deque<script_id> free_ids;
/**
* @brief 脚本对 Transform 的延迟修改缓存。
*/
utl::vector<transform::component_cache> transform_cache;
#if USE_TRANSFORM_CACHE_MAP
std::unordered_map<id::id_type, u32> cache_map;
#endif
using script_registry = std::unordered_map<size_t, detail::script_creator>;
/**
* @brief 获取全局脚本工厂注册表。
* @return 注册表引用。
*/
script_registry&
registery()
{
static script_registry reg;
return reg;
}
#ifdef USE_WITH_EDITOR
/**
* @brief 获取编辑器脚本名集合。
* @return 脚本名数组引用。
*/
utl::vector<std::string>&
script_names()
{
static utl::vector<std::string> 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 <atlsafe.h>
/**
* @brief 导出脚本名列表给编辑器端。
* @details
* 返回值为 LPSAFEARRAYSAFEARRAY*),元素类型为 BSTR。
* 内部流程:
* - 从 script_names() 读取脚本名数量;
* - 为空时返回 nullptr
* - 使用 CComSafeArray<BSTR> 分配数组;
* - 逐项把 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<BSTR> 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