diff --git a/111/111.png b/111/111.png new file mode 100644 index 00000000..cafa1648 Binary files /dev/null and b/111/111.png differ diff --git a/111/project.json b/111/project.json new file mode 100644 index 00000000..3ba730fc --- /dev/null +++ b/111/project.json @@ -0,0 +1,8 @@ +{ + "name": "111", + "path": "D:\\IMGUI\\EG\\111", + "last_modified": "2026-03-18 10:56:00", + "scene_file": "scenes\\scene.bam", + "created_at": "2026-03-18 09:46:18", + "version": "1.0" +} \ No newline at end of file diff --git a/111/scenes/scene.bam b/111/scenes/scene.bam new file mode 100644 index 00000000..97a08e2d Binary files /dev/null and b/111/scenes/scene.bam differ diff --git a/111/scripts/BouncerScript.py b/111/scripts/BouncerScript.py new file mode 100644 index 00000000..60fb4170 --- /dev/null +++ b/111/scripts/BouncerScript.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +跳跃脚本 - 让对象产生上下跳跃效果 +""" + +from core.script_system import ScriptBase +import math + +class BouncerScript(ScriptBase): + """跳跃脚本类""" + + def __init__(self): + super().__init__() + + # 跳跃参数 + self.jump_height = 2.0 # 跳跃高度 + self.jump_speed = 3.0 # 跳跃速度 (跳跃/秒) + self.bounce_type = "sine" # 跳跃类型: "sine", "abs_sine", "square" + + # 内部变量 + self.time_accumulator = 0.0 # 时间累积器 + self.original_y = None # 原始Y位置 + self.is_bouncing = True # 是否正在跳跃 + self.bounce_direction = 1 # 跳跃方向 + + def start(self): + """脚本开始时调用""" + self.log("跳跃脚本启动!") + self.log(f"跳跃参数: 高度={self.jump_height}, 速度={self.jump_speed}, 类型={self.bounce_type}") + + # 记录原始Y位置 + self.original_y = self.gameObject.getZ() # Z轴是高度 + self.log(f"原始高度: {self.original_y}") + + def update(self, dt): + """每帧更新""" + if not self.is_bouncing: + return + + # 累积时间 + self.time_accumulator += dt * self.bounce_direction + + # 根据类型计算跳跃高度 + if self.bounce_type == "sine": + # 标准正弦波跳跃 + height_offset = math.sin(self.time_accumulator * self.jump_speed * 2 * math.pi) * self.jump_height + elif self.bounce_type == "abs_sine": + # 绝对值正弦波(始终向上) + height_offset = abs(math.sin(self.time_accumulator * self.jump_speed * 2 * math.pi)) * self.jump_height + elif self.bounce_type == "square": + # 方波跳跃(突然跳起落下) + sine_val = math.sin(self.time_accumulator * self.jump_speed * 2 * math.pi) + height_offset = self.jump_height if sine_val > 0 else 0 + else: + height_offset = 0 + + # 应用跳跃 + current_pos = self.gameObject.getPos() + new_z = self.original_y + height_offset + self.gameObject.setPos(current_pos.getX(), current_pos.getY(), new_z) + + def set_bounce_parameters(self, height=None, speed=None, bounce_type=None): + """设置跳跃参数""" + if height is not None: + self.jump_height = height + if speed is not None: + self.jump_speed = speed + if bounce_type is not None and bounce_type in ["sine", "abs_sine", "square"]: + self.bounce_type = bounce_type + + self.log(f"跳跃参数更新: 高度={self.jump_height}, 速度={self.jump_speed}, 类型={self.bounce_type}") + + def toggle_bouncing(self): + """切换跳跃状态""" + self.is_bouncing = not self.is_bouncing + status = "恢复" if self.is_bouncing else "暂停" + self.log(f"跳跃{status}") + + def reverse_direction(self): + """反转跳跃方向""" + self.bounce_direction *= -1 + direction = "正向" if self.bounce_direction > 0 else "反向" + self.log(f"跳跃方向改为{direction}") + + def reset_position(self): + """重置到原始高度""" + if self.original_y is not None: + current_pos = self.gameObject.getPos() + self.gameObject.setPos(current_pos.getX(), current_pos.getY(), self.original_y) + self.time_accumulator = 0.0 + self.log("位置已重置到原始高度") + + def jump_once(self): + """执行一次跳跃""" + self.time_accumulator = 0.0 + self.log("执行单次跳跃") + + def on_destroy(self): + """脚本销毁时调用""" + self.log("跳跃脚本停止") \ No newline at end of file diff --git a/111/scripts/ColorChangerScript.py b/111/scripts/ColorChangerScript.py new file mode 100644 index 00000000..45dd59cb --- /dev/null +++ b/111/scripts/ColorChangerScript.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +颜色变化脚本 - 让对象颜色产生循环变化 +""" + +from core.script_system import ScriptBase +from panda3d.core import Vec4 +import math + +class ColorChangerScript(ScriptBase): + """颜色变化脚本类""" + + def __init__(self): + super().__init__() + + # 颜色参数 + self.color_speed = 1.0 # 颜色变化速度 (周期/秒) + self.color_mode = "rainbow" # 颜色模式: "rainbow", "pulse", "fade", "strobe" + self.base_color = Vec4(1, 1, 1, 1) # 基础颜色 + self.intensity = 1.0 # 颜色强度 + + # 内部变量 + self.time_accumulator = 0.0 # 时间累积器 + self.original_color = None # 原始颜色 + self.is_changing = True # 是否正在变化 + self.strobe_state = False # 闪烁状态 + + def start(self): + """脚本开始时调用""" + self.log("颜色变化脚本启动!") + self.log(f"颜色参数: 速度={self.color_speed}, 模式={self.color_mode}, 强度={self.intensity}") + + # 记录原始颜色 + self.original_color = self.gameObject.getColor() + self.log(f"原始颜色: {self.original_color}") + + def update(self, dt): + """每帧更新""" + if not self.is_changing: + return + + # 累积时间 + self.time_accumulator += dt + + # 根据模式计算新颜色 + if self.color_mode == "rainbow": + new_color = self._calculate_rainbow_color() + elif self.color_mode == "pulse": + new_color = self._calculate_pulse_color() + elif self.color_mode == "fade": + new_color = self._calculate_fade_color() + elif self.color_mode == "strobe": + new_color = self._calculate_strobe_color() + else: + new_color = self.base_color + + # 应用颜色 + self.gameObject.setColor(new_color) + + def _calculate_rainbow_color(self): + """计算彩虹颜色""" + # 使用HSV到RGB的转换创建彩虹效果 + hue = (self.time_accumulator * self.color_speed) % 1.0 + + # 简单的HSV到RGB转换 + i = int(hue * 6.0) + f = (hue * 6.0) - i + p = 0.0 + q = 1.0 - f + t = f + + if i % 6 == 0: + r, g, b = 1.0, t, p + elif i % 6 == 1: + r, g, b = q, 1.0, p + elif i % 6 == 2: + r, g, b = p, 1.0, t + elif i % 6 == 3: + r, g, b = p, q, 1.0 + elif i % 6 == 4: + r, g, b = t, p, 1.0 + else: + r, g, b = 1.0, p, q + + return Vec4(r * self.intensity, g * self.intensity, b * self.intensity, 1.0) + + def _calculate_pulse_color(self): + """计算脉冲颜色""" + pulse = (math.sin(self.time_accumulator * self.color_speed * 2 * math.pi) + 1.0) / 2.0 + multiplier = pulse * self.intensity + return Vec4( + self.base_color.getX() * multiplier, + self.base_color.getY() * multiplier, + self.base_color.getZ() * multiplier, + self.base_color.getW() + ) + + def _calculate_fade_color(self): + """计算淡入淡出颜色""" + fade = (math.sin(self.time_accumulator * self.color_speed * 2 * math.pi) + 1.0) / 2.0 + alpha = fade * self.intensity + return Vec4( + self.base_color.getX(), + self.base_color.getY(), + self.base_color.getZ(), + alpha + ) + + def _calculate_strobe_color(self): + """计算闪烁颜色""" + # 根据时间间隔切换状态 + interval = 1.0 / (self.color_speed * 2) # 闪烁间隔 + if int(self.time_accumulator / interval) % 2 == 0: + return Vec4( + self.base_color.getX() * self.intensity, + self.base_color.getY() * self.intensity, + self.base_color.getZ() * self.intensity, + self.base_color.getW() + ) + else: + return Vec4(0.1, 0.1, 0.1, self.base_color.getW()) # 暗色状态 + + def set_color_parameters(self, speed=None, mode=None, base_color=None, intensity=None): + """设置颜色参数""" + if speed is not None: + self.color_speed = speed + if mode is not None and mode in ["rainbow", "pulse", "fade", "strobe"]: + self.color_mode = mode + if base_color is not None: + self.base_color = base_color + if intensity is not None: + self.intensity = intensity + + self.log(f"颜色参数更新: 速度={self.color_speed}, 模式={self.color_mode}, 强度={self.intensity}") + + def toggle_color_change(self): + """切换颜色变化状态""" + self.is_changing = not self.is_changing + status = "恢复" if self.is_changing else "暂停" + self.log(f"颜色变化{status}") + + def reset_color(self): + """重置到原始颜色""" + if self.original_color: + self.gameObject.setColor(self.original_color) + self.time_accumulator = 0.0 + self.log("颜色已重置到原始值") + + def set_solid_color(self, r=1.0, g=1.0, b=1.0, a=1.0): + """设置固定颜色""" + color = Vec4(r, g, b, a) + self.gameObject.setColor(color) + self.base_color = color + self.log(f"设置固定颜色: {color}") + + def on_destroy(self): + """脚本销毁时调用""" + self.log("颜色变化脚本停止") \ No newline at end of file diff --git a/111/scripts/ComboAnimatorScript.py b/111/scripts/ComboAnimatorScript.py new file mode 100644 index 00000000..a39c2a78 --- /dev/null +++ b/111/scripts/ComboAnimatorScript.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +复合动画脚本 - 结合旋转和跳跃效果 +""" + +from core.script_system import ScriptBase +import math + +class ComboAnimatorScript(ScriptBase): + def __init__(self): + super().__init__() + self.time = 0.0 + self.original_pos = None + self.is_active = True + + def start(self): + self.log("复合动画脚本启动!") + self.original_pos = self.gameObject.getPos() + + def update(self, dt): + if not self.is_active: + return + + self.time += dt + + # 旋转效果 + current_hpr = self.gameObject.getHpr() + new_h = current_hpr.getX() + 45.0 * dt + self.gameObject.setHpr(new_h, current_hpr.getY(), current_hpr.getZ()) + + # 跳跃效果 + if self.original_pos: + bounce_offset = abs(math.sin(self.time * 3.0)) * 1.0 + self.gameObject.setZ(self.original_pos.getZ() + bounce_offset) + + def on_destroy(self): + self.log("复合动画脚本停止") \ No newline at end of file diff --git a/111/scripts/FollowerScript.py b/111/scripts/FollowerScript.py new file mode 100644 index 00000000..893406a2 --- /dev/null +++ b/111/scripts/FollowerScript.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +跟随脚本 - 让对象跟随指定的目标对象 +""" + +from core.script_system import ScriptBase +from panda3d.core import Vec3 + +class FollowerScript(ScriptBase): + """跟随脚本类""" + + def __init__(self): + super().__init__() + self.target = None # 跟随目标 + self.follow_speed = 5.0 # 跟随速度 + self.follow_distance = 2.0 # 跟随距离 + self.is_following = True # 是否正在跟随 + + def start(self): + """脚本开始时调用""" + self.log("跟随脚本启动!") + self.log(f"跟随参数: 速度={self.follow_speed}, 距离={self.follow_distance}") + + def update(self, dt): + """每帧更新""" + if not self.is_following or self.target is None: + return + + target_pos = self.target.getPos() + current_pos = self.gameObject.getPos() + + # 计算目标方向 + direction = target_pos - current_pos + distance = direction.length() + + # 如果距离大于跟随距离,则移动 + if distance > self.follow_distance: + if distance > 0: + direction.normalize() + + # 计算目标位置(保持跟随距离) + target_follow_pos = target_pos - direction * self.follow_distance + + # 平滑移动到目标位置 + move_direction = target_follow_pos - current_pos + move_distance = move_direction.length() + + if move_distance > 0: + move_direction.normalize() + move_amount = min(self.follow_speed * dt, move_distance) + new_pos = current_pos + move_direction * move_amount + self.gameObject.setPos(new_pos) + + # 朝向目标 + self.gameObject.lookAt(target_pos) + + def set_target(self, target): + """设置跟随目标""" + self.target = target + if target: + self.log(f"设置跟随目标: {target.getName()}") + else: + self.log("清除跟随目标") + + def on_destroy(self): + """脚本销毁时调用""" + self.log("跟随脚本停止") \ No newline at end of file diff --git a/111/scripts/MoverScript.py b/111/scripts/MoverScript.py new file mode 100644 index 00000000..b5c24099 --- /dev/null +++ b/111/scripts/MoverScript.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +移动脚本 - 让对象在指定方向上来回移动 +""" + +from core.script_system import ScriptBase +import math + +class MoverScript(ScriptBase): + """移动脚本类""" + + def __init__(self): + super().__init__() + + # 移动参数 + self.move_distance = 5.0 # 移动距离 + self.move_speed = 2.0 # 移动速度 (单位/秒) + self.move_axis = "x" # 移动轴: "x", "y", "z" + + # 内部变量 + self.start_position = None # 起始位置 + self.current_direction = 1 # 当前移动方向: 1或-1 + self.current_distance = 0.0 # 当前移动距离 + self.is_moving = True # 是否正在移动 + + def start(self): + """脚本开始时调用""" + self.log("移动脚本启动!") + self.log(f"移动参数: 距离={self.move_distance}, 速度={self.move_speed}, 轴={self.move_axis}") + + # 记录起始位置 + self.start_position = self.gameObject.getPos() + self.log(f"起始位置: {self.start_position}") + + def update(self, dt): + """每帧更新""" + if not self.is_moving or self.start_position is None: + return + + # 计算移动增量 + move_delta = self.move_speed * dt * self.current_direction + self.current_distance += abs(move_delta) + + # 检查是否需要改变方向 + if self.current_distance >= self.move_distance: + self.current_direction *= -1 + self.current_distance = 0.0 + + # 应用移动 + current_pos = self.gameObject.getPos() + new_pos = [current_pos.getX(), current_pos.getY(), current_pos.getZ()] + + if self.move_axis == "x": + new_pos[0] += move_delta + elif self.move_axis == "y": + new_pos[1] += move_delta + elif self.move_axis == "z": + new_pos[2] += move_delta + + self.gameObject.setPos(new_pos[0], new_pos[1], new_pos[2]) + + def set_move_parameters(self, distance=None, speed=None, axis=None): + """设置移动参数""" + if distance is not None: + self.move_distance = distance + if speed is not None: + self.move_speed = speed + if axis is not None and axis in ["x", "y", "z"]: + self.move_axis = axis + + self.log(f"移动参数更新: 距离={self.move_distance}, 速度={self.move_speed}, 轴={self.move_axis}") + + def toggle_movement(self): + """切换移动状态""" + self.is_moving = not self.is_moving + status = "恢复" if self.is_moving else "暂停" + self.log(f"移动{status}") + + def reset_position(self): + """重置到起始位置""" + if self.start_position: + self.gameObject.setPos(self.start_position) + self.current_distance = 0.0 + self.current_direction = 1 + self.log("位置已重置到起始点") + + def on_destroy(self): + """脚本销毁时调用""" + self.log("移动脚本停止") \ No newline at end of file diff --git a/111/scripts/R_P.py b/111/scripts/R_P.py new file mode 100644 index 00000000..ba6ae6b9 --- /dev/null +++ b/111/scripts/R_P.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +旋转脚本 - 让对象持续旋转 +""" + +from core.script_system import ScriptBase + + +class RotatorScript(ScriptBase): + """旋转脚本类""" + + def __init__(self): + super().__init__() + self.rotation_speed_y = 30.0 # Y轴旋转速度 (度/秒) + self.max_angle = 30.0 # 最大旋转角度(相对于初始角度) + self.direction = 1 + self.current_offset = 0.0 # 当前相对于初始角度的偏移 + self.initial_angle = None # 模型的初始角度 + self.is_rotating = True # 是否正在旋转 + + def start(self): + """脚本开始时调用""" + self.log("旋转脚本启动!") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + self.log(f"最大旋转角度: ±{self.max_angle}度") + + # 记录模型的初始角度 + if self.gameObject: + initial_hpr = self.gameObject.getHpr() + self.initial_angle = initial_hpr.getZ() # 记录Z轴的初始角度 + self.log(f"模型初始角度: {self.initial_angle}度") + self.log(f"旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") + else: + self.log("⚠️ 无法获取游戏对象,使用默认初始角度0") + self.initial_angle = 0.0 + + def update(self, dt): + """每帧更新""" + if not self.is_rotating or self.initial_angle is None: + return + + # 计算角度变化量 + delta_angle = self.rotation_speed_y * dt * self.direction + self.current_offset += delta_angle + + # 如果超出角度范围,则反向并限制在边界 + if self.current_offset > self.max_angle: + self.current_offset = self.max_angle + self.direction *= -1 + elif self.current_offset < -self.max_angle: + self.current_offset = -self.max_angle + self.direction *= -1 + + # 计算最终角度(初始角度 + 偏移量) + final_angle = self.initial_angle + self.current_offset + + # 设置新的旋转(只改变Z轴,保持其他不变) + current_hpr = self.gameObject.getHpr() + self.gameObject.setHpr(current_hpr.getX(), final_angle, current_hpr.getZ()) + + # if not self.is_rotating: + # return + # + # # 获取当前旋转并应用增量 + # current_hpr = self.gameObject.getHpr() + # new_r = current_hpr.getZ() + self.rotation_speed_y * dt + # self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), new_r) + + def on_destroy(self): + """脚本销毁时调用""" + self.log("旋转脚本停止") + + # ==================== 控制方法 ==================== + + def set_max_angle(self, new_max_angle): + """ + 设置新的最大旋转角度 + + Args: + new_max_angle: 新的最大角度值 + """ + self.max_angle = new_max_angle + self.log(f"最大旋转角度已设置为: ±{self.max_angle}度") + if self.initial_angle is not None: + self.log(f"新的旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") + + def set_rotation_speed(self, new_speed): + """ + 设置新的旋转速度 + + Args: + new_speed: 新的旋转速度(度/秒) + """ + self.rotation_speed_y = new_speed + self.log(f"旋转速度已设置为: {self.rotation_speed_y}度/秒") + + def pause_rotation(self): + """暂停旋转""" + self.is_rotating = False + self.log("旋转已暂停") + + def resume_rotation(self): + """恢复旋转""" + self.is_rotating = True + self.log("旋转已恢复") + + def reset_to_initial_angle(self): + """重置到初始角度""" + if self.initial_angle is not None and self.gameObject: + current_hpr = self.gameObject.getHpr() + self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), self.initial_angle) + self.current_offset = 0.0 + self.direction = 1 + self.log(f"已重置到初始角度: {self.initial_angle}度") + + def get_current_info(self): + """获取当前旋转信息""" + if self.gameObject and self.initial_angle is not None: + current_hpr = self.gameObject.getHpr() + current_angle = current_hpr.getZ() + self.log("=== 当前旋转信息 ===") + self.log(f"初始角度: {self.initial_angle}度") + self.log(f"当前角度: {current_angle}度") + self.log(f"偏移量: {self.current_offset}度") + self.log(f"最大角度: ±{self.max_angle}度") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + self.log(f"旋转方向: {'正向' if self.direction > 0 else '反向'}") + self.log(f"旋转状态: {'运行中' if self.is_rotating else '已暂停'}") + self.log("=== 信息结束 ===") + else: + self.log("无法获取旋转信息") \ No newline at end of file diff --git a/111/scripts/R_R.py b/111/scripts/R_R.py new file mode 100644 index 00000000..c78e88cb --- /dev/null +++ b/111/scripts/R_R.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +旋转脚本 - 让对象持续旋转 +""" + +from core.script_system import ScriptBase + + +class RotatorScript(ScriptBase): + """旋转脚本类""" + + def __init__(self): + super().__init__() + self.rotation_speed_y = 30.0 # Y轴旋转速度 (度/秒) + self.max_angle = 30.0 # 最大旋转角度(相对于初始角度) + self.direction = 1 + self.current_offset = 0.0 # 当前相对于初始角度的偏移 + self.initial_angle = None # 模型的初始角度 + self.is_rotating = True # 是否正在旋转 + + def start(self): + """脚本开始时调用""" + self.log("旋转脚本启动!") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + self.log(f"最大旋转角度: ±{self.max_angle}度") + + # 记录模型的初始角度 + if self.gameObject: + initial_hpr = self.gameObject.getHpr() + self.initial_angle = initial_hpr.getZ() # 记录Z轴的初始角度 + self.log(f"模型初始角度: {self.initial_angle}度") + self.log(f"旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") + else: + self.log("⚠️ 无法获取游戏对象,使用默认初始角度0") + self.initial_angle = 0.0 + + def update(self, dt): + """每帧更新""" + if not self.is_rotating or self.initial_angle is None: + return + + # 计算角度变化量 + delta_angle = self.rotation_speed_y * dt * self.direction + self.current_offset += delta_angle + + # 如果超出角度范围,则反向并限制在边界 + if self.current_offset > self.max_angle: + self.current_offset = self.max_angle + self.direction *= -1 + elif self.current_offset < -self.max_angle: + self.current_offset = -self.max_angle + self.direction *= -1 + + # 计算最终角度(初始角度 + 偏移量) + final_angle = self.initial_angle + self.current_offset + + # 设置新的旋转(只改变Z轴,保持其他不变) + current_hpr = self.gameObject.getHpr() + self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), final_angle) + + # if not self.is_rotating: + # return + # + # # 获取当前旋转并应用增量 + # current_hpr = self.gameObject.getHpr() + # new_r = current_hpr.getZ() + self.rotation_speed_y * dt + # self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), new_r) + + def on_destroy(self): + """脚本销毁时调用""" + self.log("旋转脚本停止") + + # ==================== 控制方法 ==================== + + def set_max_angle(self, new_max_angle): + """ + 设置新的最大旋转角度 + + Args: + new_max_angle: 新的最大角度值 + """ + self.max_angle = new_max_angle + self.log(f"最大旋转角度已设置为: ±{self.max_angle}度") + if self.initial_angle is not None: + self.log(f"新的旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") + + def set_rotation_speed(self, new_speed): + """ + 设置新的旋转速度 + + Args: + new_speed: 新的旋转速度(度/秒) + """ + self.rotation_speed_y = new_speed + self.log(f"旋转速度已设置为: {self.rotation_speed_y}度/秒") + + def pause_rotation(self): + """暂停旋转""" + self.is_rotating = False + self.log("旋转已暂停") + + def resume_rotation(self): + """恢复旋转""" + self.is_rotating = True + self.log("旋转已恢复") + + def reset_to_initial_angle(self): + """重置到初始角度""" + if self.initial_angle is not None and self.gameObject: + current_hpr = self.gameObject.getHpr() + self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), self.initial_angle) + self.current_offset = 0.0 + self.direction = 1 + self.log(f"已重置到初始角度: {self.initial_angle}度") + + def get_current_info(self): + """获取当前旋转信息""" + if self.gameObject and self.initial_angle is not None: + current_hpr = self.gameObject.getHpr() + current_angle = current_hpr.getZ() + self.log("=== 当前旋转信息 ===") + self.log(f"初始角度: {self.initial_angle}度") + self.log(f"当前角度: {current_angle}度") + self.log(f"偏移量: {self.current_offset}度") + self.log(f"最大角度: ±{self.max_angle}度") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + self.log(f"旋转方向: {'正向' if self.direction > 0 else '反向'}") + self.log(f"旋转状态: {'运行中' if self.is_rotating else '已暂停'}") + self.log("=== 信息结束 ===") + else: + self.log("无法获取旋转信息") \ No newline at end of file diff --git a/111/scripts/Rotate_H_Script.py b/111/scripts/Rotate_H_Script.py new file mode 100644 index 00000000..ffe0e294 --- /dev/null +++ b/111/scripts/Rotate_H_Script.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +旋转脚本 - 让对象持续旋转 +""" + +from core.script_system import ScriptBase + + +class RotatorScript(ScriptBase): + """旋转脚本类""" + + def __init__(self): + super().__init__() + self.rotation_speed_y = 30.0 # Y轴旋转速度 (度/秒) + self.max_angle = 30.0 # 最大旋转角度(相对于初始角度) + self.direction = 1 + self.current_offset = 0.0 # 当前相对于初始角度的偏移 + self.initial_angle = None # 模型的初始角度 + self.is_rotating = True # 是否正在旋转 + + # 机器人式停顿参数 + self.pause_duration = 0.5 # 停顿时间(秒) + self.current_pause_time = 0.0 # 当前停顿计时 + self.is_paused = False # 是否正在停顿 + self.robot_mode = True # 是否启用机器人模式 + + def start(self): + """脚本开始时调用""" + self.log("机器人旋转脚本启动!") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + self.log(f"最大旋转角度: ±{self.max_angle}度") + self.log(f"机器人模式: {'开启' if self.robot_mode else '关闭'}") + if self.robot_mode: + self.log(f"停顿时间: {self.pause_duration}秒") + + # 记录模型的初始角度 + if self.gameObject: + initial_hpr = self.gameObject.getHpr() + self.initial_angle = initial_hpr.getY() # 记录Y轴的初始角度(Pitch) + self.log(f"模型初始角度: {self.initial_angle}度") + self.log(f"旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") + else: + self.log("⚠️ 无法获取游戏对象,使用默认初始角度0") + self.initial_angle = 0.0 + + def update(self, dt): + """每帧更新 - 机器人式旋转""" + if not self.is_rotating or self.initial_angle is None: + return + + # 如果正在停顿中 + if self.is_paused: + self.current_pause_time += dt + if self.current_pause_time >= self.pause_duration: + # 停顿结束,继续旋转 + self.is_paused = False + self.current_pause_time = 0.0 + self.log(f"停顿结束,继续旋转,方向: {'正向' if self.direction > 0 else '反向'}") + return + + # 计算角度变化量 + delta_angle = self.rotation_speed_y * dt * self.direction + self.current_offset += delta_angle + + # 检查是否到达边界 + reached_boundary = False + if self.current_offset > self.max_angle: + self.current_offset = self.max_angle + self.direction *= -1 + reached_boundary = True + elif self.current_offset < -self.max_angle: + self.current_offset = -self.max_angle + self.direction *= -1 + reached_boundary = True + + # 如果到达边界且启用机器人模式,开始停顿 + if reached_boundary and self.robot_mode: + self.is_paused = True + self.current_pause_time = 0.0 + self.log(f"到达边界 ({self.current_offset}°),开始停顿 {self.pause_duration}秒") + + # 计算最终角度(初始角度 + 偏移量) + final_angle = self.initial_angle + self.current_offset + + # 设置新的旋转(只改变Y轴,保持其他不变) + current_hpr = self.gameObject.getHpr() + self.gameObject.setHpr(final_angle, current_hpr.getY(), current_hpr.getZ()) + + # if not self.is_rotating: + # return + # + # # 获取当前旋转并应用增量 + # current_hpr = self.gameObject.getHpr() + # new_r = current_hpr.getZ() + self.rotation_speed_y * dt + # self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), new_r) + + def on_destroy(self): + """脚本销毁时调用""" + self.log("机器人旋转脚本停止") + + # ==================== 机器人模式控制方法 ==================== + + def set_robot_mode(self, enabled=True): + """ + 启用或禁用机器人模式 + + Args: + enabled: 是否启用机器人模式 + """ + self.robot_mode = enabled + self.log(f"机器人模式: {'开启' if enabled else '关闭'}") + if not enabled: + self.is_paused = False # 如果禁用机器人模式,立即结束停顿 + + def set_pause_duration(self, duration): + """ + 设置停顿时间 + + Args: + duration: 停顿时间(秒) + """ + self.pause_duration = max(0.1, duration) # 最小0.1秒 + self.log(f"停顿时间已设置为: {self.pause_duration}秒") + + def set_max_angle(self, new_max_angle): + """ + 设置新的最大旋转角度 + + Args: + new_max_angle: 新的最大角度值 + """ + self.max_angle = new_max_angle + self.log(f"最大旋转角度已设置为: ±{self.max_angle}度") + if self.initial_angle is not None: + self.log(f"新的旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") + + def set_rotation_speed(self, new_speed): + """ + 设置新的旋转速度 + + Args: + new_speed: 新的旋转速度(度/秒) + """ + self.rotation_speed_y = new_speed + self.log(f"旋转速度已设置为: {self.rotation_speed_y}度/秒") + + def pause_rotation(self): + """暂停旋转""" + self.is_rotating = False + self.log("旋转已暂停") + + def resume_rotation(self): + """恢复旋转""" + self.is_rotating = True + self.is_paused = False # 同时结束停顿状态 + self.log("旋转已恢复") + + def reset_to_initial_angle(self): + """重置到初始角度""" + if self.initial_angle is not None and self.gameObject: + current_hpr = self.gameObject.getHpr() + self.gameObject.setHpr(current_hpr.getX(), self.initial_angle, current_hpr.getZ()) + self.current_offset = 0.0 + self.direction = 1 + self.is_paused = False + self.current_pause_time = 0.0 + self.log(f"已重置到初始角度: {self.initial_angle}度") + + def get_current_info(self): + """获取当前旋转信息""" + if self.gameObject and self.initial_angle is not None: + current_hpr = self.gameObject.getHpr() + current_angle = current_hpr.getY() + self.log("=== 机器人旋转信息 ===") + self.log(f"初始角度: {self.initial_angle}度") + self.log(f"当前角度: {current_angle}度") + self.log(f"偏移量: {self.current_offset}度") + self.log(f"最大角度: ±{self.max_angle}度") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + self.log(f"旋转方向: {'正向' if self.direction > 0 else '反向'}") + self.log(f"旋转状态: {'运行中' if self.is_rotating else '已暂停'}") + self.log(f"机器人模式: {'开启' if self.robot_mode else '关闭'}") + if self.robot_mode: + self.log(f"停顿时间: {self.pause_duration}秒") + self.log(f"当前状态: {'停顿中' if self.is_paused else '旋转中'}") + if self.is_paused: + remaining_time = self.pause_duration - self.current_pause_time + self.log(f"剩余停顿时间: {remaining_time:.1f}秒") + self.log("=== 信息结束 ===") + else: + self.log("无法获取旋转信息") + + # ==================== 预设配置方法 ==================== + + def set_slow_robot_mode(self): + """预设:慢速机器人模式""" + self.set_rotation_speed(15.0) + self.set_pause_duration(1.0) + self.set_robot_mode(True) + self.log("已设置为慢速机器人模式") + + def set_fast_robot_mode(self): + """预设:快速机器人模式""" + self.set_rotation_speed(45.0) + self.set_pause_duration(0.3) + self.set_robot_mode(True) + self.log("已设置为快速机器人模式") + + def set_smooth_mode(self): + """预设:平滑模式(非机器人)""" + self.set_robot_mode(False) + self.set_rotation_speed(30.0) + self.log("已设置为平滑旋转模式") \ No newline at end of file diff --git a/111/scripts/Rotate_P_Script.py b/111/scripts/Rotate_P_Script.py new file mode 100644 index 00000000..98d02cdf --- /dev/null +++ b/111/scripts/Rotate_P_Script.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +旋转脚本 - 让对象持续旋转 +""" + +from core.script_system import ScriptBase + + +class RotatorScript(ScriptBase): + """旋转脚本类""" + + def __init__(self): + super().__init__() + self.rotation_speed_y = 30.0 # Y轴旋转速度 (度/秒) + self.max_angle = 25.0 # 最大旋转角度(相对于初始角度) + self.direction = 1 + self.current_offset = 0.0 # 当前相对于初始角度的偏移 + self.initial_angle = None # 模型的初始角度 + self.is_rotating = True # 是否正在旋转 + + # 机器人式停顿参数 + self.pause_duration = 0.5 # 停顿时间(秒) + self.current_pause_time = 0.0 # 当前停顿计时 + self.is_paused = False # 是否正在停顿 + self.robot_mode = True # 是否启用机器人模式 + + def start(self): + """脚本开始时调用""" + self.log("机器人旋转脚本启动!") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + self.log(f"最大旋转角度: ±{self.max_angle}度") + self.log(f"机器人模式: {'开启' if self.robot_mode else '关闭'}") + if self.robot_mode: + self.log(f"停顿时间: {self.pause_duration}秒") + + # 记录模型的初始角度 + if self.gameObject: + initial_hpr = self.gameObject.getHpr() + self.initial_angle = initial_hpr.getY() # 记录Y轴的初始角度(Pitch) + self.log(f"模型初始角度: {self.initial_angle}度") + self.log(f"旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") + else: + self.log("⚠️ 无法获取游戏对象,使用默认初始角度0") + self.initial_angle = 0.0 + + def update(self, dt): + """每帧更新 - 机器人式旋转""" + if not self.is_rotating or self.initial_angle is None: + return + + # 如果正在停顿中 + if self.is_paused: + self.current_pause_time += dt + if self.current_pause_time >= self.pause_duration: + # 停顿结束,继续旋转 + self.is_paused = False + self.current_pause_time = 0.0 + self.log(f"停顿结束,继续旋转,方向: {'正向' if self.direction > 0 else '反向'}") + return + + # 计算角度变化量 + delta_angle = self.rotation_speed_y * dt * self.direction + self.current_offset += delta_angle + + # 检查是否到达边界 + reached_boundary = False + if self.current_offset > self.max_angle: + self.current_offset = self.max_angle + self.direction *= -1 + reached_boundary = True + elif self.current_offset < -self.max_angle: + self.current_offset = -self.max_angle + self.direction *= -1 + reached_boundary = True + + # 如果到达边界且启用机器人模式,开始停顿 + if reached_boundary and self.robot_mode: + self.is_paused = True + self.current_pause_time = 0.0 + self.log(f"到达边界 ({self.current_offset}°),开始停顿 {self.pause_duration}秒") + + # 计算最终角度(初始角度 + 偏移量) + final_angle = self.initial_angle + self.current_offset + + # 设置新的旋转(只改变Y轴,保持其他不变) + current_hpr = self.gameObject.getHpr() + self.gameObject.setHpr(current_hpr.getX(), final_angle, current_hpr.getZ()) + + # if not self.is_rotating: + # return + # + # # 获取当前旋转并应用增量 + # current_hpr = self.gameObject.getHpr() + # new_r = current_hpr.getZ() + self.rotation_speed_y * dt + # self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), new_r) + + def on_destroy(self): + """脚本销毁时调用""" + self.log("机器人旋转脚本停止") + + # ==================== 机器人模式控制方法 ==================== + + def set_robot_mode(self, enabled=True): + """ + 启用或禁用机器人模式 + + Args: + enabled: 是否启用机器人模式 + """ + self.robot_mode = enabled + self.log(f"机器人模式: {'开启' if enabled else '关闭'}") + if not enabled: + self.is_paused = False # 如果禁用机器人模式,立即结束停顿 + + def set_pause_duration(self, duration): + """ + 设置停顿时间 + + Args: + duration: 停顿时间(秒) + """ + self.pause_duration = max(0.1, duration) # 最小0.1秒 + self.log(f"停顿时间已设置为: {self.pause_duration}秒") + + def set_max_angle(self, new_max_angle): + """ + 设置新的最大旋转角度 + + Args: + new_max_angle: 新的最大角度值 + """ + self.max_angle = new_max_angle + self.log(f"最大旋转角度已设置为: ±{self.max_angle}度") + if self.initial_angle is not None: + self.log(f"新的旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") + + def set_rotation_speed(self, new_speed): + """ + 设置新的旋转速度 + + Args: + new_speed: 新的旋转速度(度/秒) + """ + self.rotation_speed_y = new_speed + self.log(f"旋转速度已设置为: {self.rotation_speed_y}度/秒") + + def pause_rotation(self): + """暂停旋转""" + self.is_rotating = False + self.log("旋转已暂停") + + def resume_rotation(self): + """恢复旋转""" + self.is_rotating = True + self.is_paused = False # 同时结束停顿状态 + self.log("旋转已恢复") + + def reset_to_initial_angle(self): + """重置到初始角度""" + if self.initial_angle is not None and self.gameObject: + current_hpr = self.gameObject.getHpr() + self.gameObject.setHpr(current_hpr.getX(), self.initial_angle, current_hpr.getZ()) + self.current_offset = 0.0 + self.direction = 1 + self.is_paused = False + self.current_pause_time = 0.0 + self.log(f"已重置到初始角度: {self.initial_angle}度") + + def get_current_info(self): + """获取当前旋转信息""" + if self.gameObject and self.initial_angle is not None: + current_hpr = self.gameObject.getHpr() + current_angle = current_hpr.getY() + self.log("=== 机器人旋转信息 ===") + self.log(f"初始角度: {self.initial_angle}度") + self.log(f"当前角度: {current_angle}度") + self.log(f"偏移量: {self.current_offset}度") + self.log(f"最大角度: ±{self.max_angle}度") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + self.log(f"旋转方向: {'正向' if self.direction > 0 else '反向'}") + self.log(f"旋转状态: {'运行中' if self.is_rotating else '已暂停'}") + self.log(f"机器人模式: {'开启' if self.robot_mode else '关闭'}") + if self.robot_mode: + self.log(f"停顿时间: {self.pause_duration}秒") + self.log(f"当前状态: {'停顿中' if self.is_paused else '旋转中'}") + if self.is_paused: + remaining_time = self.pause_duration - self.current_pause_time + self.log(f"剩余停顿时间: {remaining_time:.1f}秒") + self.log("=== 信息结束 ===") + else: + self.log("无法获取旋转信息") + + # ==================== 预设配置方法 ==================== + + def set_slow_robot_mode(self): + """预设:慢速机器人模式""" + self.set_rotation_speed(15.0) + self.set_pause_duration(1.0) + self.set_robot_mode(True) + self.log("已设置为慢速机器人模式") + + def set_fast_robot_mode(self): + """预设:快速机器人模式""" + self.set_rotation_speed(45.0) + self.set_pause_duration(0.3) + self.set_robot_mode(True) + self.log("已设置为快速机器人模式") + + def set_smooth_mode(self): + """预设:平滑模式(非机器人)""" + self.set_robot_mode(False) + self.set_rotation_speed(30.0) + self.log("已设置为平滑旋转模式") \ No newline at end of file diff --git a/111/scripts/Rotate_R_Script.py b/111/scripts/Rotate_R_Script.py new file mode 100644 index 00000000..4d22edac --- /dev/null +++ b/111/scripts/Rotate_R_Script.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +旋转脚本 - 让对象持续旋转 +""" + +from core.script_system import ScriptBase + + +class RotatorScript(ScriptBase): + """旋转脚本类""" + + def __init__(self): + super().__init__() + self.rotation_speed_y = 30.0 # Y轴旋转速度 (度/秒) + self.max_angle = 15.0 + self.direction = 1 + self.current_angle = 0.0 + self.is_rotating = True # 是否正在旋转 + + def start(self): + """脚本开始时调用""" + self.log("旋转脚本启动!") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + + def update(self, dt): + """每帧更新""" + + delta_angle = self.rotation_speed_y * dt * self.direction + self.current_angle += delta_angle + + # 如果超出角度范围,则反向 + if self.current_angle > self.max_angle: + self.current_angle = self.max_angle + self.direction *= -1 + elif self.current_angle < -self.max_angle: + self.current_angle = -self.max_angle + self.direction *= -1 + + # 设置新的旋转(只改变Z轴,保持其他不变) + base_hpr = self.gameObject.getHpr() + new_r = self.current_angle + self.gameObject.setHpr(base_hpr.getX(), base_hpr.getY(), new_r) + + # if not self.is_rotating: + # return + # + # # 获取当前旋转并应用增量 + # current_hpr = self.gameObject.getHpr() + # new_r = current_hpr.getZ() + self.rotation_speed_y * dt + # self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), new_r) + + def on_destroy(self): + """脚本销毁时调用""" + self.log("旋转脚本停止") \ No newline at end of file diff --git a/111/scripts/RotatorScript.py b/111/scripts/RotatorScript.py new file mode 100644 index 00000000..bdc49495 --- /dev/null +++ b/111/scripts/RotatorScript.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +旋转脚本 - 让对象持续旋转 +""" + +from core.script_system import ScriptBase + +class RotatorScript(ScriptBase): + """旋转脚本类""" + + def __init__(self): + super().__init__() + self.rotation_speed_y = 30.0 # Y轴旋转速度 (度/秒) + self.is_rotating = True # 是否正在旋转 + + def start(self): + """脚本开始时调用""" + self.log("旋转脚本启动!") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + + def update(self, dt): + # 检查 gameObject 是否存在且不为空 + if not self.gameObject or self.gameObject.isEmpty(): + print("RotatorScript: gameObject is empty or None, skipping update") + return + """每帧更新""" + if not self.is_rotating: + return + + # 获取当前旋转并应用增量 + current_hpr = self.gameObject.getHpr() + new_h = current_hpr.getX() + self.rotation_speed_y * dt + self.gameObject.setHpr(new_h, current_hpr.getY(), current_hpr.getZ()) + + def on_destroy(self): + """脚本销毁时调用""" + self.log("旋转脚本停止") \ No newline at end of file diff --git a/111/scripts/ScalerScript.py b/111/scripts/ScalerScript.py new file mode 100644 index 00000000..406d65fa --- /dev/null +++ b/111/scripts/ScalerScript.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +缩放脚本 - 让对象产生呼吸般的缩放效果 +""" + +from core.script_system import ScriptBase +import math + +class ScalerScript(ScriptBase): + """缩放脚本类""" + + def __init__(self): + super().__init__() + + # 缩放参数 + self.base_scale = 1.0 # 基础缩放 + self.scale_amplitude = 0.3 # 缩放幅度 + self.scale_speed = 2.0 # 缩放速度 (周期/秒) + self.uniform_scale = True # 是否统一缩放(所有轴) + + # 内部变量 + self.time_accumulator = 0.0 # 时间累积器 + self.original_scale = None # 原始缩放 + self.is_scaling = True # 是否正在缩放 + + def start(self): + """脚本开始时调用""" + self.log("缩放脚本启动!") + self.log(f"缩放参数: 基础={self.base_scale}, 幅度={self.scale_amplitude}, 速度={self.scale_speed}") + + # 记录原始缩放 + self.original_scale = self.gameObject.getScale() + self.log(f"原始缩放: {self.original_scale}") + + def update(self, dt): + """每帧更新""" + if not self.is_scaling: + return + + # 累积时间 + self.time_accumulator += dt + + # 计算正弦波缩放值 + sine_value = math.sin(self.time_accumulator * self.scale_speed * 2 * math.pi) + scale_factor = self.base_scale + (self.scale_amplitude * sine_value) + + # 应用缩放 + if self.uniform_scale: + # 统一缩放 + self.gameObject.setScale(scale_factor) + else: + # 非统一缩放(仅Z轴) + current_scale = self.gameObject.getScale() + self.gameObject.setScale(current_scale.getX(), current_scale.getY(), scale_factor) + + def set_scale_parameters(self, base=None, amplitude=None, speed=None, uniform=None): + """设置缩放参数""" + if base is not None: + self.base_scale = base + if amplitude is not None: + self.scale_amplitude = amplitude + if speed is not None: + self.scale_speed = speed + if uniform is not None: + self.uniform_scale = uniform + + self.log(f"缩放参数更新: 基础={self.base_scale}, 幅度={self.scale_amplitude}, 速度={self.scale_speed}") + + def toggle_scaling(self): + """切换缩放状态""" + self.is_scaling = not self.is_scaling + status = "恢复" if self.is_scaling else "暂停" + self.log(f"缩放{status}") + + def reset_scale(self): + """重置到原始缩放""" + if self.original_scale: + self.gameObject.setScale(self.original_scale) + self.time_accumulator = 0.0 + self.log("缩放已重置到原始值") + + def pulse_once(self): + """执行一次脉冲缩放""" + self.time_accumulator = 0.0 + self.log("执行脉冲缩放") + + def on_destroy(self): + """脚本销毁时调用""" + self.log("缩放脚本停止") \ No newline at end of file diff --git a/111/scripts/TestMover.py b/111/scripts/TestMover.py new file mode 100644 index 00000000..4116b1e3 --- /dev/null +++ b/111/scripts/TestMover.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +TestMover - 移动脚本 +""" + +from core.script_system import ScriptBase + +class Testmover(ScriptBase): + """移动脚本类""" + + def __init__(self): + super().__init__() + self.speed = 5.0 # 移动速度 + self.direction = [1, 0, 0] # 移动方向 + + def start(self): + """脚本开始时调用""" + self.log("移动脚本开始运行!") + + def update(self, dt): + """每帧更新""" + if self.transform: + # 计算移动偏移 + offset_x = self.direction[0] * self.speed * dt + offset_y = self.direction[1] * self.speed * dt + offset_z = self.direction[2] * self.speed * dt + + # 更新位置 + current_pos = self.transform.getPos() + new_pos = ( + current_pos.x + offset_x, + current_pos.y + offset_y, + current_pos.z + offset_z + ) + self.transform.setPos(*new_pos) + + def on_destroy(self): + """脚本销毁时调用""" + self.log("移动脚本被销毁") diff --git a/111/scripts/TestRotator.py b/111/scripts/TestRotator.py new file mode 100644 index 00000000..83042e31 --- /dev/null +++ b/111/scripts/TestRotator.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +TestRotator - 自定义脚本 +""" + +from core.script_system import ScriptBase + +class Testrotator(ScriptBase): + """自定义脚本类""" + + def __init__(self): + super().__init__() + # 在这里初始化您的变量 + + def start(self): + """脚本开始时调用""" + self.log("脚本开始运行!") + + def update(self, dt): + """每帧更新""" + # 在这里编写更新逻辑 + pass + + def on_destroy(self): + """脚本销毁时调用""" + self.log("脚本被销毁") diff --git a/111/scripts/TestScaler.py b/111/scripts/TestScaler.py new file mode 100644 index 00000000..536883d7 --- /dev/null +++ b/111/scripts/TestScaler.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +TestScaler - 自定义脚本 +""" + +from core.script_system import ScriptBase + +class Testscaler(ScriptBase): + """自定义脚本类""" + + def __init__(self): + super().__init__() + # 在这里初始化您的变量 + + def start(self): + """脚本开始时调用""" + self.log("脚本开始运行!") + + def update(self, dt): + """每帧更新""" + # 在这里编写更新逻辑 + pass + + def on_destroy(self): + """脚本销毁时调用""" + self.log("脚本被销毁") diff --git a/111/scripts/a.py b/111/scripts/a.py new file mode 100644 index 00000000..34bd5408 --- /dev/null +++ b/111/scripts/a.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +a - 自定义脚本 +""" + +from core.script_system import ScriptBase + +class A(ScriptBase): + """自定义脚本类""" + + def __init__(self): + super().__init__() + # 在这里初始化您的变量 + + def start(self): + """脚本开始时调用""" + self.log("脚本开始运行!") + + def update(self, dt): + """每帧更新""" + # 在这里编写更新逻辑 + pass + + def on_destroy(self): + """脚本销毁时调用""" + self.log("脚本被销毁") diff --git a/111/scripts/example_script.py b/111/scripts/example_script.py new file mode 100644 index 00000000..ecba0da9 --- /dev/null +++ b/111/scripts/example_script.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +示例脚本 - 演示如何编写脚本 +""" + +from core.script_system import ScriptBase + +class ExampleScript(ScriptBase): + """示例脚本类""" + + def __init__(self): + super().__init__() + self.counter = 0 + self.rotation_speed = 30.0 # 度/秒 + + def start(self): + """脚本开始时调用""" + self.log("示例脚本开始运行!") + self.log(f"挂载到对象: {self.gameObject.getName()}") + + def update(self, dt): + """每帧更新""" + self.counter += 1 + + # 每60帧输出一次信息 + if self.counter % 60 == 0: + self.log(f"运行了 {self.counter} 帧") + + # 让对象旋转 + if self.transform: + current_h = self.transform.getH() + new_h = current_h + self.rotation_speed * dt + self.transform.setH(new_h) + + def on_destroy(self): + """脚本销毁时调用""" + self.log("示例脚本被销毁") + + def on_enable(self): + """脚本启用时调用""" + self.log("示例脚本被启用") + + def on_disable(self): + """脚本禁用时调用""" + self.log("示例脚本被禁用") diff --git a/111/scripts/test_quick_script.py b/111/scripts/test_quick_script.py new file mode 100644 index 00000000..716070a3 --- /dev/null +++ b/111/scripts/test_quick_script.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +test_quick_script - 自定义脚本 +""" + +from core.script_system import ScriptBase + +class TestQuickScript(ScriptBase): + """自定义脚本类""" + + def __init__(self): + super().__init__() + # 在这里初始化您的变量 + + def start(self): + """脚本开始时调用""" + self.log("脚本开始运行!") + + def update(self, dt): + """每帧更新""" + # 在这里编写更新逻辑 + pass + + def on_destroy(self): + """脚本销毁时调用""" + self.log("脚本被销毁") diff --git a/111/新项目/project.json b/111/新项目/project.json new file mode 100644 index 00000000..d90be3fc --- /dev/null +++ b/111/新项目/project.json @@ -0,0 +1,8 @@ +{ + "name": "新项目", + "path": "D:\\IMGUI\\EG\\111\\新项目", + "last_modified": "2026-03-18 12:12:50", + "scene_file": "scenes\\scene.bam", + "created_at": "2026-03-18 11:31:59", + "version": "1.0" +} \ No newline at end of file diff --git a/111/新项目/scenes/scene.bam b/111/新项目/scenes/scene.bam new file mode 100644 index 00000000..91e9d2aa Binary files /dev/null and b/111/新项目/scenes/scene.bam differ diff --git a/111/新项目/scripts/BouncerScript.py b/111/新项目/scripts/BouncerScript.py new file mode 100644 index 00000000..60fb4170 --- /dev/null +++ b/111/新项目/scripts/BouncerScript.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +跳跃脚本 - 让对象产生上下跳跃效果 +""" + +from core.script_system import ScriptBase +import math + +class BouncerScript(ScriptBase): + """跳跃脚本类""" + + def __init__(self): + super().__init__() + + # 跳跃参数 + self.jump_height = 2.0 # 跳跃高度 + self.jump_speed = 3.0 # 跳跃速度 (跳跃/秒) + self.bounce_type = "sine" # 跳跃类型: "sine", "abs_sine", "square" + + # 内部变量 + self.time_accumulator = 0.0 # 时间累积器 + self.original_y = None # 原始Y位置 + self.is_bouncing = True # 是否正在跳跃 + self.bounce_direction = 1 # 跳跃方向 + + def start(self): + """脚本开始时调用""" + self.log("跳跃脚本启动!") + self.log(f"跳跃参数: 高度={self.jump_height}, 速度={self.jump_speed}, 类型={self.bounce_type}") + + # 记录原始Y位置 + self.original_y = self.gameObject.getZ() # Z轴是高度 + self.log(f"原始高度: {self.original_y}") + + def update(self, dt): + """每帧更新""" + if not self.is_bouncing: + return + + # 累积时间 + self.time_accumulator += dt * self.bounce_direction + + # 根据类型计算跳跃高度 + if self.bounce_type == "sine": + # 标准正弦波跳跃 + height_offset = math.sin(self.time_accumulator * self.jump_speed * 2 * math.pi) * self.jump_height + elif self.bounce_type == "abs_sine": + # 绝对值正弦波(始终向上) + height_offset = abs(math.sin(self.time_accumulator * self.jump_speed * 2 * math.pi)) * self.jump_height + elif self.bounce_type == "square": + # 方波跳跃(突然跳起落下) + sine_val = math.sin(self.time_accumulator * self.jump_speed * 2 * math.pi) + height_offset = self.jump_height if sine_val > 0 else 0 + else: + height_offset = 0 + + # 应用跳跃 + current_pos = self.gameObject.getPos() + new_z = self.original_y + height_offset + self.gameObject.setPos(current_pos.getX(), current_pos.getY(), new_z) + + def set_bounce_parameters(self, height=None, speed=None, bounce_type=None): + """设置跳跃参数""" + if height is not None: + self.jump_height = height + if speed is not None: + self.jump_speed = speed + if bounce_type is not None and bounce_type in ["sine", "abs_sine", "square"]: + self.bounce_type = bounce_type + + self.log(f"跳跃参数更新: 高度={self.jump_height}, 速度={self.jump_speed}, 类型={self.bounce_type}") + + def toggle_bouncing(self): + """切换跳跃状态""" + self.is_bouncing = not self.is_bouncing + status = "恢复" if self.is_bouncing else "暂停" + self.log(f"跳跃{status}") + + def reverse_direction(self): + """反转跳跃方向""" + self.bounce_direction *= -1 + direction = "正向" if self.bounce_direction > 0 else "反向" + self.log(f"跳跃方向改为{direction}") + + def reset_position(self): + """重置到原始高度""" + if self.original_y is not None: + current_pos = self.gameObject.getPos() + self.gameObject.setPos(current_pos.getX(), current_pos.getY(), self.original_y) + self.time_accumulator = 0.0 + self.log("位置已重置到原始高度") + + def jump_once(self): + """执行一次跳跃""" + self.time_accumulator = 0.0 + self.log("执行单次跳跃") + + def on_destroy(self): + """脚本销毁时调用""" + self.log("跳跃脚本停止") \ No newline at end of file diff --git a/111/新项目/scripts/ColorChangerScript.py b/111/新项目/scripts/ColorChangerScript.py new file mode 100644 index 00000000..45dd59cb --- /dev/null +++ b/111/新项目/scripts/ColorChangerScript.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +颜色变化脚本 - 让对象颜色产生循环变化 +""" + +from core.script_system import ScriptBase +from panda3d.core import Vec4 +import math + +class ColorChangerScript(ScriptBase): + """颜色变化脚本类""" + + def __init__(self): + super().__init__() + + # 颜色参数 + self.color_speed = 1.0 # 颜色变化速度 (周期/秒) + self.color_mode = "rainbow" # 颜色模式: "rainbow", "pulse", "fade", "strobe" + self.base_color = Vec4(1, 1, 1, 1) # 基础颜色 + self.intensity = 1.0 # 颜色强度 + + # 内部变量 + self.time_accumulator = 0.0 # 时间累积器 + self.original_color = None # 原始颜色 + self.is_changing = True # 是否正在变化 + self.strobe_state = False # 闪烁状态 + + def start(self): + """脚本开始时调用""" + self.log("颜色变化脚本启动!") + self.log(f"颜色参数: 速度={self.color_speed}, 模式={self.color_mode}, 强度={self.intensity}") + + # 记录原始颜色 + self.original_color = self.gameObject.getColor() + self.log(f"原始颜色: {self.original_color}") + + def update(self, dt): + """每帧更新""" + if not self.is_changing: + return + + # 累积时间 + self.time_accumulator += dt + + # 根据模式计算新颜色 + if self.color_mode == "rainbow": + new_color = self._calculate_rainbow_color() + elif self.color_mode == "pulse": + new_color = self._calculate_pulse_color() + elif self.color_mode == "fade": + new_color = self._calculate_fade_color() + elif self.color_mode == "strobe": + new_color = self._calculate_strobe_color() + else: + new_color = self.base_color + + # 应用颜色 + self.gameObject.setColor(new_color) + + def _calculate_rainbow_color(self): + """计算彩虹颜色""" + # 使用HSV到RGB的转换创建彩虹效果 + hue = (self.time_accumulator * self.color_speed) % 1.0 + + # 简单的HSV到RGB转换 + i = int(hue * 6.0) + f = (hue * 6.0) - i + p = 0.0 + q = 1.0 - f + t = f + + if i % 6 == 0: + r, g, b = 1.0, t, p + elif i % 6 == 1: + r, g, b = q, 1.0, p + elif i % 6 == 2: + r, g, b = p, 1.0, t + elif i % 6 == 3: + r, g, b = p, q, 1.0 + elif i % 6 == 4: + r, g, b = t, p, 1.0 + else: + r, g, b = 1.0, p, q + + return Vec4(r * self.intensity, g * self.intensity, b * self.intensity, 1.0) + + def _calculate_pulse_color(self): + """计算脉冲颜色""" + pulse = (math.sin(self.time_accumulator * self.color_speed * 2 * math.pi) + 1.0) / 2.0 + multiplier = pulse * self.intensity + return Vec4( + self.base_color.getX() * multiplier, + self.base_color.getY() * multiplier, + self.base_color.getZ() * multiplier, + self.base_color.getW() + ) + + def _calculate_fade_color(self): + """计算淡入淡出颜色""" + fade = (math.sin(self.time_accumulator * self.color_speed * 2 * math.pi) + 1.0) / 2.0 + alpha = fade * self.intensity + return Vec4( + self.base_color.getX(), + self.base_color.getY(), + self.base_color.getZ(), + alpha + ) + + def _calculate_strobe_color(self): + """计算闪烁颜色""" + # 根据时间间隔切换状态 + interval = 1.0 / (self.color_speed * 2) # 闪烁间隔 + if int(self.time_accumulator / interval) % 2 == 0: + return Vec4( + self.base_color.getX() * self.intensity, + self.base_color.getY() * self.intensity, + self.base_color.getZ() * self.intensity, + self.base_color.getW() + ) + else: + return Vec4(0.1, 0.1, 0.1, self.base_color.getW()) # 暗色状态 + + def set_color_parameters(self, speed=None, mode=None, base_color=None, intensity=None): + """设置颜色参数""" + if speed is not None: + self.color_speed = speed + if mode is not None and mode in ["rainbow", "pulse", "fade", "strobe"]: + self.color_mode = mode + if base_color is not None: + self.base_color = base_color + if intensity is not None: + self.intensity = intensity + + self.log(f"颜色参数更新: 速度={self.color_speed}, 模式={self.color_mode}, 强度={self.intensity}") + + def toggle_color_change(self): + """切换颜色变化状态""" + self.is_changing = not self.is_changing + status = "恢复" if self.is_changing else "暂停" + self.log(f"颜色变化{status}") + + def reset_color(self): + """重置到原始颜色""" + if self.original_color: + self.gameObject.setColor(self.original_color) + self.time_accumulator = 0.0 + self.log("颜色已重置到原始值") + + def set_solid_color(self, r=1.0, g=1.0, b=1.0, a=1.0): + """设置固定颜色""" + color = Vec4(r, g, b, a) + self.gameObject.setColor(color) + self.base_color = color + self.log(f"设置固定颜色: {color}") + + def on_destroy(self): + """脚本销毁时调用""" + self.log("颜色变化脚本停止") \ No newline at end of file diff --git a/111/新项目/scripts/ComboAnimatorScript.py b/111/新项目/scripts/ComboAnimatorScript.py new file mode 100644 index 00000000..a39c2a78 --- /dev/null +++ b/111/新项目/scripts/ComboAnimatorScript.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +复合动画脚本 - 结合旋转和跳跃效果 +""" + +from core.script_system import ScriptBase +import math + +class ComboAnimatorScript(ScriptBase): + def __init__(self): + super().__init__() + self.time = 0.0 + self.original_pos = None + self.is_active = True + + def start(self): + self.log("复合动画脚本启动!") + self.original_pos = self.gameObject.getPos() + + def update(self, dt): + if not self.is_active: + return + + self.time += dt + + # 旋转效果 + current_hpr = self.gameObject.getHpr() + new_h = current_hpr.getX() + 45.0 * dt + self.gameObject.setHpr(new_h, current_hpr.getY(), current_hpr.getZ()) + + # 跳跃效果 + if self.original_pos: + bounce_offset = abs(math.sin(self.time * 3.0)) * 1.0 + self.gameObject.setZ(self.original_pos.getZ() + bounce_offset) + + def on_destroy(self): + self.log("复合动画脚本停止") \ No newline at end of file diff --git a/111/新项目/scripts/FollowerScript.py b/111/新项目/scripts/FollowerScript.py new file mode 100644 index 00000000..893406a2 --- /dev/null +++ b/111/新项目/scripts/FollowerScript.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +跟随脚本 - 让对象跟随指定的目标对象 +""" + +from core.script_system import ScriptBase +from panda3d.core import Vec3 + +class FollowerScript(ScriptBase): + """跟随脚本类""" + + def __init__(self): + super().__init__() + self.target = None # 跟随目标 + self.follow_speed = 5.0 # 跟随速度 + self.follow_distance = 2.0 # 跟随距离 + self.is_following = True # 是否正在跟随 + + def start(self): + """脚本开始时调用""" + self.log("跟随脚本启动!") + self.log(f"跟随参数: 速度={self.follow_speed}, 距离={self.follow_distance}") + + def update(self, dt): + """每帧更新""" + if not self.is_following or self.target is None: + return + + target_pos = self.target.getPos() + current_pos = self.gameObject.getPos() + + # 计算目标方向 + direction = target_pos - current_pos + distance = direction.length() + + # 如果距离大于跟随距离,则移动 + if distance > self.follow_distance: + if distance > 0: + direction.normalize() + + # 计算目标位置(保持跟随距离) + target_follow_pos = target_pos - direction * self.follow_distance + + # 平滑移动到目标位置 + move_direction = target_follow_pos - current_pos + move_distance = move_direction.length() + + if move_distance > 0: + move_direction.normalize() + move_amount = min(self.follow_speed * dt, move_distance) + new_pos = current_pos + move_direction * move_amount + self.gameObject.setPos(new_pos) + + # 朝向目标 + self.gameObject.lookAt(target_pos) + + def set_target(self, target): + """设置跟随目标""" + self.target = target + if target: + self.log(f"设置跟随目标: {target.getName()}") + else: + self.log("清除跟随目标") + + def on_destroy(self): + """脚本销毁时调用""" + self.log("跟随脚本停止") \ No newline at end of file diff --git a/111/新项目/scripts/MoverScript.py b/111/新项目/scripts/MoverScript.py new file mode 100644 index 00000000..b5c24099 --- /dev/null +++ b/111/新项目/scripts/MoverScript.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +移动脚本 - 让对象在指定方向上来回移动 +""" + +from core.script_system import ScriptBase +import math + +class MoverScript(ScriptBase): + """移动脚本类""" + + def __init__(self): + super().__init__() + + # 移动参数 + self.move_distance = 5.0 # 移动距离 + self.move_speed = 2.0 # 移动速度 (单位/秒) + self.move_axis = "x" # 移动轴: "x", "y", "z" + + # 内部变量 + self.start_position = None # 起始位置 + self.current_direction = 1 # 当前移动方向: 1或-1 + self.current_distance = 0.0 # 当前移动距离 + self.is_moving = True # 是否正在移动 + + def start(self): + """脚本开始时调用""" + self.log("移动脚本启动!") + self.log(f"移动参数: 距离={self.move_distance}, 速度={self.move_speed}, 轴={self.move_axis}") + + # 记录起始位置 + self.start_position = self.gameObject.getPos() + self.log(f"起始位置: {self.start_position}") + + def update(self, dt): + """每帧更新""" + if not self.is_moving or self.start_position is None: + return + + # 计算移动增量 + move_delta = self.move_speed * dt * self.current_direction + self.current_distance += abs(move_delta) + + # 检查是否需要改变方向 + if self.current_distance >= self.move_distance: + self.current_direction *= -1 + self.current_distance = 0.0 + + # 应用移动 + current_pos = self.gameObject.getPos() + new_pos = [current_pos.getX(), current_pos.getY(), current_pos.getZ()] + + if self.move_axis == "x": + new_pos[0] += move_delta + elif self.move_axis == "y": + new_pos[1] += move_delta + elif self.move_axis == "z": + new_pos[2] += move_delta + + self.gameObject.setPos(new_pos[0], new_pos[1], new_pos[2]) + + def set_move_parameters(self, distance=None, speed=None, axis=None): + """设置移动参数""" + if distance is not None: + self.move_distance = distance + if speed is not None: + self.move_speed = speed + if axis is not None and axis in ["x", "y", "z"]: + self.move_axis = axis + + self.log(f"移动参数更新: 距离={self.move_distance}, 速度={self.move_speed}, 轴={self.move_axis}") + + def toggle_movement(self): + """切换移动状态""" + self.is_moving = not self.is_moving + status = "恢复" if self.is_moving else "暂停" + self.log(f"移动{status}") + + def reset_position(self): + """重置到起始位置""" + if self.start_position: + self.gameObject.setPos(self.start_position) + self.current_distance = 0.0 + self.current_direction = 1 + self.log("位置已重置到起始点") + + def on_destroy(self): + """脚本销毁时调用""" + self.log("移动脚本停止") \ No newline at end of file diff --git a/111/新项目/scripts/R_P.py b/111/新项目/scripts/R_P.py new file mode 100644 index 00000000..ba6ae6b9 --- /dev/null +++ b/111/新项目/scripts/R_P.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +旋转脚本 - 让对象持续旋转 +""" + +from core.script_system import ScriptBase + + +class RotatorScript(ScriptBase): + """旋转脚本类""" + + def __init__(self): + super().__init__() + self.rotation_speed_y = 30.0 # Y轴旋转速度 (度/秒) + self.max_angle = 30.0 # 最大旋转角度(相对于初始角度) + self.direction = 1 + self.current_offset = 0.0 # 当前相对于初始角度的偏移 + self.initial_angle = None # 模型的初始角度 + self.is_rotating = True # 是否正在旋转 + + def start(self): + """脚本开始时调用""" + self.log("旋转脚本启动!") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + self.log(f"最大旋转角度: ±{self.max_angle}度") + + # 记录模型的初始角度 + if self.gameObject: + initial_hpr = self.gameObject.getHpr() + self.initial_angle = initial_hpr.getZ() # 记录Z轴的初始角度 + self.log(f"模型初始角度: {self.initial_angle}度") + self.log(f"旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") + else: + self.log("⚠️ 无法获取游戏对象,使用默认初始角度0") + self.initial_angle = 0.0 + + def update(self, dt): + """每帧更新""" + if not self.is_rotating or self.initial_angle is None: + return + + # 计算角度变化量 + delta_angle = self.rotation_speed_y * dt * self.direction + self.current_offset += delta_angle + + # 如果超出角度范围,则反向并限制在边界 + if self.current_offset > self.max_angle: + self.current_offset = self.max_angle + self.direction *= -1 + elif self.current_offset < -self.max_angle: + self.current_offset = -self.max_angle + self.direction *= -1 + + # 计算最终角度(初始角度 + 偏移量) + final_angle = self.initial_angle + self.current_offset + + # 设置新的旋转(只改变Z轴,保持其他不变) + current_hpr = self.gameObject.getHpr() + self.gameObject.setHpr(current_hpr.getX(), final_angle, current_hpr.getZ()) + + # if not self.is_rotating: + # return + # + # # 获取当前旋转并应用增量 + # current_hpr = self.gameObject.getHpr() + # new_r = current_hpr.getZ() + self.rotation_speed_y * dt + # self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), new_r) + + def on_destroy(self): + """脚本销毁时调用""" + self.log("旋转脚本停止") + + # ==================== 控制方法 ==================== + + def set_max_angle(self, new_max_angle): + """ + 设置新的最大旋转角度 + + Args: + new_max_angle: 新的最大角度值 + """ + self.max_angle = new_max_angle + self.log(f"最大旋转角度已设置为: ±{self.max_angle}度") + if self.initial_angle is not None: + self.log(f"新的旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") + + def set_rotation_speed(self, new_speed): + """ + 设置新的旋转速度 + + Args: + new_speed: 新的旋转速度(度/秒) + """ + self.rotation_speed_y = new_speed + self.log(f"旋转速度已设置为: {self.rotation_speed_y}度/秒") + + def pause_rotation(self): + """暂停旋转""" + self.is_rotating = False + self.log("旋转已暂停") + + def resume_rotation(self): + """恢复旋转""" + self.is_rotating = True + self.log("旋转已恢复") + + def reset_to_initial_angle(self): + """重置到初始角度""" + if self.initial_angle is not None and self.gameObject: + current_hpr = self.gameObject.getHpr() + self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), self.initial_angle) + self.current_offset = 0.0 + self.direction = 1 + self.log(f"已重置到初始角度: {self.initial_angle}度") + + def get_current_info(self): + """获取当前旋转信息""" + if self.gameObject and self.initial_angle is not None: + current_hpr = self.gameObject.getHpr() + current_angle = current_hpr.getZ() + self.log("=== 当前旋转信息 ===") + self.log(f"初始角度: {self.initial_angle}度") + self.log(f"当前角度: {current_angle}度") + self.log(f"偏移量: {self.current_offset}度") + self.log(f"最大角度: ±{self.max_angle}度") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + self.log(f"旋转方向: {'正向' if self.direction > 0 else '反向'}") + self.log(f"旋转状态: {'运行中' if self.is_rotating else '已暂停'}") + self.log("=== 信息结束 ===") + else: + self.log("无法获取旋转信息") \ No newline at end of file diff --git a/111/新项目/scripts/R_R.py b/111/新项目/scripts/R_R.py new file mode 100644 index 00000000..c78e88cb --- /dev/null +++ b/111/新项目/scripts/R_R.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +旋转脚本 - 让对象持续旋转 +""" + +from core.script_system import ScriptBase + + +class RotatorScript(ScriptBase): + """旋转脚本类""" + + def __init__(self): + super().__init__() + self.rotation_speed_y = 30.0 # Y轴旋转速度 (度/秒) + self.max_angle = 30.0 # 最大旋转角度(相对于初始角度) + self.direction = 1 + self.current_offset = 0.0 # 当前相对于初始角度的偏移 + self.initial_angle = None # 模型的初始角度 + self.is_rotating = True # 是否正在旋转 + + def start(self): + """脚本开始时调用""" + self.log("旋转脚本启动!") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + self.log(f"最大旋转角度: ±{self.max_angle}度") + + # 记录模型的初始角度 + if self.gameObject: + initial_hpr = self.gameObject.getHpr() + self.initial_angle = initial_hpr.getZ() # 记录Z轴的初始角度 + self.log(f"模型初始角度: {self.initial_angle}度") + self.log(f"旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") + else: + self.log("⚠️ 无法获取游戏对象,使用默认初始角度0") + self.initial_angle = 0.0 + + def update(self, dt): + """每帧更新""" + if not self.is_rotating or self.initial_angle is None: + return + + # 计算角度变化量 + delta_angle = self.rotation_speed_y * dt * self.direction + self.current_offset += delta_angle + + # 如果超出角度范围,则反向并限制在边界 + if self.current_offset > self.max_angle: + self.current_offset = self.max_angle + self.direction *= -1 + elif self.current_offset < -self.max_angle: + self.current_offset = -self.max_angle + self.direction *= -1 + + # 计算最终角度(初始角度 + 偏移量) + final_angle = self.initial_angle + self.current_offset + + # 设置新的旋转(只改变Z轴,保持其他不变) + current_hpr = self.gameObject.getHpr() + self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), final_angle) + + # if not self.is_rotating: + # return + # + # # 获取当前旋转并应用增量 + # current_hpr = self.gameObject.getHpr() + # new_r = current_hpr.getZ() + self.rotation_speed_y * dt + # self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), new_r) + + def on_destroy(self): + """脚本销毁时调用""" + self.log("旋转脚本停止") + + # ==================== 控制方法 ==================== + + def set_max_angle(self, new_max_angle): + """ + 设置新的最大旋转角度 + + Args: + new_max_angle: 新的最大角度值 + """ + self.max_angle = new_max_angle + self.log(f"最大旋转角度已设置为: ±{self.max_angle}度") + if self.initial_angle is not None: + self.log(f"新的旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") + + def set_rotation_speed(self, new_speed): + """ + 设置新的旋转速度 + + Args: + new_speed: 新的旋转速度(度/秒) + """ + self.rotation_speed_y = new_speed + self.log(f"旋转速度已设置为: {self.rotation_speed_y}度/秒") + + def pause_rotation(self): + """暂停旋转""" + self.is_rotating = False + self.log("旋转已暂停") + + def resume_rotation(self): + """恢复旋转""" + self.is_rotating = True + self.log("旋转已恢复") + + def reset_to_initial_angle(self): + """重置到初始角度""" + if self.initial_angle is not None and self.gameObject: + current_hpr = self.gameObject.getHpr() + self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), self.initial_angle) + self.current_offset = 0.0 + self.direction = 1 + self.log(f"已重置到初始角度: {self.initial_angle}度") + + def get_current_info(self): + """获取当前旋转信息""" + if self.gameObject and self.initial_angle is not None: + current_hpr = self.gameObject.getHpr() + current_angle = current_hpr.getZ() + self.log("=== 当前旋转信息 ===") + self.log(f"初始角度: {self.initial_angle}度") + self.log(f"当前角度: {current_angle}度") + self.log(f"偏移量: {self.current_offset}度") + self.log(f"最大角度: ±{self.max_angle}度") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + self.log(f"旋转方向: {'正向' if self.direction > 0 else '反向'}") + self.log(f"旋转状态: {'运行中' if self.is_rotating else '已暂停'}") + self.log("=== 信息结束 ===") + else: + self.log("无法获取旋转信息") \ No newline at end of file diff --git a/111/新项目/scripts/Rotate_H_Script.py b/111/新项目/scripts/Rotate_H_Script.py new file mode 100644 index 00000000..ffe0e294 --- /dev/null +++ b/111/新项目/scripts/Rotate_H_Script.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +旋转脚本 - 让对象持续旋转 +""" + +from core.script_system import ScriptBase + + +class RotatorScript(ScriptBase): + """旋转脚本类""" + + def __init__(self): + super().__init__() + self.rotation_speed_y = 30.0 # Y轴旋转速度 (度/秒) + self.max_angle = 30.0 # 最大旋转角度(相对于初始角度) + self.direction = 1 + self.current_offset = 0.0 # 当前相对于初始角度的偏移 + self.initial_angle = None # 模型的初始角度 + self.is_rotating = True # 是否正在旋转 + + # 机器人式停顿参数 + self.pause_duration = 0.5 # 停顿时间(秒) + self.current_pause_time = 0.0 # 当前停顿计时 + self.is_paused = False # 是否正在停顿 + self.robot_mode = True # 是否启用机器人模式 + + def start(self): + """脚本开始时调用""" + self.log("机器人旋转脚本启动!") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + self.log(f"最大旋转角度: ±{self.max_angle}度") + self.log(f"机器人模式: {'开启' if self.robot_mode else '关闭'}") + if self.robot_mode: + self.log(f"停顿时间: {self.pause_duration}秒") + + # 记录模型的初始角度 + if self.gameObject: + initial_hpr = self.gameObject.getHpr() + self.initial_angle = initial_hpr.getY() # 记录Y轴的初始角度(Pitch) + self.log(f"模型初始角度: {self.initial_angle}度") + self.log(f"旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") + else: + self.log("⚠️ 无法获取游戏对象,使用默认初始角度0") + self.initial_angle = 0.0 + + def update(self, dt): + """每帧更新 - 机器人式旋转""" + if not self.is_rotating or self.initial_angle is None: + return + + # 如果正在停顿中 + if self.is_paused: + self.current_pause_time += dt + if self.current_pause_time >= self.pause_duration: + # 停顿结束,继续旋转 + self.is_paused = False + self.current_pause_time = 0.0 + self.log(f"停顿结束,继续旋转,方向: {'正向' if self.direction > 0 else '反向'}") + return + + # 计算角度变化量 + delta_angle = self.rotation_speed_y * dt * self.direction + self.current_offset += delta_angle + + # 检查是否到达边界 + reached_boundary = False + if self.current_offset > self.max_angle: + self.current_offset = self.max_angle + self.direction *= -1 + reached_boundary = True + elif self.current_offset < -self.max_angle: + self.current_offset = -self.max_angle + self.direction *= -1 + reached_boundary = True + + # 如果到达边界且启用机器人模式,开始停顿 + if reached_boundary and self.robot_mode: + self.is_paused = True + self.current_pause_time = 0.0 + self.log(f"到达边界 ({self.current_offset}°),开始停顿 {self.pause_duration}秒") + + # 计算最终角度(初始角度 + 偏移量) + final_angle = self.initial_angle + self.current_offset + + # 设置新的旋转(只改变Y轴,保持其他不变) + current_hpr = self.gameObject.getHpr() + self.gameObject.setHpr(final_angle, current_hpr.getY(), current_hpr.getZ()) + + # if not self.is_rotating: + # return + # + # # 获取当前旋转并应用增量 + # current_hpr = self.gameObject.getHpr() + # new_r = current_hpr.getZ() + self.rotation_speed_y * dt + # self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), new_r) + + def on_destroy(self): + """脚本销毁时调用""" + self.log("机器人旋转脚本停止") + + # ==================== 机器人模式控制方法 ==================== + + def set_robot_mode(self, enabled=True): + """ + 启用或禁用机器人模式 + + Args: + enabled: 是否启用机器人模式 + """ + self.robot_mode = enabled + self.log(f"机器人模式: {'开启' if enabled else '关闭'}") + if not enabled: + self.is_paused = False # 如果禁用机器人模式,立即结束停顿 + + def set_pause_duration(self, duration): + """ + 设置停顿时间 + + Args: + duration: 停顿时间(秒) + """ + self.pause_duration = max(0.1, duration) # 最小0.1秒 + self.log(f"停顿时间已设置为: {self.pause_duration}秒") + + def set_max_angle(self, new_max_angle): + """ + 设置新的最大旋转角度 + + Args: + new_max_angle: 新的最大角度值 + """ + self.max_angle = new_max_angle + self.log(f"最大旋转角度已设置为: ±{self.max_angle}度") + if self.initial_angle is not None: + self.log(f"新的旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") + + def set_rotation_speed(self, new_speed): + """ + 设置新的旋转速度 + + Args: + new_speed: 新的旋转速度(度/秒) + """ + self.rotation_speed_y = new_speed + self.log(f"旋转速度已设置为: {self.rotation_speed_y}度/秒") + + def pause_rotation(self): + """暂停旋转""" + self.is_rotating = False + self.log("旋转已暂停") + + def resume_rotation(self): + """恢复旋转""" + self.is_rotating = True + self.is_paused = False # 同时结束停顿状态 + self.log("旋转已恢复") + + def reset_to_initial_angle(self): + """重置到初始角度""" + if self.initial_angle is not None and self.gameObject: + current_hpr = self.gameObject.getHpr() + self.gameObject.setHpr(current_hpr.getX(), self.initial_angle, current_hpr.getZ()) + self.current_offset = 0.0 + self.direction = 1 + self.is_paused = False + self.current_pause_time = 0.0 + self.log(f"已重置到初始角度: {self.initial_angle}度") + + def get_current_info(self): + """获取当前旋转信息""" + if self.gameObject and self.initial_angle is not None: + current_hpr = self.gameObject.getHpr() + current_angle = current_hpr.getY() + self.log("=== 机器人旋转信息 ===") + self.log(f"初始角度: {self.initial_angle}度") + self.log(f"当前角度: {current_angle}度") + self.log(f"偏移量: {self.current_offset}度") + self.log(f"最大角度: ±{self.max_angle}度") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + self.log(f"旋转方向: {'正向' if self.direction > 0 else '反向'}") + self.log(f"旋转状态: {'运行中' if self.is_rotating else '已暂停'}") + self.log(f"机器人模式: {'开启' if self.robot_mode else '关闭'}") + if self.robot_mode: + self.log(f"停顿时间: {self.pause_duration}秒") + self.log(f"当前状态: {'停顿中' if self.is_paused else '旋转中'}") + if self.is_paused: + remaining_time = self.pause_duration - self.current_pause_time + self.log(f"剩余停顿时间: {remaining_time:.1f}秒") + self.log("=== 信息结束 ===") + else: + self.log("无法获取旋转信息") + + # ==================== 预设配置方法 ==================== + + def set_slow_robot_mode(self): + """预设:慢速机器人模式""" + self.set_rotation_speed(15.0) + self.set_pause_duration(1.0) + self.set_robot_mode(True) + self.log("已设置为慢速机器人模式") + + def set_fast_robot_mode(self): + """预设:快速机器人模式""" + self.set_rotation_speed(45.0) + self.set_pause_duration(0.3) + self.set_robot_mode(True) + self.log("已设置为快速机器人模式") + + def set_smooth_mode(self): + """预设:平滑模式(非机器人)""" + self.set_robot_mode(False) + self.set_rotation_speed(30.0) + self.log("已设置为平滑旋转模式") \ No newline at end of file diff --git a/111/新项目/scripts/Rotate_P_Script.py b/111/新项目/scripts/Rotate_P_Script.py new file mode 100644 index 00000000..98d02cdf --- /dev/null +++ b/111/新项目/scripts/Rotate_P_Script.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +旋转脚本 - 让对象持续旋转 +""" + +from core.script_system import ScriptBase + + +class RotatorScript(ScriptBase): + """旋转脚本类""" + + def __init__(self): + super().__init__() + self.rotation_speed_y = 30.0 # Y轴旋转速度 (度/秒) + self.max_angle = 25.0 # 最大旋转角度(相对于初始角度) + self.direction = 1 + self.current_offset = 0.0 # 当前相对于初始角度的偏移 + self.initial_angle = None # 模型的初始角度 + self.is_rotating = True # 是否正在旋转 + + # 机器人式停顿参数 + self.pause_duration = 0.5 # 停顿时间(秒) + self.current_pause_time = 0.0 # 当前停顿计时 + self.is_paused = False # 是否正在停顿 + self.robot_mode = True # 是否启用机器人模式 + + def start(self): + """脚本开始时调用""" + self.log("机器人旋转脚本启动!") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + self.log(f"最大旋转角度: ±{self.max_angle}度") + self.log(f"机器人模式: {'开启' if self.robot_mode else '关闭'}") + if self.robot_mode: + self.log(f"停顿时间: {self.pause_duration}秒") + + # 记录模型的初始角度 + if self.gameObject: + initial_hpr = self.gameObject.getHpr() + self.initial_angle = initial_hpr.getY() # 记录Y轴的初始角度(Pitch) + self.log(f"模型初始角度: {self.initial_angle}度") + self.log(f"旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") + else: + self.log("⚠️ 无法获取游戏对象,使用默认初始角度0") + self.initial_angle = 0.0 + + def update(self, dt): + """每帧更新 - 机器人式旋转""" + if not self.is_rotating or self.initial_angle is None: + return + + # 如果正在停顿中 + if self.is_paused: + self.current_pause_time += dt + if self.current_pause_time >= self.pause_duration: + # 停顿结束,继续旋转 + self.is_paused = False + self.current_pause_time = 0.0 + self.log(f"停顿结束,继续旋转,方向: {'正向' if self.direction > 0 else '反向'}") + return + + # 计算角度变化量 + delta_angle = self.rotation_speed_y * dt * self.direction + self.current_offset += delta_angle + + # 检查是否到达边界 + reached_boundary = False + if self.current_offset > self.max_angle: + self.current_offset = self.max_angle + self.direction *= -1 + reached_boundary = True + elif self.current_offset < -self.max_angle: + self.current_offset = -self.max_angle + self.direction *= -1 + reached_boundary = True + + # 如果到达边界且启用机器人模式,开始停顿 + if reached_boundary and self.robot_mode: + self.is_paused = True + self.current_pause_time = 0.0 + self.log(f"到达边界 ({self.current_offset}°),开始停顿 {self.pause_duration}秒") + + # 计算最终角度(初始角度 + 偏移量) + final_angle = self.initial_angle + self.current_offset + + # 设置新的旋转(只改变Y轴,保持其他不变) + current_hpr = self.gameObject.getHpr() + self.gameObject.setHpr(current_hpr.getX(), final_angle, current_hpr.getZ()) + + # if not self.is_rotating: + # return + # + # # 获取当前旋转并应用增量 + # current_hpr = self.gameObject.getHpr() + # new_r = current_hpr.getZ() + self.rotation_speed_y * dt + # self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), new_r) + + def on_destroy(self): + """脚本销毁时调用""" + self.log("机器人旋转脚本停止") + + # ==================== 机器人模式控制方法 ==================== + + def set_robot_mode(self, enabled=True): + """ + 启用或禁用机器人模式 + + Args: + enabled: 是否启用机器人模式 + """ + self.robot_mode = enabled + self.log(f"机器人模式: {'开启' if enabled else '关闭'}") + if not enabled: + self.is_paused = False # 如果禁用机器人模式,立即结束停顿 + + def set_pause_duration(self, duration): + """ + 设置停顿时间 + + Args: + duration: 停顿时间(秒) + """ + self.pause_duration = max(0.1, duration) # 最小0.1秒 + self.log(f"停顿时间已设置为: {self.pause_duration}秒") + + def set_max_angle(self, new_max_angle): + """ + 设置新的最大旋转角度 + + Args: + new_max_angle: 新的最大角度值 + """ + self.max_angle = new_max_angle + self.log(f"最大旋转角度已设置为: ±{self.max_angle}度") + if self.initial_angle is not None: + self.log(f"新的旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") + + def set_rotation_speed(self, new_speed): + """ + 设置新的旋转速度 + + Args: + new_speed: 新的旋转速度(度/秒) + """ + self.rotation_speed_y = new_speed + self.log(f"旋转速度已设置为: {self.rotation_speed_y}度/秒") + + def pause_rotation(self): + """暂停旋转""" + self.is_rotating = False + self.log("旋转已暂停") + + def resume_rotation(self): + """恢复旋转""" + self.is_rotating = True + self.is_paused = False # 同时结束停顿状态 + self.log("旋转已恢复") + + def reset_to_initial_angle(self): + """重置到初始角度""" + if self.initial_angle is not None and self.gameObject: + current_hpr = self.gameObject.getHpr() + self.gameObject.setHpr(current_hpr.getX(), self.initial_angle, current_hpr.getZ()) + self.current_offset = 0.0 + self.direction = 1 + self.is_paused = False + self.current_pause_time = 0.0 + self.log(f"已重置到初始角度: {self.initial_angle}度") + + def get_current_info(self): + """获取当前旋转信息""" + if self.gameObject and self.initial_angle is not None: + current_hpr = self.gameObject.getHpr() + current_angle = current_hpr.getY() + self.log("=== 机器人旋转信息 ===") + self.log(f"初始角度: {self.initial_angle}度") + self.log(f"当前角度: {current_angle}度") + self.log(f"偏移量: {self.current_offset}度") + self.log(f"最大角度: ±{self.max_angle}度") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + self.log(f"旋转方向: {'正向' if self.direction > 0 else '反向'}") + self.log(f"旋转状态: {'运行中' if self.is_rotating else '已暂停'}") + self.log(f"机器人模式: {'开启' if self.robot_mode else '关闭'}") + if self.robot_mode: + self.log(f"停顿时间: {self.pause_duration}秒") + self.log(f"当前状态: {'停顿中' if self.is_paused else '旋转中'}") + if self.is_paused: + remaining_time = self.pause_duration - self.current_pause_time + self.log(f"剩余停顿时间: {remaining_time:.1f}秒") + self.log("=== 信息结束 ===") + else: + self.log("无法获取旋转信息") + + # ==================== 预设配置方法 ==================== + + def set_slow_robot_mode(self): + """预设:慢速机器人模式""" + self.set_rotation_speed(15.0) + self.set_pause_duration(1.0) + self.set_robot_mode(True) + self.log("已设置为慢速机器人模式") + + def set_fast_robot_mode(self): + """预设:快速机器人模式""" + self.set_rotation_speed(45.0) + self.set_pause_duration(0.3) + self.set_robot_mode(True) + self.log("已设置为快速机器人模式") + + def set_smooth_mode(self): + """预设:平滑模式(非机器人)""" + self.set_robot_mode(False) + self.set_rotation_speed(30.0) + self.log("已设置为平滑旋转模式") \ No newline at end of file diff --git a/111/新项目/scripts/Rotate_R_Script.py b/111/新项目/scripts/Rotate_R_Script.py new file mode 100644 index 00000000..4d22edac --- /dev/null +++ b/111/新项目/scripts/Rotate_R_Script.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +旋转脚本 - 让对象持续旋转 +""" + +from core.script_system import ScriptBase + + +class RotatorScript(ScriptBase): + """旋转脚本类""" + + def __init__(self): + super().__init__() + self.rotation_speed_y = 30.0 # Y轴旋转速度 (度/秒) + self.max_angle = 15.0 + self.direction = 1 + self.current_angle = 0.0 + self.is_rotating = True # 是否正在旋转 + + def start(self): + """脚本开始时调用""" + self.log("旋转脚本启动!") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + + def update(self, dt): + """每帧更新""" + + delta_angle = self.rotation_speed_y * dt * self.direction + self.current_angle += delta_angle + + # 如果超出角度范围,则反向 + if self.current_angle > self.max_angle: + self.current_angle = self.max_angle + self.direction *= -1 + elif self.current_angle < -self.max_angle: + self.current_angle = -self.max_angle + self.direction *= -1 + + # 设置新的旋转(只改变Z轴,保持其他不变) + base_hpr = self.gameObject.getHpr() + new_r = self.current_angle + self.gameObject.setHpr(base_hpr.getX(), base_hpr.getY(), new_r) + + # if not self.is_rotating: + # return + # + # # 获取当前旋转并应用增量 + # current_hpr = self.gameObject.getHpr() + # new_r = current_hpr.getZ() + self.rotation_speed_y * dt + # self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), new_r) + + def on_destroy(self): + """脚本销毁时调用""" + self.log("旋转脚本停止") \ No newline at end of file diff --git a/111/新项目/scripts/RotatorScript.py b/111/新项目/scripts/RotatorScript.py new file mode 100644 index 00000000..bdc49495 --- /dev/null +++ b/111/新项目/scripts/RotatorScript.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +旋转脚本 - 让对象持续旋转 +""" + +from core.script_system import ScriptBase + +class RotatorScript(ScriptBase): + """旋转脚本类""" + + def __init__(self): + super().__init__() + self.rotation_speed_y = 30.0 # Y轴旋转速度 (度/秒) + self.is_rotating = True # 是否正在旋转 + + def start(self): + """脚本开始时调用""" + self.log("旋转脚本启动!") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + + def update(self, dt): + # 检查 gameObject 是否存在且不为空 + if not self.gameObject or self.gameObject.isEmpty(): + print("RotatorScript: gameObject is empty or None, skipping update") + return + """每帧更新""" + if not self.is_rotating: + return + + # 获取当前旋转并应用增量 + current_hpr = self.gameObject.getHpr() + new_h = current_hpr.getX() + self.rotation_speed_y * dt + self.gameObject.setHpr(new_h, current_hpr.getY(), current_hpr.getZ()) + + def on_destroy(self): + """脚本销毁时调用""" + self.log("旋转脚本停止") \ No newline at end of file diff --git a/111/新项目/scripts/ScalerScript.py b/111/新项目/scripts/ScalerScript.py new file mode 100644 index 00000000..406d65fa --- /dev/null +++ b/111/新项目/scripts/ScalerScript.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +缩放脚本 - 让对象产生呼吸般的缩放效果 +""" + +from core.script_system import ScriptBase +import math + +class ScalerScript(ScriptBase): + """缩放脚本类""" + + def __init__(self): + super().__init__() + + # 缩放参数 + self.base_scale = 1.0 # 基础缩放 + self.scale_amplitude = 0.3 # 缩放幅度 + self.scale_speed = 2.0 # 缩放速度 (周期/秒) + self.uniform_scale = True # 是否统一缩放(所有轴) + + # 内部变量 + self.time_accumulator = 0.0 # 时间累积器 + self.original_scale = None # 原始缩放 + self.is_scaling = True # 是否正在缩放 + + def start(self): + """脚本开始时调用""" + self.log("缩放脚本启动!") + self.log(f"缩放参数: 基础={self.base_scale}, 幅度={self.scale_amplitude}, 速度={self.scale_speed}") + + # 记录原始缩放 + self.original_scale = self.gameObject.getScale() + self.log(f"原始缩放: {self.original_scale}") + + def update(self, dt): + """每帧更新""" + if not self.is_scaling: + return + + # 累积时间 + self.time_accumulator += dt + + # 计算正弦波缩放值 + sine_value = math.sin(self.time_accumulator * self.scale_speed * 2 * math.pi) + scale_factor = self.base_scale + (self.scale_amplitude * sine_value) + + # 应用缩放 + if self.uniform_scale: + # 统一缩放 + self.gameObject.setScale(scale_factor) + else: + # 非统一缩放(仅Z轴) + current_scale = self.gameObject.getScale() + self.gameObject.setScale(current_scale.getX(), current_scale.getY(), scale_factor) + + def set_scale_parameters(self, base=None, amplitude=None, speed=None, uniform=None): + """设置缩放参数""" + if base is not None: + self.base_scale = base + if amplitude is not None: + self.scale_amplitude = amplitude + if speed is not None: + self.scale_speed = speed + if uniform is not None: + self.uniform_scale = uniform + + self.log(f"缩放参数更新: 基础={self.base_scale}, 幅度={self.scale_amplitude}, 速度={self.scale_speed}") + + def toggle_scaling(self): + """切换缩放状态""" + self.is_scaling = not self.is_scaling + status = "恢复" if self.is_scaling else "暂停" + self.log(f"缩放{status}") + + def reset_scale(self): + """重置到原始缩放""" + if self.original_scale: + self.gameObject.setScale(self.original_scale) + self.time_accumulator = 0.0 + self.log("缩放已重置到原始值") + + def pulse_once(self): + """执行一次脉冲缩放""" + self.time_accumulator = 0.0 + self.log("执行脉冲缩放") + + def on_destroy(self): + """脚本销毁时调用""" + self.log("缩放脚本停止") \ No newline at end of file diff --git a/111/新项目/scripts/TestMover.py b/111/新项目/scripts/TestMover.py new file mode 100644 index 00000000..4116b1e3 --- /dev/null +++ b/111/新项目/scripts/TestMover.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +TestMover - 移动脚本 +""" + +from core.script_system import ScriptBase + +class Testmover(ScriptBase): + """移动脚本类""" + + def __init__(self): + super().__init__() + self.speed = 5.0 # 移动速度 + self.direction = [1, 0, 0] # 移动方向 + + def start(self): + """脚本开始时调用""" + self.log("移动脚本开始运行!") + + def update(self, dt): + """每帧更新""" + if self.transform: + # 计算移动偏移 + offset_x = self.direction[0] * self.speed * dt + offset_y = self.direction[1] * self.speed * dt + offset_z = self.direction[2] * self.speed * dt + + # 更新位置 + current_pos = self.transform.getPos() + new_pos = ( + current_pos.x + offset_x, + current_pos.y + offset_y, + current_pos.z + offset_z + ) + self.transform.setPos(*new_pos) + + def on_destroy(self): + """脚本销毁时调用""" + self.log("移动脚本被销毁") diff --git a/111/新项目/scripts/TestRotator.py b/111/新项目/scripts/TestRotator.py new file mode 100644 index 00000000..83042e31 --- /dev/null +++ b/111/新项目/scripts/TestRotator.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +TestRotator - 自定义脚本 +""" + +from core.script_system import ScriptBase + +class Testrotator(ScriptBase): + """自定义脚本类""" + + def __init__(self): + super().__init__() + # 在这里初始化您的变量 + + def start(self): + """脚本开始时调用""" + self.log("脚本开始运行!") + + def update(self, dt): + """每帧更新""" + # 在这里编写更新逻辑 + pass + + def on_destroy(self): + """脚本销毁时调用""" + self.log("脚本被销毁") diff --git a/111/新项目/scripts/TestScaler.py b/111/新项目/scripts/TestScaler.py new file mode 100644 index 00000000..536883d7 --- /dev/null +++ b/111/新项目/scripts/TestScaler.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +TestScaler - 自定义脚本 +""" + +from core.script_system import ScriptBase + +class Testscaler(ScriptBase): + """自定义脚本类""" + + def __init__(self): + super().__init__() + # 在这里初始化您的变量 + + def start(self): + """脚本开始时调用""" + self.log("脚本开始运行!") + + def update(self, dt): + """每帧更新""" + # 在这里编写更新逻辑 + pass + + def on_destroy(self): + """脚本销毁时调用""" + self.log("脚本被销毁") diff --git a/111/新项目/scripts/a.py b/111/新项目/scripts/a.py new file mode 100644 index 00000000..34bd5408 --- /dev/null +++ b/111/新项目/scripts/a.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +a - 自定义脚本 +""" + +from core.script_system import ScriptBase + +class A(ScriptBase): + """自定义脚本类""" + + def __init__(self): + super().__init__() + # 在这里初始化您的变量 + + def start(self): + """脚本开始时调用""" + self.log("脚本开始运行!") + + def update(self, dt): + """每帧更新""" + # 在这里编写更新逻辑 + pass + + def on_destroy(self): + """脚本销毁时调用""" + self.log("脚本被销毁") diff --git a/111/新项目/scripts/example_script.py b/111/新项目/scripts/example_script.py new file mode 100644 index 00000000..ecba0da9 --- /dev/null +++ b/111/新项目/scripts/example_script.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +示例脚本 - 演示如何编写脚本 +""" + +from core.script_system import ScriptBase + +class ExampleScript(ScriptBase): + """示例脚本类""" + + def __init__(self): + super().__init__() + self.counter = 0 + self.rotation_speed = 30.0 # 度/秒 + + def start(self): + """脚本开始时调用""" + self.log("示例脚本开始运行!") + self.log(f"挂载到对象: {self.gameObject.getName()}") + + def update(self, dt): + """每帧更新""" + self.counter += 1 + + # 每60帧输出一次信息 + if self.counter % 60 == 0: + self.log(f"运行了 {self.counter} 帧") + + # 让对象旋转 + if self.transform: + current_h = self.transform.getH() + new_h = current_h + self.rotation_speed * dt + self.transform.setH(new_h) + + def on_destroy(self): + """脚本销毁时调用""" + self.log("示例脚本被销毁") + + def on_enable(self): + """脚本启用时调用""" + self.log("示例脚本被启用") + + def on_disable(self): + """脚本禁用时调用""" + self.log("示例脚本被禁用") diff --git a/111/新项目/scripts/test_quick_script.py b/111/新项目/scripts/test_quick_script.py new file mode 100644 index 00000000..716070a3 --- /dev/null +++ b/111/新项目/scripts/test_quick_script.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +test_quick_script - 自定义脚本 +""" + +from core.script_system import ScriptBase + +class TestQuickScript(ScriptBase): + """自定义脚本类""" + + def __init__(self): + super().__init__() + # 在这里初始化您的变量 + + def start(self): + """脚本开始时调用""" + self.log("脚本开始运行!") + + def update(self, dt): + """每帧更新""" + # 在这里编写更新逻辑 + pass + + def on_destroy(self): + """脚本销毁时调用""" + self.log("脚本被销毁") diff --git a/111/新项目/新项目.png b/111/新项目/新项目.png new file mode 100644 index 00000000..5781b463 Binary files /dev/null and b/111/新项目/新项目.png differ diff --git a/Resources/models/jyc.glb b/Resources/models/jyc.glb new file mode 100644 index 00000000..9326300b Binary files /dev/null and b/Resources/models/jyc.glb differ diff --git a/build/.111_staging/assets/gui/gui_elements.json b/build/.111_staging/assets/gui/gui_elements.json new file mode 100644 index 00000000..0637a088 --- /dev/null +++ b/build/.111_staging/assets/gui/gui_elements.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/build/.111_staging/assets/scene.bam b/build/.111_staging/assets/scene.bam new file mode 100644 index 00000000..a8cccf47 Binary files /dev/null and b/build/.111_staging/assets/scene.bam differ diff --git a/build/.111_staging/assets/scripts/BouncerScript.py b/build/.111_staging/assets/scripts/BouncerScript.py new file mode 100644 index 00000000..60fb4170 --- /dev/null +++ b/build/.111_staging/assets/scripts/BouncerScript.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +跳跃脚本 - 让对象产生上下跳跃效果 +""" + +from core.script_system import ScriptBase +import math + +class BouncerScript(ScriptBase): + """跳跃脚本类""" + + def __init__(self): + super().__init__() + + # 跳跃参数 + self.jump_height = 2.0 # 跳跃高度 + self.jump_speed = 3.0 # 跳跃速度 (跳跃/秒) + self.bounce_type = "sine" # 跳跃类型: "sine", "abs_sine", "square" + + # 内部变量 + self.time_accumulator = 0.0 # 时间累积器 + self.original_y = None # 原始Y位置 + self.is_bouncing = True # 是否正在跳跃 + self.bounce_direction = 1 # 跳跃方向 + + def start(self): + """脚本开始时调用""" + self.log("跳跃脚本启动!") + self.log(f"跳跃参数: 高度={self.jump_height}, 速度={self.jump_speed}, 类型={self.bounce_type}") + + # 记录原始Y位置 + self.original_y = self.gameObject.getZ() # Z轴是高度 + self.log(f"原始高度: {self.original_y}") + + def update(self, dt): + """每帧更新""" + if not self.is_bouncing: + return + + # 累积时间 + self.time_accumulator += dt * self.bounce_direction + + # 根据类型计算跳跃高度 + if self.bounce_type == "sine": + # 标准正弦波跳跃 + height_offset = math.sin(self.time_accumulator * self.jump_speed * 2 * math.pi) * self.jump_height + elif self.bounce_type == "abs_sine": + # 绝对值正弦波(始终向上) + height_offset = abs(math.sin(self.time_accumulator * self.jump_speed * 2 * math.pi)) * self.jump_height + elif self.bounce_type == "square": + # 方波跳跃(突然跳起落下) + sine_val = math.sin(self.time_accumulator * self.jump_speed * 2 * math.pi) + height_offset = self.jump_height if sine_val > 0 else 0 + else: + height_offset = 0 + + # 应用跳跃 + current_pos = self.gameObject.getPos() + new_z = self.original_y + height_offset + self.gameObject.setPos(current_pos.getX(), current_pos.getY(), new_z) + + def set_bounce_parameters(self, height=None, speed=None, bounce_type=None): + """设置跳跃参数""" + if height is not None: + self.jump_height = height + if speed is not None: + self.jump_speed = speed + if bounce_type is not None and bounce_type in ["sine", "abs_sine", "square"]: + self.bounce_type = bounce_type + + self.log(f"跳跃参数更新: 高度={self.jump_height}, 速度={self.jump_speed}, 类型={self.bounce_type}") + + def toggle_bouncing(self): + """切换跳跃状态""" + self.is_bouncing = not self.is_bouncing + status = "恢复" if self.is_bouncing else "暂停" + self.log(f"跳跃{status}") + + def reverse_direction(self): + """反转跳跃方向""" + self.bounce_direction *= -1 + direction = "正向" if self.bounce_direction > 0 else "反向" + self.log(f"跳跃方向改为{direction}") + + def reset_position(self): + """重置到原始高度""" + if self.original_y is not None: + current_pos = self.gameObject.getPos() + self.gameObject.setPos(current_pos.getX(), current_pos.getY(), self.original_y) + self.time_accumulator = 0.0 + self.log("位置已重置到原始高度") + + def jump_once(self): + """执行一次跳跃""" + self.time_accumulator = 0.0 + self.log("执行单次跳跃") + + def on_destroy(self): + """脚本销毁时调用""" + self.log("跳跃脚本停止") \ No newline at end of file diff --git a/build/.111_staging/assets/scripts/ColorChangerScript.py b/build/.111_staging/assets/scripts/ColorChangerScript.py new file mode 100644 index 00000000..45dd59cb --- /dev/null +++ b/build/.111_staging/assets/scripts/ColorChangerScript.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +颜色变化脚本 - 让对象颜色产生循环变化 +""" + +from core.script_system import ScriptBase +from panda3d.core import Vec4 +import math + +class ColorChangerScript(ScriptBase): + """颜色变化脚本类""" + + def __init__(self): + super().__init__() + + # 颜色参数 + self.color_speed = 1.0 # 颜色变化速度 (周期/秒) + self.color_mode = "rainbow" # 颜色模式: "rainbow", "pulse", "fade", "strobe" + self.base_color = Vec4(1, 1, 1, 1) # 基础颜色 + self.intensity = 1.0 # 颜色强度 + + # 内部变量 + self.time_accumulator = 0.0 # 时间累积器 + self.original_color = None # 原始颜色 + self.is_changing = True # 是否正在变化 + self.strobe_state = False # 闪烁状态 + + def start(self): + """脚本开始时调用""" + self.log("颜色变化脚本启动!") + self.log(f"颜色参数: 速度={self.color_speed}, 模式={self.color_mode}, 强度={self.intensity}") + + # 记录原始颜色 + self.original_color = self.gameObject.getColor() + self.log(f"原始颜色: {self.original_color}") + + def update(self, dt): + """每帧更新""" + if not self.is_changing: + return + + # 累积时间 + self.time_accumulator += dt + + # 根据模式计算新颜色 + if self.color_mode == "rainbow": + new_color = self._calculate_rainbow_color() + elif self.color_mode == "pulse": + new_color = self._calculate_pulse_color() + elif self.color_mode == "fade": + new_color = self._calculate_fade_color() + elif self.color_mode == "strobe": + new_color = self._calculate_strobe_color() + else: + new_color = self.base_color + + # 应用颜色 + self.gameObject.setColor(new_color) + + def _calculate_rainbow_color(self): + """计算彩虹颜色""" + # 使用HSV到RGB的转换创建彩虹效果 + hue = (self.time_accumulator * self.color_speed) % 1.0 + + # 简单的HSV到RGB转换 + i = int(hue * 6.0) + f = (hue * 6.0) - i + p = 0.0 + q = 1.0 - f + t = f + + if i % 6 == 0: + r, g, b = 1.0, t, p + elif i % 6 == 1: + r, g, b = q, 1.0, p + elif i % 6 == 2: + r, g, b = p, 1.0, t + elif i % 6 == 3: + r, g, b = p, q, 1.0 + elif i % 6 == 4: + r, g, b = t, p, 1.0 + else: + r, g, b = 1.0, p, q + + return Vec4(r * self.intensity, g * self.intensity, b * self.intensity, 1.0) + + def _calculate_pulse_color(self): + """计算脉冲颜色""" + pulse = (math.sin(self.time_accumulator * self.color_speed * 2 * math.pi) + 1.0) / 2.0 + multiplier = pulse * self.intensity + return Vec4( + self.base_color.getX() * multiplier, + self.base_color.getY() * multiplier, + self.base_color.getZ() * multiplier, + self.base_color.getW() + ) + + def _calculate_fade_color(self): + """计算淡入淡出颜色""" + fade = (math.sin(self.time_accumulator * self.color_speed * 2 * math.pi) + 1.0) / 2.0 + alpha = fade * self.intensity + return Vec4( + self.base_color.getX(), + self.base_color.getY(), + self.base_color.getZ(), + alpha + ) + + def _calculate_strobe_color(self): + """计算闪烁颜色""" + # 根据时间间隔切换状态 + interval = 1.0 / (self.color_speed * 2) # 闪烁间隔 + if int(self.time_accumulator / interval) % 2 == 0: + return Vec4( + self.base_color.getX() * self.intensity, + self.base_color.getY() * self.intensity, + self.base_color.getZ() * self.intensity, + self.base_color.getW() + ) + else: + return Vec4(0.1, 0.1, 0.1, self.base_color.getW()) # 暗色状态 + + def set_color_parameters(self, speed=None, mode=None, base_color=None, intensity=None): + """设置颜色参数""" + if speed is not None: + self.color_speed = speed + if mode is not None and mode in ["rainbow", "pulse", "fade", "strobe"]: + self.color_mode = mode + if base_color is not None: + self.base_color = base_color + if intensity is not None: + self.intensity = intensity + + self.log(f"颜色参数更新: 速度={self.color_speed}, 模式={self.color_mode}, 强度={self.intensity}") + + def toggle_color_change(self): + """切换颜色变化状态""" + self.is_changing = not self.is_changing + status = "恢复" if self.is_changing else "暂停" + self.log(f"颜色变化{status}") + + def reset_color(self): + """重置到原始颜色""" + if self.original_color: + self.gameObject.setColor(self.original_color) + self.time_accumulator = 0.0 + self.log("颜色已重置到原始值") + + def set_solid_color(self, r=1.0, g=1.0, b=1.0, a=1.0): + """设置固定颜色""" + color = Vec4(r, g, b, a) + self.gameObject.setColor(color) + self.base_color = color + self.log(f"设置固定颜色: {color}") + + def on_destroy(self): + """脚本销毁时调用""" + self.log("颜色变化脚本停止") \ No newline at end of file diff --git a/build/.111_staging/assets/scripts/ComboAnimatorScript.py b/build/.111_staging/assets/scripts/ComboAnimatorScript.py new file mode 100644 index 00000000..a39c2a78 --- /dev/null +++ b/build/.111_staging/assets/scripts/ComboAnimatorScript.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +复合动画脚本 - 结合旋转和跳跃效果 +""" + +from core.script_system import ScriptBase +import math + +class ComboAnimatorScript(ScriptBase): + def __init__(self): + super().__init__() + self.time = 0.0 + self.original_pos = None + self.is_active = True + + def start(self): + self.log("复合动画脚本启动!") + self.original_pos = self.gameObject.getPos() + + def update(self, dt): + if not self.is_active: + return + + self.time += dt + + # 旋转效果 + current_hpr = self.gameObject.getHpr() + new_h = current_hpr.getX() + 45.0 * dt + self.gameObject.setHpr(new_h, current_hpr.getY(), current_hpr.getZ()) + + # 跳跃效果 + if self.original_pos: + bounce_offset = abs(math.sin(self.time * 3.0)) * 1.0 + self.gameObject.setZ(self.original_pos.getZ() + bounce_offset) + + def on_destroy(self): + self.log("复合动画脚本停止") \ No newline at end of file diff --git a/build/.111_staging/assets/scripts/FollowerScript.py b/build/.111_staging/assets/scripts/FollowerScript.py new file mode 100644 index 00000000..893406a2 --- /dev/null +++ b/build/.111_staging/assets/scripts/FollowerScript.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +跟随脚本 - 让对象跟随指定的目标对象 +""" + +from core.script_system import ScriptBase +from panda3d.core import Vec3 + +class FollowerScript(ScriptBase): + """跟随脚本类""" + + def __init__(self): + super().__init__() + self.target = None # 跟随目标 + self.follow_speed = 5.0 # 跟随速度 + self.follow_distance = 2.0 # 跟随距离 + self.is_following = True # 是否正在跟随 + + def start(self): + """脚本开始时调用""" + self.log("跟随脚本启动!") + self.log(f"跟随参数: 速度={self.follow_speed}, 距离={self.follow_distance}") + + def update(self, dt): + """每帧更新""" + if not self.is_following or self.target is None: + return + + target_pos = self.target.getPos() + current_pos = self.gameObject.getPos() + + # 计算目标方向 + direction = target_pos - current_pos + distance = direction.length() + + # 如果距离大于跟随距离,则移动 + if distance > self.follow_distance: + if distance > 0: + direction.normalize() + + # 计算目标位置(保持跟随距离) + target_follow_pos = target_pos - direction * self.follow_distance + + # 平滑移动到目标位置 + move_direction = target_follow_pos - current_pos + move_distance = move_direction.length() + + if move_distance > 0: + move_direction.normalize() + move_amount = min(self.follow_speed * dt, move_distance) + new_pos = current_pos + move_direction * move_amount + self.gameObject.setPos(new_pos) + + # 朝向目标 + self.gameObject.lookAt(target_pos) + + def set_target(self, target): + """设置跟随目标""" + self.target = target + if target: + self.log(f"设置跟随目标: {target.getName()}") + else: + self.log("清除跟随目标") + + def on_destroy(self): + """脚本销毁时调用""" + self.log("跟随脚本停止") \ No newline at end of file diff --git a/build/.111_staging/assets/scripts/MoverScript.py b/build/.111_staging/assets/scripts/MoverScript.py new file mode 100644 index 00000000..b5c24099 --- /dev/null +++ b/build/.111_staging/assets/scripts/MoverScript.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +移动脚本 - 让对象在指定方向上来回移动 +""" + +from core.script_system import ScriptBase +import math + +class MoverScript(ScriptBase): + """移动脚本类""" + + def __init__(self): + super().__init__() + + # 移动参数 + self.move_distance = 5.0 # 移动距离 + self.move_speed = 2.0 # 移动速度 (单位/秒) + self.move_axis = "x" # 移动轴: "x", "y", "z" + + # 内部变量 + self.start_position = None # 起始位置 + self.current_direction = 1 # 当前移动方向: 1或-1 + self.current_distance = 0.0 # 当前移动距离 + self.is_moving = True # 是否正在移动 + + def start(self): + """脚本开始时调用""" + self.log("移动脚本启动!") + self.log(f"移动参数: 距离={self.move_distance}, 速度={self.move_speed}, 轴={self.move_axis}") + + # 记录起始位置 + self.start_position = self.gameObject.getPos() + self.log(f"起始位置: {self.start_position}") + + def update(self, dt): + """每帧更新""" + if not self.is_moving or self.start_position is None: + return + + # 计算移动增量 + move_delta = self.move_speed * dt * self.current_direction + self.current_distance += abs(move_delta) + + # 检查是否需要改变方向 + if self.current_distance >= self.move_distance: + self.current_direction *= -1 + self.current_distance = 0.0 + + # 应用移动 + current_pos = self.gameObject.getPos() + new_pos = [current_pos.getX(), current_pos.getY(), current_pos.getZ()] + + if self.move_axis == "x": + new_pos[0] += move_delta + elif self.move_axis == "y": + new_pos[1] += move_delta + elif self.move_axis == "z": + new_pos[2] += move_delta + + self.gameObject.setPos(new_pos[0], new_pos[1], new_pos[2]) + + def set_move_parameters(self, distance=None, speed=None, axis=None): + """设置移动参数""" + if distance is not None: + self.move_distance = distance + if speed is not None: + self.move_speed = speed + if axis is not None and axis in ["x", "y", "z"]: + self.move_axis = axis + + self.log(f"移动参数更新: 距离={self.move_distance}, 速度={self.move_speed}, 轴={self.move_axis}") + + def toggle_movement(self): + """切换移动状态""" + self.is_moving = not self.is_moving + status = "恢复" if self.is_moving else "暂停" + self.log(f"移动{status}") + + def reset_position(self): + """重置到起始位置""" + if self.start_position: + self.gameObject.setPos(self.start_position) + self.current_distance = 0.0 + self.current_direction = 1 + self.log("位置已重置到起始点") + + def on_destroy(self): + """脚本销毁时调用""" + self.log("移动脚本停止") \ No newline at end of file diff --git a/build/.111_staging/assets/scripts/R_P.py b/build/.111_staging/assets/scripts/R_P.py new file mode 100644 index 00000000..ba6ae6b9 --- /dev/null +++ b/build/.111_staging/assets/scripts/R_P.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +旋转脚本 - 让对象持续旋转 +""" + +from core.script_system import ScriptBase + + +class RotatorScript(ScriptBase): + """旋转脚本类""" + + def __init__(self): + super().__init__() + self.rotation_speed_y = 30.0 # Y轴旋转速度 (度/秒) + self.max_angle = 30.0 # 最大旋转角度(相对于初始角度) + self.direction = 1 + self.current_offset = 0.0 # 当前相对于初始角度的偏移 + self.initial_angle = None # 模型的初始角度 + self.is_rotating = True # 是否正在旋转 + + def start(self): + """脚本开始时调用""" + self.log("旋转脚本启动!") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + self.log(f"最大旋转角度: ±{self.max_angle}度") + + # 记录模型的初始角度 + if self.gameObject: + initial_hpr = self.gameObject.getHpr() + self.initial_angle = initial_hpr.getZ() # 记录Z轴的初始角度 + self.log(f"模型初始角度: {self.initial_angle}度") + self.log(f"旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") + else: + self.log("⚠️ 无法获取游戏对象,使用默认初始角度0") + self.initial_angle = 0.0 + + def update(self, dt): + """每帧更新""" + if not self.is_rotating or self.initial_angle is None: + return + + # 计算角度变化量 + delta_angle = self.rotation_speed_y * dt * self.direction + self.current_offset += delta_angle + + # 如果超出角度范围,则反向并限制在边界 + if self.current_offset > self.max_angle: + self.current_offset = self.max_angle + self.direction *= -1 + elif self.current_offset < -self.max_angle: + self.current_offset = -self.max_angle + self.direction *= -1 + + # 计算最终角度(初始角度 + 偏移量) + final_angle = self.initial_angle + self.current_offset + + # 设置新的旋转(只改变Z轴,保持其他不变) + current_hpr = self.gameObject.getHpr() + self.gameObject.setHpr(current_hpr.getX(), final_angle, current_hpr.getZ()) + + # if not self.is_rotating: + # return + # + # # 获取当前旋转并应用增量 + # current_hpr = self.gameObject.getHpr() + # new_r = current_hpr.getZ() + self.rotation_speed_y * dt + # self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), new_r) + + def on_destroy(self): + """脚本销毁时调用""" + self.log("旋转脚本停止") + + # ==================== 控制方法 ==================== + + def set_max_angle(self, new_max_angle): + """ + 设置新的最大旋转角度 + + Args: + new_max_angle: 新的最大角度值 + """ + self.max_angle = new_max_angle + self.log(f"最大旋转角度已设置为: ±{self.max_angle}度") + if self.initial_angle is not None: + self.log(f"新的旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") + + def set_rotation_speed(self, new_speed): + """ + 设置新的旋转速度 + + Args: + new_speed: 新的旋转速度(度/秒) + """ + self.rotation_speed_y = new_speed + self.log(f"旋转速度已设置为: {self.rotation_speed_y}度/秒") + + def pause_rotation(self): + """暂停旋转""" + self.is_rotating = False + self.log("旋转已暂停") + + def resume_rotation(self): + """恢复旋转""" + self.is_rotating = True + self.log("旋转已恢复") + + def reset_to_initial_angle(self): + """重置到初始角度""" + if self.initial_angle is not None and self.gameObject: + current_hpr = self.gameObject.getHpr() + self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), self.initial_angle) + self.current_offset = 0.0 + self.direction = 1 + self.log(f"已重置到初始角度: {self.initial_angle}度") + + def get_current_info(self): + """获取当前旋转信息""" + if self.gameObject and self.initial_angle is not None: + current_hpr = self.gameObject.getHpr() + current_angle = current_hpr.getZ() + self.log("=== 当前旋转信息 ===") + self.log(f"初始角度: {self.initial_angle}度") + self.log(f"当前角度: {current_angle}度") + self.log(f"偏移量: {self.current_offset}度") + self.log(f"最大角度: ±{self.max_angle}度") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + self.log(f"旋转方向: {'正向' if self.direction > 0 else '反向'}") + self.log(f"旋转状态: {'运行中' if self.is_rotating else '已暂停'}") + self.log("=== 信息结束 ===") + else: + self.log("无法获取旋转信息") \ No newline at end of file diff --git a/build/.111_staging/assets/scripts/R_R.py b/build/.111_staging/assets/scripts/R_R.py new file mode 100644 index 00000000..c78e88cb --- /dev/null +++ b/build/.111_staging/assets/scripts/R_R.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +旋转脚本 - 让对象持续旋转 +""" + +from core.script_system import ScriptBase + + +class RotatorScript(ScriptBase): + """旋转脚本类""" + + def __init__(self): + super().__init__() + self.rotation_speed_y = 30.0 # Y轴旋转速度 (度/秒) + self.max_angle = 30.0 # 最大旋转角度(相对于初始角度) + self.direction = 1 + self.current_offset = 0.0 # 当前相对于初始角度的偏移 + self.initial_angle = None # 模型的初始角度 + self.is_rotating = True # 是否正在旋转 + + def start(self): + """脚本开始时调用""" + self.log("旋转脚本启动!") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + self.log(f"最大旋转角度: ±{self.max_angle}度") + + # 记录模型的初始角度 + if self.gameObject: + initial_hpr = self.gameObject.getHpr() + self.initial_angle = initial_hpr.getZ() # 记录Z轴的初始角度 + self.log(f"模型初始角度: {self.initial_angle}度") + self.log(f"旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") + else: + self.log("⚠️ 无法获取游戏对象,使用默认初始角度0") + self.initial_angle = 0.0 + + def update(self, dt): + """每帧更新""" + if not self.is_rotating or self.initial_angle is None: + return + + # 计算角度变化量 + delta_angle = self.rotation_speed_y * dt * self.direction + self.current_offset += delta_angle + + # 如果超出角度范围,则反向并限制在边界 + if self.current_offset > self.max_angle: + self.current_offset = self.max_angle + self.direction *= -1 + elif self.current_offset < -self.max_angle: + self.current_offset = -self.max_angle + self.direction *= -1 + + # 计算最终角度(初始角度 + 偏移量) + final_angle = self.initial_angle + self.current_offset + + # 设置新的旋转(只改变Z轴,保持其他不变) + current_hpr = self.gameObject.getHpr() + self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), final_angle) + + # if not self.is_rotating: + # return + # + # # 获取当前旋转并应用增量 + # current_hpr = self.gameObject.getHpr() + # new_r = current_hpr.getZ() + self.rotation_speed_y * dt + # self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), new_r) + + def on_destroy(self): + """脚本销毁时调用""" + self.log("旋转脚本停止") + + # ==================== 控制方法 ==================== + + def set_max_angle(self, new_max_angle): + """ + 设置新的最大旋转角度 + + Args: + new_max_angle: 新的最大角度值 + """ + self.max_angle = new_max_angle + self.log(f"最大旋转角度已设置为: ±{self.max_angle}度") + if self.initial_angle is not None: + self.log(f"新的旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") + + def set_rotation_speed(self, new_speed): + """ + 设置新的旋转速度 + + Args: + new_speed: 新的旋转速度(度/秒) + """ + self.rotation_speed_y = new_speed + self.log(f"旋转速度已设置为: {self.rotation_speed_y}度/秒") + + def pause_rotation(self): + """暂停旋转""" + self.is_rotating = False + self.log("旋转已暂停") + + def resume_rotation(self): + """恢复旋转""" + self.is_rotating = True + self.log("旋转已恢复") + + def reset_to_initial_angle(self): + """重置到初始角度""" + if self.initial_angle is not None and self.gameObject: + current_hpr = self.gameObject.getHpr() + self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), self.initial_angle) + self.current_offset = 0.0 + self.direction = 1 + self.log(f"已重置到初始角度: {self.initial_angle}度") + + def get_current_info(self): + """获取当前旋转信息""" + if self.gameObject and self.initial_angle is not None: + current_hpr = self.gameObject.getHpr() + current_angle = current_hpr.getZ() + self.log("=== 当前旋转信息 ===") + self.log(f"初始角度: {self.initial_angle}度") + self.log(f"当前角度: {current_angle}度") + self.log(f"偏移量: {self.current_offset}度") + self.log(f"最大角度: ±{self.max_angle}度") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + self.log(f"旋转方向: {'正向' if self.direction > 0 else '反向'}") + self.log(f"旋转状态: {'运行中' if self.is_rotating else '已暂停'}") + self.log("=== 信息结束 ===") + else: + self.log("无法获取旋转信息") \ No newline at end of file diff --git a/build/.111_staging/assets/scripts/Rotate_H_Script.py b/build/.111_staging/assets/scripts/Rotate_H_Script.py new file mode 100644 index 00000000..ffe0e294 --- /dev/null +++ b/build/.111_staging/assets/scripts/Rotate_H_Script.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +旋转脚本 - 让对象持续旋转 +""" + +from core.script_system import ScriptBase + + +class RotatorScript(ScriptBase): + """旋转脚本类""" + + def __init__(self): + super().__init__() + self.rotation_speed_y = 30.0 # Y轴旋转速度 (度/秒) + self.max_angle = 30.0 # 最大旋转角度(相对于初始角度) + self.direction = 1 + self.current_offset = 0.0 # 当前相对于初始角度的偏移 + self.initial_angle = None # 模型的初始角度 + self.is_rotating = True # 是否正在旋转 + + # 机器人式停顿参数 + self.pause_duration = 0.5 # 停顿时间(秒) + self.current_pause_time = 0.0 # 当前停顿计时 + self.is_paused = False # 是否正在停顿 + self.robot_mode = True # 是否启用机器人模式 + + def start(self): + """脚本开始时调用""" + self.log("机器人旋转脚本启动!") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + self.log(f"最大旋转角度: ±{self.max_angle}度") + self.log(f"机器人模式: {'开启' if self.robot_mode else '关闭'}") + if self.robot_mode: + self.log(f"停顿时间: {self.pause_duration}秒") + + # 记录模型的初始角度 + if self.gameObject: + initial_hpr = self.gameObject.getHpr() + self.initial_angle = initial_hpr.getY() # 记录Y轴的初始角度(Pitch) + self.log(f"模型初始角度: {self.initial_angle}度") + self.log(f"旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") + else: + self.log("⚠️ 无法获取游戏对象,使用默认初始角度0") + self.initial_angle = 0.0 + + def update(self, dt): + """每帧更新 - 机器人式旋转""" + if not self.is_rotating or self.initial_angle is None: + return + + # 如果正在停顿中 + if self.is_paused: + self.current_pause_time += dt + if self.current_pause_time >= self.pause_duration: + # 停顿结束,继续旋转 + self.is_paused = False + self.current_pause_time = 0.0 + self.log(f"停顿结束,继续旋转,方向: {'正向' if self.direction > 0 else '反向'}") + return + + # 计算角度变化量 + delta_angle = self.rotation_speed_y * dt * self.direction + self.current_offset += delta_angle + + # 检查是否到达边界 + reached_boundary = False + if self.current_offset > self.max_angle: + self.current_offset = self.max_angle + self.direction *= -1 + reached_boundary = True + elif self.current_offset < -self.max_angle: + self.current_offset = -self.max_angle + self.direction *= -1 + reached_boundary = True + + # 如果到达边界且启用机器人模式,开始停顿 + if reached_boundary and self.robot_mode: + self.is_paused = True + self.current_pause_time = 0.0 + self.log(f"到达边界 ({self.current_offset}°),开始停顿 {self.pause_duration}秒") + + # 计算最终角度(初始角度 + 偏移量) + final_angle = self.initial_angle + self.current_offset + + # 设置新的旋转(只改变Y轴,保持其他不变) + current_hpr = self.gameObject.getHpr() + self.gameObject.setHpr(final_angle, current_hpr.getY(), current_hpr.getZ()) + + # if not self.is_rotating: + # return + # + # # 获取当前旋转并应用增量 + # current_hpr = self.gameObject.getHpr() + # new_r = current_hpr.getZ() + self.rotation_speed_y * dt + # self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), new_r) + + def on_destroy(self): + """脚本销毁时调用""" + self.log("机器人旋转脚本停止") + + # ==================== 机器人模式控制方法 ==================== + + def set_robot_mode(self, enabled=True): + """ + 启用或禁用机器人模式 + + Args: + enabled: 是否启用机器人模式 + """ + self.robot_mode = enabled + self.log(f"机器人模式: {'开启' if enabled else '关闭'}") + if not enabled: + self.is_paused = False # 如果禁用机器人模式,立即结束停顿 + + def set_pause_duration(self, duration): + """ + 设置停顿时间 + + Args: + duration: 停顿时间(秒) + """ + self.pause_duration = max(0.1, duration) # 最小0.1秒 + self.log(f"停顿时间已设置为: {self.pause_duration}秒") + + def set_max_angle(self, new_max_angle): + """ + 设置新的最大旋转角度 + + Args: + new_max_angle: 新的最大角度值 + """ + self.max_angle = new_max_angle + self.log(f"最大旋转角度已设置为: ±{self.max_angle}度") + if self.initial_angle is not None: + self.log(f"新的旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") + + def set_rotation_speed(self, new_speed): + """ + 设置新的旋转速度 + + Args: + new_speed: 新的旋转速度(度/秒) + """ + self.rotation_speed_y = new_speed + self.log(f"旋转速度已设置为: {self.rotation_speed_y}度/秒") + + def pause_rotation(self): + """暂停旋转""" + self.is_rotating = False + self.log("旋转已暂停") + + def resume_rotation(self): + """恢复旋转""" + self.is_rotating = True + self.is_paused = False # 同时结束停顿状态 + self.log("旋转已恢复") + + def reset_to_initial_angle(self): + """重置到初始角度""" + if self.initial_angle is not None and self.gameObject: + current_hpr = self.gameObject.getHpr() + self.gameObject.setHpr(current_hpr.getX(), self.initial_angle, current_hpr.getZ()) + self.current_offset = 0.0 + self.direction = 1 + self.is_paused = False + self.current_pause_time = 0.0 + self.log(f"已重置到初始角度: {self.initial_angle}度") + + def get_current_info(self): + """获取当前旋转信息""" + if self.gameObject and self.initial_angle is not None: + current_hpr = self.gameObject.getHpr() + current_angle = current_hpr.getY() + self.log("=== 机器人旋转信息 ===") + self.log(f"初始角度: {self.initial_angle}度") + self.log(f"当前角度: {current_angle}度") + self.log(f"偏移量: {self.current_offset}度") + self.log(f"最大角度: ±{self.max_angle}度") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + self.log(f"旋转方向: {'正向' if self.direction > 0 else '反向'}") + self.log(f"旋转状态: {'运行中' if self.is_rotating else '已暂停'}") + self.log(f"机器人模式: {'开启' if self.robot_mode else '关闭'}") + if self.robot_mode: + self.log(f"停顿时间: {self.pause_duration}秒") + self.log(f"当前状态: {'停顿中' if self.is_paused else '旋转中'}") + if self.is_paused: + remaining_time = self.pause_duration - self.current_pause_time + self.log(f"剩余停顿时间: {remaining_time:.1f}秒") + self.log("=== 信息结束 ===") + else: + self.log("无法获取旋转信息") + + # ==================== 预设配置方法 ==================== + + def set_slow_robot_mode(self): + """预设:慢速机器人模式""" + self.set_rotation_speed(15.0) + self.set_pause_duration(1.0) + self.set_robot_mode(True) + self.log("已设置为慢速机器人模式") + + def set_fast_robot_mode(self): + """预设:快速机器人模式""" + self.set_rotation_speed(45.0) + self.set_pause_duration(0.3) + self.set_robot_mode(True) + self.log("已设置为快速机器人模式") + + def set_smooth_mode(self): + """预设:平滑模式(非机器人)""" + self.set_robot_mode(False) + self.set_rotation_speed(30.0) + self.log("已设置为平滑旋转模式") \ No newline at end of file diff --git a/build/.111_staging/assets/scripts/Rotate_P_Script.py b/build/.111_staging/assets/scripts/Rotate_P_Script.py new file mode 100644 index 00000000..98d02cdf --- /dev/null +++ b/build/.111_staging/assets/scripts/Rotate_P_Script.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +旋转脚本 - 让对象持续旋转 +""" + +from core.script_system import ScriptBase + + +class RotatorScript(ScriptBase): + """旋转脚本类""" + + def __init__(self): + super().__init__() + self.rotation_speed_y = 30.0 # Y轴旋转速度 (度/秒) + self.max_angle = 25.0 # 最大旋转角度(相对于初始角度) + self.direction = 1 + self.current_offset = 0.0 # 当前相对于初始角度的偏移 + self.initial_angle = None # 模型的初始角度 + self.is_rotating = True # 是否正在旋转 + + # 机器人式停顿参数 + self.pause_duration = 0.5 # 停顿时间(秒) + self.current_pause_time = 0.0 # 当前停顿计时 + self.is_paused = False # 是否正在停顿 + self.robot_mode = True # 是否启用机器人模式 + + def start(self): + """脚本开始时调用""" + self.log("机器人旋转脚本启动!") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + self.log(f"最大旋转角度: ±{self.max_angle}度") + self.log(f"机器人模式: {'开启' if self.robot_mode else '关闭'}") + if self.robot_mode: + self.log(f"停顿时间: {self.pause_duration}秒") + + # 记录模型的初始角度 + if self.gameObject: + initial_hpr = self.gameObject.getHpr() + self.initial_angle = initial_hpr.getY() # 记录Y轴的初始角度(Pitch) + self.log(f"模型初始角度: {self.initial_angle}度") + self.log(f"旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") + else: + self.log("⚠️ 无法获取游戏对象,使用默认初始角度0") + self.initial_angle = 0.0 + + def update(self, dt): + """每帧更新 - 机器人式旋转""" + if not self.is_rotating or self.initial_angle is None: + return + + # 如果正在停顿中 + if self.is_paused: + self.current_pause_time += dt + if self.current_pause_time >= self.pause_duration: + # 停顿结束,继续旋转 + self.is_paused = False + self.current_pause_time = 0.0 + self.log(f"停顿结束,继续旋转,方向: {'正向' if self.direction > 0 else '反向'}") + return + + # 计算角度变化量 + delta_angle = self.rotation_speed_y * dt * self.direction + self.current_offset += delta_angle + + # 检查是否到达边界 + reached_boundary = False + if self.current_offset > self.max_angle: + self.current_offset = self.max_angle + self.direction *= -1 + reached_boundary = True + elif self.current_offset < -self.max_angle: + self.current_offset = -self.max_angle + self.direction *= -1 + reached_boundary = True + + # 如果到达边界且启用机器人模式,开始停顿 + if reached_boundary and self.robot_mode: + self.is_paused = True + self.current_pause_time = 0.0 + self.log(f"到达边界 ({self.current_offset}°),开始停顿 {self.pause_duration}秒") + + # 计算最终角度(初始角度 + 偏移量) + final_angle = self.initial_angle + self.current_offset + + # 设置新的旋转(只改变Y轴,保持其他不变) + current_hpr = self.gameObject.getHpr() + self.gameObject.setHpr(current_hpr.getX(), final_angle, current_hpr.getZ()) + + # if not self.is_rotating: + # return + # + # # 获取当前旋转并应用增量 + # current_hpr = self.gameObject.getHpr() + # new_r = current_hpr.getZ() + self.rotation_speed_y * dt + # self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), new_r) + + def on_destroy(self): + """脚本销毁时调用""" + self.log("机器人旋转脚本停止") + + # ==================== 机器人模式控制方法 ==================== + + def set_robot_mode(self, enabled=True): + """ + 启用或禁用机器人模式 + + Args: + enabled: 是否启用机器人模式 + """ + self.robot_mode = enabled + self.log(f"机器人模式: {'开启' if enabled else '关闭'}") + if not enabled: + self.is_paused = False # 如果禁用机器人模式,立即结束停顿 + + def set_pause_duration(self, duration): + """ + 设置停顿时间 + + Args: + duration: 停顿时间(秒) + """ + self.pause_duration = max(0.1, duration) # 最小0.1秒 + self.log(f"停顿时间已设置为: {self.pause_duration}秒") + + def set_max_angle(self, new_max_angle): + """ + 设置新的最大旋转角度 + + Args: + new_max_angle: 新的最大角度值 + """ + self.max_angle = new_max_angle + self.log(f"最大旋转角度已设置为: ±{self.max_angle}度") + if self.initial_angle is not None: + self.log(f"新的旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") + + def set_rotation_speed(self, new_speed): + """ + 设置新的旋转速度 + + Args: + new_speed: 新的旋转速度(度/秒) + """ + self.rotation_speed_y = new_speed + self.log(f"旋转速度已设置为: {self.rotation_speed_y}度/秒") + + def pause_rotation(self): + """暂停旋转""" + self.is_rotating = False + self.log("旋转已暂停") + + def resume_rotation(self): + """恢复旋转""" + self.is_rotating = True + self.is_paused = False # 同时结束停顿状态 + self.log("旋转已恢复") + + def reset_to_initial_angle(self): + """重置到初始角度""" + if self.initial_angle is not None and self.gameObject: + current_hpr = self.gameObject.getHpr() + self.gameObject.setHpr(current_hpr.getX(), self.initial_angle, current_hpr.getZ()) + self.current_offset = 0.0 + self.direction = 1 + self.is_paused = False + self.current_pause_time = 0.0 + self.log(f"已重置到初始角度: {self.initial_angle}度") + + def get_current_info(self): + """获取当前旋转信息""" + if self.gameObject and self.initial_angle is not None: + current_hpr = self.gameObject.getHpr() + current_angle = current_hpr.getY() + self.log("=== 机器人旋转信息 ===") + self.log(f"初始角度: {self.initial_angle}度") + self.log(f"当前角度: {current_angle}度") + self.log(f"偏移量: {self.current_offset}度") + self.log(f"最大角度: ±{self.max_angle}度") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + self.log(f"旋转方向: {'正向' if self.direction > 0 else '反向'}") + self.log(f"旋转状态: {'运行中' if self.is_rotating else '已暂停'}") + self.log(f"机器人模式: {'开启' if self.robot_mode else '关闭'}") + if self.robot_mode: + self.log(f"停顿时间: {self.pause_duration}秒") + self.log(f"当前状态: {'停顿中' if self.is_paused else '旋转中'}") + if self.is_paused: + remaining_time = self.pause_duration - self.current_pause_time + self.log(f"剩余停顿时间: {remaining_time:.1f}秒") + self.log("=== 信息结束 ===") + else: + self.log("无法获取旋转信息") + + # ==================== 预设配置方法 ==================== + + def set_slow_robot_mode(self): + """预设:慢速机器人模式""" + self.set_rotation_speed(15.0) + self.set_pause_duration(1.0) + self.set_robot_mode(True) + self.log("已设置为慢速机器人模式") + + def set_fast_robot_mode(self): + """预设:快速机器人模式""" + self.set_rotation_speed(45.0) + self.set_pause_duration(0.3) + self.set_robot_mode(True) + self.log("已设置为快速机器人模式") + + def set_smooth_mode(self): + """预设:平滑模式(非机器人)""" + self.set_robot_mode(False) + self.set_rotation_speed(30.0) + self.log("已设置为平滑旋转模式") \ No newline at end of file diff --git a/build/.111_staging/assets/scripts/Rotate_R_Script.py b/build/.111_staging/assets/scripts/Rotate_R_Script.py new file mode 100644 index 00000000..4d22edac --- /dev/null +++ b/build/.111_staging/assets/scripts/Rotate_R_Script.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +旋转脚本 - 让对象持续旋转 +""" + +from core.script_system import ScriptBase + + +class RotatorScript(ScriptBase): + """旋转脚本类""" + + def __init__(self): + super().__init__() + self.rotation_speed_y = 30.0 # Y轴旋转速度 (度/秒) + self.max_angle = 15.0 + self.direction = 1 + self.current_angle = 0.0 + self.is_rotating = True # 是否正在旋转 + + def start(self): + """脚本开始时调用""" + self.log("旋转脚本启动!") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + + def update(self, dt): + """每帧更新""" + + delta_angle = self.rotation_speed_y * dt * self.direction + self.current_angle += delta_angle + + # 如果超出角度范围,则反向 + if self.current_angle > self.max_angle: + self.current_angle = self.max_angle + self.direction *= -1 + elif self.current_angle < -self.max_angle: + self.current_angle = -self.max_angle + self.direction *= -1 + + # 设置新的旋转(只改变Z轴,保持其他不变) + base_hpr = self.gameObject.getHpr() + new_r = self.current_angle + self.gameObject.setHpr(base_hpr.getX(), base_hpr.getY(), new_r) + + # if not self.is_rotating: + # return + # + # # 获取当前旋转并应用增量 + # current_hpr = self.gameObject.getHpr() + # new_r = current_hpr.getZ() + self.rotation_speed_y * dt + # self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), new_r) + + def on_destroy(self): + """脚本销毁时调用""" + self.log("旋转脚本停止") \ No newline at end of file diff --git a/build/.111_staging/assets/scripts/RotatorScript.py b/build/.111_staging/assets/scripts/RotatorScript.py new file mode 100644 index 00000000..bdc49495 --- /dev/null +++ b/build/.111_staging/assets/scripts/RotatorScript.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +旋转脚本 - 让对象持续旋转 +""" + +from core.script_system import ScriptBase + +class RotatorScript(ScriptBase): + """旋转脚本类""" + + def __init__(self): + super().__init__() + self.rotation_speed_y = 30.0 # Y轴旋转速度 (度/秒) + self.is_rotating = True # 是否正在旋转 + + def start(self): + """脚本开始时调用""" + self.log("旋转脚本启动!") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + + def update(self, dt): + # 检查 gameObject 是否存在且不为空 + if not self.gameObject or self.gameObject.isEmpty(): + print("RotatorScript: gameObject is empty or None, skipping update") + return + """每帧更新""" + if not self.is_rotating: + return + + # 获取当前旋转并应用增量 + current_hpr = self.gameObject.getHpr() + new_h = current_hpr.getX() + self.rotation_speed_y * dt + self.gameObject.setHpr(new_h, current_hpr.getY(), current_hpr.getZ()) + + def on_destroy(self): + """脚本销毁时调用""" + self.log("旋转脚本停止") \ No newline at end of file diff --git a/build/.111_staging/assets/scripts/ScalerScript.py b/build/.111_staging/assets/scripts/ScalerScript.py new file mode 100644 index 00000000..406d65fa --- /dev/null +++ b/build/.111_staging/assets/scripts/ScalerScript.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +缩放脚本 - 让对象产生呼吸般的缩放效果 +""" + +from core.script_system import ScriptBase +import math + +class ScalerScript(ScriptBase): + """缩放脚本类""" + + def __init__(self): + super().__init__() + + # 缩放参数 + self.base_scale = 1.0 # 基础缩放 + self.scale_amplitude = 0.3 # 缩放幅度 + self.scale_speed = 2.0 # 缩放速度 (周期/秒) + self.uniform_scale = True # 是否统一缩放(所有轴) + + # 内部变量 + self.time_accumulator = 0.0 # 时间累积器 + self.original_scale = None # 原始缩放 + self.is_scaling = True # 是否正在缩放 + + def start(self): + """脚本开始时调用""" + self.log("缩放脚本启动!") + self.log(f"缩放参数: 基础={self.base_scale}, 幅度={self.scale_amplitude}, 速度={self.scale_speed}") + + # 记录原始缩放 + self.original_scale = self.gameObject.getScale() + self.log(f"原始缩放: {self.original_scale}") + + def update(self, dt): + """每帧更新""" + if not self.is_scaling: + return + + # 累积时间 + self.time_accumulator += dt + + # 计算正弦波缩放值 + sine_value = math.sin(self.time_accumulator * self.scale_speed * 2 * math.pi) + scale_factor = self.base_scale + (self.scale_amplitude * sine_value) + + # 应用缩放 + if self.uniform_scale: + # 统一缩放 + self.gameObject.setScale(scale_factor) + else: + # 非统一缩放(仅Z轴) + current_scale = self.gameObject.getScale() + self.gameObject.setScale(current_scale.getX(), current_scale.getY(), scale_factor) + + def set_scale_parameters(self, base=None, amplitude=None, speed=None, uniform=None): + """设置缩放参数""" + if base is not None: + self.base_scale = base + if amplitude is not None: + self.scale_amplitude = amplitude + if speed is not None: + self.scale_speed = speed + if uniform is not None: + self.uniform_scale = uniform + + self.log(f"缩放参数更新: 基础={self.base_scale}, 幅度={self.scale_amplitude}, 速度={self.scale_speed}") + + def toggle_scaling(self): + """切换缩放状态""" + self.is_scaling = not self.is_scaling + status = "恢复" if self.is_scaling else "暂停" + self.log(f"缩放{status}") + + def reset_scale(self): + """重置到原始缩放""" + if self.original_scale: + self.gameObject.setScale(self.original_scale) + self.time_accumulator = 0.0 + self.log("缩放已重置到原始值") + + def pulse_once(self): + """执行一次脉冲缩放""" + self.time_accumulator = 0.0 + self.log("执行脉冲缩放") + + def on_destroy(self): + """脚本销毁时调用""" + self.log("缩放脚本停止") \ No newline at end of file diff --git a/build/.111_staging/assets/scripts/TestMover.py b/build/.111_staging/assets/scripts/TestMover.py new file mode 100644 index 00000000..4116b1e3 --- /dev/null +++ b/build/.111_staging/assets/scripts/TestMover.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +TestMover - 移动脚本 +""" + +from core.script_system import ScriptBase + +class Testmover(ScriptBase): + """移动脚本类""" + + def __init__(self): + super().__init__() + self.speed = 5.0 # 移动速度 + self.direction = [1, 0, 0] # 移动方向 + + def start(self): + """脚本开始时调用""" + self.log("移动脚本开始运行!") + + def update(self, dt): + """每帧更新""" + if self.transform: + # 计算移动偏移 + offset_x = self.direction[0] * self.speed * dt + offset_y = self.direction[1] * self.speed * dt + offset_z = self.direction[2] * self.speed * dt + + # 更新位置 + current_pos = self.transform.getPos() + new_pos = ( + current_pos.x + offset_x, + current_pos.y + offset_y, + current_pos.z + offset_z + ) + self.transform.setPos(*new_pos) + + def on_destroy(self): + """脚本销毁时调用""" + self.log("移动脚本被销毁") diff --git a/build/.111_staging/assets/scripts/TestRotator.py b/build/.111_staging/assets/scripts/TestRotator.py new file mode 100644 index 00000000..83042e31 --- /dev/null +++ b/build/.111_staging/assets/scripts/TestRotator.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +TestRotator - 自定义脚本 +""" + +from core.script_system import ScriptBase + +class Testrotator(ScriptBase): + """自定义脚本类""" + + def __init__(self): + super().__init__() + # 在这里初始化您的变量 + + def start(self): + """脚本开始时调用""" + self.log("脚本开始运行!") + + def update(self, dt): + """每帧更新""" + # 在这里编写更新逻辑 + pass + + def on_destroy(self): + """脚本销毁时调用""" + self.log("脚本被销毁") diff --git a/build/.111_staging/assets/scripts/TestScaler.py b/build/.111_staging/assets/scripts/TestScaler.py new file mode 100644 index 00000000..536883d7 --- /dev/null +++ b/build/.111_staging/assets/scripts/TestScaler.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +TestScaler - 自定义脚本 +""" + +from core.script_system import ScriptBase + +class Testscaler(ScriptBase): + """自定义脚本类""" + + def __init__(self): + super().__init__() + # 在这里初始化您的变量 + + def start(self): + """脚本开始时调用""" + self.log("脚本开始运行!") + + def update(self, dt): + """每帧更新""" + # 在这里编写更新逻辑 + pass + + def on_destroy(self): + """脚本销毁时调用""" + self.log("脚本被销毁") diff --git a/build/.111_staging/assets/scripts/a.py b/build/.111_staging/assets/scripts/a.py new file mode 100644 index 00000000..34bd5408 --- /dev/null +++ b/build/.111_staging/assets/scripts/a.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +a - 自定义脚本 +""" + +from core.script_system import ScriptBase + +class A(ScriptBase): + """自定义脚本类""" + + def __init__(self): + super().__init__() + # 在这里初始化您的变量 + + def start(self): + """脚本开始时调用""" + self.log("脚本开始运行!") + + def update(self, dt): + """每帧更新""" + # 在这里编写更新逻辑 + pass + + def on_destroy(self): + """脚本销毁时调用""" + self.log("脚本被销毁") diff --git a/build/.111_staging/assets/scripts/example_script.py b/build/.111_staging/assets/scripts/example_script.py new file mode 100644 index 00000000..ecba0da9 --- /dev/null +++ b/build/.111_staging/assets/scripts/example_script.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +示例脚本 - 演示如何编写脚本 +""" + +from core.script_system import ScriptBase + +class ExampleScript(ScriptBase): + """示例脚本类""" + + def __init__(self): + super().__init__() + self.counter = 0 + self.rotation_speed = 30.0 # 度/秒 + + def start(self): + """脚本开始时调用""" + self.log("示例脚本开始运行!") + self.log(f"挂载到对象: {self.gameObject.getName()}") + + def update(self, dt): + """每帧更新""" + self.counter += 1 + + # 每60帧输出一次信息 + if self.counter % 60 == 0: + self.log(f"运行了 {self.counter} 帧") + + # 让对象旋转 + if self.transform: + current_h = self.transform.getH() + new_h = current_h + self.rotation_speed * dt + self.transform.setH(new_h) + + def on_destroy(self): + """脚本销毁时调用""" + self.log("示例脚本被销毁") + + def on_enable(self): + """脚本启用时调用""" + self.log("示例脚本被启用") + + def on_disable(self): + """脚本禁用时调用""" + self.log("示例脚本被禁用") diff --git a/build/.111_staging/assets/scripts/test_quick_script.py b/build/.111_staging/assets/scripts/test_quick_script.py new file mode 100644 index 00000000..716070a3 --- /dev/null +++ b/build/.111_staging/assets/scripts/test_quick_script.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +test_quick_script - 自定义脚本 +""" + +from core.script_system import ScriptBase + +class TestQuickScript(ScriptBase): + """自定义脚本类""" + + def __init__(self): + super().__init__() + # 在这里初始化您的变量 + + def start(self): + """脚本开始时调用""" + self.log("脚本开始运行!") + + def update(self, dt): + """每帧更新""" + # 在这里编写更新逻辑 + pass + + def on_destroy(self): + """脚本销毁时调用""" + self.log("脚本被销毁") diff --git a/build/.111_staging/source/main.py b/build/.111_staging/source/main.py new file mode 100644 index 00000000..1d0bc1da --- /dev/null +++ b/build/.111_staging/source/main.py @@ -0,0 +1,534 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +"""EG packaged project runtime template.""" + +from __future__ import annotations + +import importlib +import json +import os +import sys +import traceback + +from direct.actor.Actor import Actor +from direct.showbase.ShowBase import ShowBase +from panda3d.core import ( + CardMaker, + Filename, + MovieTexture, + Point3, + TextNode, + Texture, + TransparencyAttrib, + Vec3, + load_prc_file_data, +) + +PROJECT_NAME = "111" + + +def _bootstrap_paths(): + if getattr(sys, "frozen", False): + project_root = os.path.dirname(sys.executable) + else: + project_root = os.path.dirname(os.path.abspath(__file__)) + + os.chdir(project_root) + + search_paths = [ + project_root, + os.path.join(project_root, "third_party"), + os.path.join(project_root, "RenderPipelineFile"), + ] + for path in search_paths: + if os.path.isdir(path) and path not in sys.path: + sys.path.insert(0, path) + + return project_root + + +PROJECT_ROOT = _bootstrap_paths() + + +class MainApp(ShowBase): + def __init__(self): + self.project_path = PROJECT_ROOT + self.gui_elements = [] + self.chinese_font = None + self.script_manager = None + self.render_pipeline = None + + load_prc_file_data( + "", + f""" + win-size 1380 750 + window-title {PROJECT_NAME} + sync-video false + show-frame-rate-meter false + support-threads false + """, + ) + + from rpcore import RenderPipeline + + self.render_pipeline = RenderPipeline() + self.render_pipeline.pre_showbase_init() + ShowBase.__init__(self) + self.render_pipeline.create(self) + self.render_pipeline._showbase.camera = self.render_pipeline._showbase.cam + self.disableMouse() + + self._load_font() + self._init_script_manager() + self.load_full_scene() + self.load_gui_from_json() + + def _init_script_manager(self): + script_module = importlib.import_module("core.script_system") + self.script_manager = script_module.ScriptManager(self) + self.script_manager.hot_reload_enabled = False + + scripts_dir = self.get_resource_path("scripts") + if hasattr(self.script_manager, "set_scripts_directory"): + self.script_manager.set_scripts_directory( + scripts_dir, + create=False, + reload_scripts=False, + ) + + self.script_manager.start_system() + self.script_manager.set_hot_reload_enabled(False) + + def _load_font(self): + font_candidates = [ + "C:/Windows/Fonts/msyh.ttc", + "C:/Windows/Fonts/simhei.ttf", + ] + for font_path in font_candidates: + if os.path.exists(font_path): + try: + self.chinese_font = self.loader.loadFont(font_path) + if self.chinese_font: + print(f"✓ 中文字体加载成功: {font_path}") + return + except Exception: + continue + print("⚠ 未找到可用中文字体,继续使用默认字体") + + def get_chinese_font(self): + return self.chinese_font + + def get_resource_path(self, relative_path): + return os.path.normpath(os.path.join(PROJECT_ROOT, relative_path)) + + def _resolve_media_path(self, relative_path): + if not relative_path: + return "" + if os.path.isabs(relative_path): + return relative_path + return self.get_resource_path(relative_path.replace("/", os.sep)) + + def load_full_scene(self): + scene_file = self.get_resource_path("scene.bam") + if not os.path.exists(scene_file): + print(f"⚠ 未找到场景文件: {scene_file}") + return + + scene = self.loader.loadModel(Filename.fromOsSpecific(scene_file)) + if not scene: + print("⚠ 场景文件加载失败") + return + + scene.reparentTo(self.render) + self.render_pipeline.prepare_scene(scene) + self.process_scene_elements(scene) + print("✓ 场景加载完成") + + def process_scene_elements(self, root_node): + processed_lights = set() + + def walk(node_path): + self._apply_user_visibility(node_path) + + if node_path.hasTag("scripts_info"): + try: + scripts_info = json.loads(node_path.getTag("scripts_info")) + self.process_scripts(node_path, scripts_info) + except Exception as e: + print(f"处理节点脚本失败 {node_path.getName()}: {e}") + + if node_path.hasTag("light_type") and node_path not in processed_lights: + if node_path.hasTag("is_auxiliary_light") and node_path.getTag("is_auxiliary_light").lower() == "true": + return + + light_type = node_path.getTag("light_type") + if light_type == "spot_light": + self._recreate_spot_light(node_path) + elif light_type == "point_light": + self._recreate_point_light(node_path) + processed_lights.add(node_path) + + for child in node_path.getChildren(): + walk(child) + + walk(root_node) + + def _apply_user_visibility(self, node_path): + if not node_path.hasTag("user_visible"): + return + + user_visible = node_path.getTag("user_visible").lower() == "true" + node_path.setPythonTag("user_visible", user_visible) + if user_visible: + node_path.show() + else: + node_path.hide() + + def _recreate_spot_light(self, light_node): + try: + from rpcore import SpotLight + + light = SpotLight() + light.direction = Vec3(0, 0, -1) + light.fov = float(light_node.getTag("light_fov")) if light_node.hasTag("light_fov") else 70.0 + light.energy = float(light_node.getTag("light_energy")) if light_node.hasTag("light_energy") else 5000.0 + light.radius = float(light_node.getTag("light_radius")) if light_node.hasTag("light_radius") else 1000.0 + light.casts_shadows = True + light.shadow_map_resolution = 256 + light.setPos(light_node.getPos()) + self.render_pipeline.add_light(light) + except Exception as e: + print(f"创建聚光灯失败 {light_node.getName()}: {e}") + + def _recreate_point_light(self, light_node): + try: + from rpcore import PointLight + + light = PointLight() + light.energy = float(light_node.getTag("light_energy")) if light_node.hasTag("light_energy") else 5000.0 + light.radius = float(light_node.getTag("light_radius")) if light_node.hasTag("light_radius") else 1000.0 + light.inner_radius = 0.4 + light.casts_shadows = True + light.shadow_map_resolution = 256 + light.setPos(light_node.getPos()) + self.render_pipeline.add_light(light) + except Exception as e: + print(f"创建点光源失败 {light_node.getName()}: {e}") + + def process_scripts(self, node_path, script_info_list): + if not self.script_manager: + return + + for script_info in script_info_list or []: + script_name = str(script_info.get("name", "") or "").strip() + if not script_name: + continue + + try: + if script_name not in self.script_manager.loader.script_classes: + script_path = "" + if hasattr(self.script_manager, "resolve_script_path"): + script_path = self.script_manager.resolve_script_path(script_info) + if script_path: + self.script_manager.load_script_from_file(script_path) + + script_component = self.script_manager.add_script_to_object(node_path, script_name) + if script_component: + print(f"✓ 脚本 {script_name} 已挂载到 {node_path.getName()}") + else: + print(f"⚠ 脚本 {script_name} 挂载失败") + except Exception as e: + print(f"挂载脚本失败 {script_name}: {e}") + + def load_gui_from_json(self): + gui_json_path = self.get_resource_path(os.path.join("gui", "gui_elements.json")) + if not os.path.exists(gui_json_path): + return + + with open(gui_json_path, "r", encoding="utf-8") as f: + content = f.read().strip() + if not content: + return + + gui_data = json.loads(content) + self.create_gui_elements(gui_data) + + def create_gui_elements(self, element_data): + processed_names = set() + element_original_data = {} + + for index, gui_info in enumerate(element_data or []): + name = gui_info.get("name", f"gui_element_{index}") + element_original_data[name] = { + "scale": gui_info.get("scale", [1, 1, 1]), + "position": gui_info.get("position", [0, 0, 0]), + "parent_name": gui_info.get("parent_name"), + } + + for index, gui_info in enumerate(element_data or []): + try: + gui_type = gui_info.get("type", "unknown") + name = gui_info.get("name", f"gui_element_{index}") + if name in processed_names: + continue + processed_names.add(name) + + position = list(gui_info.get("position", [0, 0, 0])) + scale = list(gui_info.get("scale", [1, 1, 1])) + parent_name = gui_info.get("parent_name") + + if parent_name and parent_name in element_original_data: + parent_scale = element_original_data[parent_name]["scale"] + for i in range(min(len(position), len(parent_scale))): + position[i] *= parent_scale[i] + for i in range(min(len(scale), len(parent_scale))): + scale[i] *= parent_scale[i] + + text = gui_info.get("text", "") + image_path = self._resolve_media_path(gui_info.get("image_path", "")) + video_path = self._resolve_media_path(gui_info.get("video_path", "")) + + new_element = None + if gui_type == "3d_text": + new_element = self.create_gui_3d_text(tuple(position), text, scale[0] if scale else 1.0) + elif gui_type == "3d_image": + new_element = self.create_gui_3d_image(tuple(position), image_path, scale) + elif gui_type == "button": + new_element = self.create_gui_button(tuple(position), text, scale[0] if scale else 1.0) + elif gui_type == "label": + new_element = self.create_gui_label(tuple(position), text, scale[0] if scale else 1.0) + elif gui_type == "entry": + new_element = self.create_gui_entry(tuple(position), text, scale[0] if scale else 1.0) + elif gui_type == "2d_image": + new_element = self.create_gui_2d_image(tuple(position), image_path, scale) + elif gui_type == "video_screen": + new_element = self.create_gui_video_screen(tuple(position), scale, video_path) + elif gui_type == "2d_video_screen": + new_element = self.create_gui_2d_video_screen(tuple(position), scale, video_path) + + if not new_element: + continue + + self._apply_gui_metadata(new_element, gui_info, gui_type, text, image_path, video_path) + self.gui_elements.append(new_element) + + if gui_info.get("scripts"): + self.process_scripts(new_element, gui_info["scripts"]) + except Exception as e: + print(f"重建 GUI 元素失败: {e}") + traceback.print_exc() + + def _apply_gui_metadata(self, node, gui_info, gui_type, text, image_path, video_path): + try: + name = gui_info.get("name") + if name and hasattr(node, "setName"): + node.setName(name) + except Exception: + pass + + if hasattr(node, "setTag"): + node.setTag("gui_type", gui_type) + node.setTag("is_gui_element", "true") + if text: + node.setTag("gui_text", str(text)) + if image_path: + node.setTag("gui_image_path", str(image_path)) + if video_path: + node.setTag("video_path", str(video_path)) + for key, value in (gui_info.get("tags") or {}).items(): + try: + node.setTag(str(key), str(value)) + except Exception: + continue + + user_visible = bool(gui_info.get("user_visible", True)) + if hasattr(node, "setPythonTag"): + node.setPythonTag("user_visible", user_visible) + if hasattr(node, "show") and hasattr(node, "hide"): + if user_visible: + node.show() + else: + node.hide() + + def create_gui_button(self, pos=(0, 0, 0), text="按钮", size=0.1): + from direct.gui.DirectGui import DirectButton + + return DirectButton( + text=text, + pos=(pos[0], pos[1], pos[2]), + scale=size, + frameColor=(0.2, 0.6, 0.8, 1), + text_font=self.get_chinese_font() or None, + rolloverSound=None, + clickSound=None, + parent=self.aspect2d, + command=None, + ) + + def create_gui_label(self, pos=(0, 0, 0), text="标签", size=0.08): + from direct.gui.DirectGui import DirectLabel + + return DirectLabel( + text=text, + pos=(pos[0], pos[1], pos[2]), + scale=size, + frameColor=(0, 0, 0, 0), + text_fg=(1, 1, 1, 1), + text_font=self.get_chinese_font() or None, + text_align=TextNode.ACenter, + text_mayChange=True, + parent=self.aspect2d, + ) + + def create_gui_entry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08): + from direct.gui.DirectGui import DirectEntry + + return DirectEntry( + text="", + pos=(pos[0], pos[1], pos[2]), + scale=size, + command=self.on_gui_entry_submit, + initialText=placeholder, + numLines=1, + width=12, + focus=0, + frameColor=(0, 0, 0, 0), + text_fg=(1, 1, 1, 1), + text_font=self.get_chinese_font() or None, + text_align=TextNode.ACenter, + text_mayChange=True, + parent=self.aspect2d, + rolloverSound=None, + clickSound=None, + suppressKeys=True, + suppressMouse=True, + ) + + def on_gui_entry_submit(self, text, *_args): + print(f"GUI 输入框提交: {text}") + + def create_gui_2d_image(self, pos=(0, 0, 0), image_path="", size=(1, 1, 1)): + if isinstance(size, (list, tuple)) and len(size) >= 3: + width_scale = float(size[0]) * 0.2 + height_scale = float(size[2]) * 0.2 + else: + scalar = float(size) if isinstance(size, (int, float)) else 0.2 + width_scale = scalar * 0.1 + height_scale = width_scale + + cm = CardMaker("gui_2d_image") + cm.setFrame(-width_scale, width_scale, -height_scale, height_scale) + image_node = self.aspect2d.attachNewNode(cm.generate()) + image_node.setPos(pos) + image_node.setBin("fixed", 0) + image_node.setDepthWrite(False) + image_node.setDepthTest(False) + image_node.setTransparency(TransparencyAttrib.MAlpha) + if image_path: + texture = self.loader.loadTexture(image_path) + if texture: + image_node.setTexture(texture, 1) + return image_node + + def create_gui_3d_text(self, pos=(0, 0, 0), text="3D文本", size=0.5): + text_node = TextNode("gui_3d_text") + text_node.setText(text) + text_node.setAlign(TextNode.ACenter) + if self.get_chinese_font(): + text_node.setFont(self.get_chinese_font()) + + text_np = self.render.attachNewNode(text_node) + text_np.setPos(Vec3(pos[0], pos[1], pos[2])) + text_np.setScale(size) + text_np.setBin("fixed", 40) + text_np.setDepthWrite(False) + return text_np + + def create_gui_3d_image(self, pos=(0, 0, 0), image_path="", size=(1, 1, 1)): + if isinstance(size, (list, tuple)) and len(size) >= 3: + width_scale = float(size[0]) + height_scale = float(size[2]) + else: + width_scale = float(size) if isinstance(size, (int, float)) else 1.0 + height_scale = width_scale + + cm = CardMaker("gui_3d_image") + cm.setFrame(-width_scale, width_scale, -height_scale, height_scale) + image_node = self.render.attachNewNode(cm.generate()) + image_node.setPos(pos) + image_node.setTransparency(TransparencyAttrib.MAlpha) + if image_path: + texture = self.loader.loadTexture(image_path) + if texture: + image_node.setTexture(texture, 1) + return image_node + + def _load_movie_texture(self, name, video_path): + if not video_path: + return None + movie_texture = MovieTexture(name) + if not movie_texture.read(Filename.fromOsSpecific(video_path)): + print(f"⚠ 无法加载视频: {video_path}") + return None + movie_texture.play() + return movie_texture + + def create_gui_video_screen(self, pos=(0, 0, 0), size=(1, 1, 1), video_path=""): + if isinstance(size, (list, tuple)) and len(size) >= 3: + width_scale = float(size[0]) + height_scale = float(size[2]) + else: + width_scale = float(size) if isinstance(size, (int, float)) else 1.0 + height_scale = width_scale + + cm = CardMaker("gui_video_screen") + cm.setFrame(-width_scale, width_scale, -height_scale, height_scale) + video_node = self.render.attachNewNode(cm.generate()) + video_node.setPos(pos) + video_node.setTransparency(TransparencyAttrib.MAlpha) + + movie_texture = self._load_movie_texture("gui_video_texture_3d", video_path) + if movie_texture: + video_node.setTexture(movie_texture, 1) + return video_node + + def create_gui_2d_video_screen(self, pos=(0, 0, 0), size=(1, 1, 1), video_path=""): + if isinstance(size, (list, tuple)) and len(size) >= 3: + width_scale = float(size[0]) * 0.2 + height_scale = float(size[2]) * 0.2 + else: + scalar = float(size) if isinstance(size, (int, float)) else 0.2 + width_scale = scalar * 0.1 + height_scale = width_scale + + cm = CardMaker("gui_2d_video_screen") + cm.setFrame(-width_scale, width_scale, -height_scale, height_scale) + video_node = self.aspect2d.attachNewNode(cm.generate()) + video_node.setPos(pos) + video_node.setTransparency(TransparencyAttrib.MAlpha) + video_node.setDepthWrite(False) + video_node.setDepthTest(False) + + movie_texture = self._load_movie_texture("gui_video_texture_2d", video_path) + if movie_texture: + video_node.setTexture(movie_texture, 1) + return video_node + + def play_model_animation(self): + actors = self.render.findAllMatches("**/+ActorNode") + for actor_np in actors: + actor_node = actor_np.node() + if isinstance(actor_node, Actor): + anim_names = actor_node.getAnimNames() + if anim_names: + actor_node.loop(anim_names[0]) + + +if __name__ == "__main__": + try: + app = MainApp() + app.run() + except Exception as e: + print(f"应用程序启动失败: {e}") + traceback.print_exc() diff --git a/core/__init__.py b/core/__init__.py index 8ee75f7d..5ec285ba 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -1,26 +1,40 @@ -""" -Core package - 核心功能模块 - -包含引擎的核心功能: -- world.py: 基础世界功能(相机、光照、地板等) -- selection.py: 选择和变换系统 -- event_handler.py: 事件处理系统 -- tool_manager.py: 工具管理系统 -- script_system.py: 脚本系统 -""" - -from .world import CoreWorld -from .selection import SelectionSystem -from .event_handler import EventHandler -from .tool_manager import ToolManager -from .script_system import ScriptManager, ScriptBase, ScriptComponent - -__all__ = [ - 'CoreWorld', - 'SelectionSystem', - 'EventHandler', - 'ToolManager', - 'ScriptManager', - 'ScriptBase', - 'ScriptComponent' -] \ No newline at end of file +""" +Core package - 核心功能模块 + +Keep package imports lazy so lightweight runtime contexts can import +`core.script_system` without eagerly pulling in the editor stack. +""" + +from importlib import import_module + +__all__ = [ + "CoreWorld", + "SelectionSystem", + "EventHandler", + "ToolManager", + "ScriptManager", + "ScriptBase", + "ScriptComponent", +] + +_LAZY_IMPORTS = { + "CoreWorld": ("core.world", "CoreWorld"), + "SelectionSystem": ("core.selection", "SelectionSystem"), + "EventHandler": ("core.event_handler", "EventHandler"), + "ToolManager": ("core.tool_manager", "ToolManager"), + "ScriptManager": ("core.script_system", "ScriptManager"), + "ScriptBase": ("core.script_system", "ScriptBase"), + "ScriptComponent": ("core.script_system", "ScriptComponent"), +} + + +def __getattr__(name): + if name not in _LAZY_IMPORTS: + raise AttributeError(f"module 'core' has no attribute {name!r}") + + module_name, attr_name = _LAZY_IMPORTS[name] + module = import_module(module_name) + value = getattr(module, attr_name) + globals()[name] = value + return value + diff --git a/core/script_system.py b/core/script_system.py index f2086597..8be57be7 100644 --- a/core/script_system.py +++ b/core/script_system.py @@ -266,6 +266,24 @@ class ScriptLoader: del self.script_classes[script_name] print(f"✓ 脚本已卸载: {script_name}") + + def clear(self, unload_components: bool = False): + """清空当前加载的脚本缓存。""" + if unload_components: + for script_name in list(self.loaded_modules.keys()): + try: + self.unload_script(script_name) + except Exception as e: + print(f"卸载脚本失败 {script_name}: {e}") + else: + for module in list(self.loaded_modules.values()): + module_name = getattr(module, "__name__", "") + if module_name and module_name in sys.modules: + del sys.modules[module_name] + + self.loaded_modules.clear() + self.script_classes.clear() + self.file_mtimes.clear() def reload_script(self, script_path: str) -> Optional[type]: """重新加载脚本(热重载)""" @@ -313,7 +331,7 @@ class ScriptLoader: scripts_dir = self.script_manager.scripts_directory if os.path.exists(scripts_dir): for file_name in os.listdir(scripts_dir): - if file_name.endswith('.py'): + if file_name.endswith(('.py', '.pyc')): base_name = os.path.splitext(file_name)[0] if base_name == script_name: return os.path.join(scripts_dir, file_name) @@ -418,7 +436,7 @@ class ScriptManager: self.script_templates: Dict[str, type] = {} # 脚本名 -> 脚本类 # 脚本目录 - self.scripts_directory = "scripts" + self.scripts_directory = self._normalize_scripts_directory("scripts") self._ensure_scripts_directory() # 热重载监控 @@ -426,6 +444,137 @@ class ScriptManager: self.hot_reload_task = None print("✓ 脚本管理系统初始化完成") + + def _normalize_scripts_directory(self, directory: str) -> str: + directory = directory or "scripts" + return os.path.normpath(os.path.abspath(directory)) + + def get_project_path(self) -> Optional[str]: + project_manager = getattr(self.world, "project_manager", None) + project_path = getattr(project_manager, "current_project_path", None) + if project_path: + return os.path.normpath(project_path) + + project_path = getattr(self.world, "project_path", None) + if project_path: + return os.path.normpath(project_path) + + return None + + def get_project_scripts_directory(self, project_path: Optional[str] = None) -> Optional[str]: + project_path = project_path or self.get_project_path() + if not project_path: + return None + return os.path.normpath(os.path.join(project_path, "scripts")) + + def get_script_relative_path(self, script_path: str) -> str: + if not script_path: + return "" + + script_path = os.path.normpath(os.path.abspath(script_path)) + project_path = self.get_project_path() + if not project_path: + return "" + + try: + relative_path = os.path.relpath(script_path, project_path) + except ValueError: + return "" + + if relative_path.startswith(".."): + return "" + + return relative_path.replace("\\", "/") + + def resolve_script_path(self, script_info: Dict[str, Any]) -> str: + if not isinstance(script_info, dict): + return "" + + project_path = self.get_project_path() + candidates = [] + + for key in ("project_relative_path", "relative_path", "path", "file"): + raw_value = str(script_info.get(key, "") or "").strip() + if not raw_value: + continue + + normalized_value = raw_value.replace("/", os.sep) + if os.path.isabs(normalized_value): + candidates.append(os.path.normpath(normalized_value)) + continue + + if project_path: + candidates.append(os.path.normpath(os.path.join(project_path, normalized_value))) + + candidates.append(os.path.normpath(os.path.join(self.scripts_directory, normalized_value))) + + lower_value = normalized_value.lower() + if not lower_value.endswith((".py", ".pyc")): + candidates.append(os.path.normpath(os.path.join(self.scripts_directory, f"{normalized_value}.py"))) + candidates.append(os.path.normpath(os.path.join(self.scripts_directory, f"{normalized_value}.pyc"))) + elif lower_value.endswith(".py"): + candidates.append(os.path.normpath(os.path.join(self.scripts_directory, f"{os.path.splitext(normalized_value)[0]}.pyc"))) + elif lower_value.endswith(".pyc"): + candidates.append(os.path.normpath(os.path.join(self.scripts_directory, f"{os.path.splitext(normalized_value)[0]}.py"))) + + script_name = str(script_info.get("name", "") or "").strip() + if script_name: + candidates.append(os.path.normpath(os.path.join(self.scripts_directory, f"{script_name}.py"))) + candidates.append(os.path.normpath(os.path.join(self.scripts_directory, f"{script_name}.pyc"))) + + seen = set() + for candidate in candidates: + if not candidate or candidate in seen: + continue + seen.add(candidate) + if os.path.exists(candidate): + return candidate + + if script_name: + return self.loader.find_script_file(script_name) or "" + + return "" + + def build_script_reference(self, script_name: str, script_file: str = "") -> Dict[str, Any]: + reference = {"name": script_name} + + resolved_script_path = script_file or self.loader.find_script_file(script_name) or "" + if resolved_script_path: + resolved_script_path = os.path.normpath(os.path.abspath(resolved_script_path)) + relative_path = self.get_script_relative_path(resolved_script_path) + if relative_path: + reference["project_relative_path"] = relative_path + reference["file"] = relative_path + else: + reference["file"] = resolved_script_path + + return reference + + def set_scripts_directory( + self, + directory: str, + *, + create: bool = True, + reload_scripts: bool = True, + ) -> str: + normalized_directory = self._normalize_scripts_directory(directory) + if normalized_directory == self.scripts_directory and ( + os.path.exists(normalized_directory) or not create + ): + return self.scripts_directory + + self.scripts_directory = normalized_directory + if create: + self._ensure_scripts_directory() + + self.loader.clear(unload_components=False) + self.script_templates.clear() + + if reload_scripts and os.path.exists(self.scripts_directory): + self.load_all_scripts_from_directory() + + print(f"✓ 当前脚本目录已切换到: {self.scripts_directory}") + return self.scripts_directory def _ensure_scripts_directory(self): """确保脚本目录存在""" @@ -550,6 +699,9 @@ class ExampleScript(ScriptBase): """创建新的脚本文件""" script_base_name = os.path.splitext(script_name.strip())[0] script_path = os.path.join(self.scripts_directory, f"{script_base_name}.py") + + if not os.path.exists(self.scripts_directory): + os.makedirs(self.scripts_directory) if os.path.exists(script_path): print(f"脚本文件已存在: {script_path}") @@ -670,19 +822,32 @@ class {class_name}(ScriptBase): """从目录加载所有脚本""" if directory is None: directory = self.scripts_directory + else: + directory = self._normalize_scripts_directory(directory) if not os.path.exists(directory): print(f"脚本目录不存在: {directory}") return [] loaded_scripts = [] - for filename in os.listdir(directory): - if filename.endswith('.py') and not filename.startswith('__'): - script_path = os.path.join(directory, filename) - script_class = self.load_script_from_file(script_path) - if script_class: - script_name = os.path.splitext(filename)[0] - loaded_scripts.append(script_name) + seen_script_names = set() + for filename in sorted(os.listdir(directory)): + if filename.startswith('__') or not filename.endswith(('.py', '.pyc')): + continue + + script_name = os.path.splitext(filename)[0] + if script_name in seen_script_names: + continue + + preferred_path = os.path.join(directory, f"{script_name}.py") + script_path = preferred_path if os.path.exists(preferred_path) else os.path.join(directory, filename) + if not os.path.exists(script_path): + continue + + script_class = self.load_script_from_file(script_path) + if script_class: + loaded_scripts.append(script_name) + seen_script_names.add(script_name) print(f"✓ 从目录 {directory} 加载了 {len(loaded_scripts)} 个脚本") return loaded_scripts @@ -773,13 +938,8 @@ class {class_name}(ScriptBase): script_info_list = [] for script_component in remaining_scripts: script_name = script_component.script_name - script_class = script_component.script_instance.__class__ script_file = self.loader.find_script_file(script_name) or "" - - script_info_list.append({ - "name": script_name, - "file": script_file - }) + script_info_list.append(self.build_script_reference(script_name, script_file)) import json game_object.setTag("has_scripts", "true") diff --git a/imgui.ini b/imgui.ini index 4f4f7d46..1587caed 100644 --- a/imgui.ini +++ b/imgui.ini @@ -31,19 +31,19 @@ DockId=0x0000000D,0 [Window][场景树] Pos=0,20 -Size=274,1012 +Size=274,1084 Collapsed=0 DockId=0x00000007,0 [Window][属性面板] -Pos=1527,20 -Size=393,1012 +Pos=1655,20 +Size=393,1084 Collapsed=0 DockId=0x00000002,0 [Window][控制台] -Pos=276,632 -Size=1249,400 +Pos=276,704 +Size=1377,400 Collapsed=0 DockId=0x00000006,1 @@ -60,7 +60,7 @@ Collapsed=0 [Window][WindowOverViewport_11111111] Pos=0,20 -Size=1920,1012 +Size=2048,1084 Collapsed=0 [Window][测试窗口1] @@ -84,12 +84,12 @@ Size=400,300 Collapsed=0 [Window][选择路径] -Pos=660,254 +Pos=724,302 Size=600,500 Collapsed=0 [Window][打开项目] -Pos=710,304 +Pos=774,352 Size=500,400 Collapsed=0 @@ -99,8 +99,8 @@ Size=600,500 Collapsed=0 [Window][资源管理器] -Pos=276,632 -Size=1249,400 +Pos=276,704 +Size=1377,400 Collapsed=0 DockId=0x00000006,0 @@ -150,10 +150,9 @@ Size=101,226 Collapsed=0 [Window][LUI编辑器] -Pos=1690,20 -Size=358,748 +Pos=1675,295 +Size=358,569 Collapsed=0 -DockId=0x00000005,2 [Window][LUI测试控制面板] Pos=6,10 @@ -207,18 +206,23 @@ Collapsed=0 DockId=0x00000005,2 [Window][项目另存为] -Pos=794,432 +Pos=1050,555 Size=460,240 Collapsed=0 +[Window][打包项目] +Pos=784,442 +Size=480,220 +Collapsed=0 + [Docking][Data] -DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=1920,1012 Split=X +DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=2048,1084 Split=X DockNode ID=0x00000001 Parent=0x08BD597D SizeRef=1525,1012 Split=X DockNode ID=0x00000007 Parent=0x00000001 SizeRef=274,1084 Selected=0xE0015051 DockNode ID=0x00000008 Parent=0x00000001 SizeRef=1249,1084 Split=Y DockNode ID=0x00000005 Parent=0x00000008 SizeRef=2048,610 Split=Y DockNode ID=0x0000000D Parent=0x00000005 SizeRef=1318,383 HiddenTabBar=1 Selected=0x43A39006 DockNode ID=0x0000000E Parent=0x00000005 SizeRef=1318,363 CentralNode=1 Selected=0xE0015051 - DockNode ID=0x00000006 Parent=0x00000008 SizeRef=2048,400 Selected=0x3A2E05C3 + DockNode ID=0x00000006 Parent=0x00000008 SizeRef=2048,400 Selected=0x5428E753 DockNode ID=0x00000002 Parent=0x08BD597D SizeRef=393,1012 Selected=0x5DB6FF37 diff --git a/index_name.txt b/index_name.txt new file mode 100644 index 00000000..4e9d06bb --- /dev/null +++ b/index_name.txt @@ -0,0 +1 @@ +index-b2d982.boo diff --git a/main.py b/main.py index f5c38661..3c7299b7 100644 --- a/main.py +++ b/main.py @@ -383,9 +383,11 @@ class MyWorld(PanelDelegates, CoreWorld): self.show_new_project_dialog = False self.show_open_project_dialog = False self.show_save_as_dialog = False + self.show_build_project_dialog = False self.show_about_dialog = False self.save_as_project_name = "" self.save_as_project_path = "" + self.build_output_path = "" # 路径选择对话框状态 self.show_path_browser = False @@ -1003,6 +1005,7 @@ class MyWorld(PanelDelegates, CoreWorld): self._draw_new_project_dialog() self._draw_open_project_dialog() self._draw_save_as_project_dialog() + self._draw_build_project_dialog() self._draw_path_browser() self._draw_import_dialog() diff --git a/project/project_manager.py b/project/project_manager.py index 111a0056..58844321 100644 --- a/project/project_manager.py +++ b/project/project_manager.py @@ -4,6 +4,8 @@ import json import datetime import subprocess import shutil +import importlib.util +import py_compile class ProjectManager: @@ -13,6 +15,58 @@ class ProjectManager: self.project_config = None print("✓ 项目管理系统初始化完成") + + def _get_repo_root(self): + return os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + def get_project_scripts_dir(self, project_path=None): + project_path = os.path.normpath(project_path or self.current_project_path or "") + if not project_path: + return "" + return os.path.join(project_path, "scripts") + + def _ensure_project_scripts_dir(self, project_path): + scripts_dir = self.get_project_scripts_dir(project_path) + if scripts_dir: + os.makedirs(scripts_dir, exist_ok=True) + return scripts_dir + + def _sync_project_script_manager(self, project_path, reload_scripts=True): + script_manager = getattr(self.world, "script_manager", None) + if not script_manager: + return "" + + scripts_dir = self._ensure_project_scripts_dir(project_path) + script_manager.set_scripts_directory( + scripts_dir, + create=True, + reload_scripts=reload_scripts, + ) + return scripts_dir + + def _restore_project_context(self, project_path, project_config, reload_scripts=True): + normalized_project_path = os.path.normpath(project_path) if project_path else None + self.current_project_path = normalized_project_path + self.project_config = project_config + + script_manager = getattr(self.world, "script_manager", None) + if not script_manager: + return + + if normalized_project_path: + self._sync_project_script_manager(normalized_project_path, reload_scripts=reload_scripts) + else: + script_manager.set_scripts_directory( + "scripts", + create=True, + reload_scripts=reload_scripts, + ) + + def _sanitize_build_name(self, project_name): + invalid_chars = '<>:"/\\|?*' + safe_name = "".join("_" if char in invalid_chars else char for char in str(project_name or "").strip()) + safe_name = safe_name.rstrip(". ") + return safe_name or "EGProject" # ==================== 项目生命周期管理 ==================== @@ -41,6 +95,7 @@ class ProjectManager: os.makedirs(os.path.join(full_project_path, "textures")) # 贴图文件夹 scenes_path = os.path.join(full_project_path, "scenes") # 场景文件夹 os.makedirs(scenes_path) + self._ensure_project_scripts_dir(full_project_path) # 创建项目配置文件 project_config = { @@ -61,9 +116,14 @@ class ProjectManager: # 清空当前场景 self._clearCurrentScene() + # 更新项目状态,确保后续保存脚本元数据时能拿到项目路径 + self.current_project_path = full_project_path + self.project_config = project_config + self._sync_project_script_manager(full_project_path, reload_scripts=True) + # 自动保存初始场景 scene_file = os.path.join(scenes_path, "scene.bam") - if self.world.scene_manager.saveScene(scene_file, project_path): + if self.world.scene_manager.saveScene(scene_file, full_project_path): # 更新配置文件中的场景路径 project_config["scene_file"] = os.path.relpath(scene_file, full_project_path) with open(config_file, "w", encoding="utf-8") as f: @@ -71,10 +131,6 @@ class ProjectManager: print(f"初始场景已保存到: {scene_file}") else: print("初始场景保存失败") - - # 更新项目状态 - self.current_project_path = full_project_path - self.project_config = project_config print(f"项目 '{project_name}' 创建成功!") return True @@ -113,6 +169,13 @@ class ProjectManager: project_config = json.load(f) # print(f"[DEBUG] 项目配置读取成功: {project_config.get('name', 'Unknown')}") + previous_project_path = self.current_project_path + previous_project_config = self.project_config + + self.current_project_path = os.path.normpath(project_path) + self.project_config = project_config + self._sync_project_script_manager(self.current_project_path, reload_scripts=True) + # 检查场景文件 scene_file = os.path.join(project_path, "scenes", "scene.bam") # print(f"[DEBUG] 场景文件路径: {scene_file}") @@ -129,10 +192,20 @@ class ProjectManager: # 检查world的基本状态 if not hasattr(self.world, 'render') or self.world.render is None: # print(f"[DEBUG] 错误: world.render不存在或为None") + self._restore_project_context( + previous_project_path, + previous_project_config, + reload_scripts=True, + ) return False if not hasattr(self.world, 'loader') or self.world.loader is None: # print(f"[DEBUG] 错误: world.loader不存在或为None") + self._restore_project_context( + previous_project_path, + previous_project_config, + reload_scripts=True, + ) return False # 清理可能的残留状态 @@ -154,17 +227,32 @@ class ProjectManager: # print(f"[DEBUG] 验证BAM文件完整性...") if os.path.getsize(scene_file) == 0: # print(f"[DEBUG] 错误: BAM文件为空") + self._restore_project_context( + previous_project_path, + previous_project_config, + reload_scripts=True, + ) return False # 检查文件权限 if not os.access(scene_file, os.R_OK): # print(f"[DEBUG] 错误: BAM文件不可读") + self._restore_project_context( + previous_project_path, + previous_project_config, + reload_scripts=True, + ) return False # print(f"[DEBUG] BAM文件验证通过") except Exception as e: # print(f"[DEBUG] 清理状态时发生异常: {e}") + self._restore_project_context( + previous_project_path, + previous_project_config, + reload_scripts=True, + ) return False if self.world.scene_manager.loadScene(scene_file): @@ -173,6 +261,11 @@ class ProjectManager: project_config["scene_file"] = os.path.relpath(scene_file, project_path) else: # print(f"[DEBUG] 场景加载失败") + self._restore_project_context( + previous_project_path, + previous_project_config, + reload_scripts=True, + ) return False else: # print(f"[DEBUG] 场景文件不存在,跳过场景加载") @@ -183,7 +276,7 @@ class ProjectManager: json.dump(project_config, f, ensure_ascii=False, indent=4) # 更新项目状态 - self.current_project_path = project_path + self.current_project_path = os.path.normpath(project_path) self.project_config = project_config # print(f"[DEBUG] ===== 项目打开成功: {project_path} =====") @@ -191,6 +284,11 @@ class ProjectManager: return True except Exception as e: + self._restore_project_context( + locals().get("previous_project_path"), + locals().get("previous_project_config"), + reload_scripts=True, + ) # print(f"[DEBUG] ===== 项目打开失败 =====") print(f"加载项目时发生错误:{str(e)}") import traceback @@ -209,6 +307,10 @@ class ProjectManager: try: if not project_path: return False + + previous_project_path = self.current_project_path + previous_project_config = self.project_config + # 检查是否是有效的项目文件夹 config_file = os.path.join(project_path, "project.json") if not os.path.exists(config_file): @@ -219,6 +321,10 @@ class ProjectManager: with open(config_file, "r", encoding="utf-8") as f: project_config = json.load(f) + self.current_project_path = os.path.normpath(project_path) + self.project_config = project_config + self._sync_project_script_manager(self.current_project_path, reload_scripts=True) + # 检查场景文件 scene_file = os.path.join(project_path, "scenes", "scene.bam") if os.path.exists(scene_file): @@ -226,19 +332,31 @@ class ProjectManager: if self.world.scene_manager.loadScene(scene_file): # 更新项目配置 project_config["scene_file"] = os.path.relpath(scene_file, project_path) + else: + self._restore_project_context( + previous_project_path, + previous_project_config, + reload_scripts=True, + ) + return False project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") with open(config_file, "w", encoding="utf-8") as f: json.dump(project_config, f, ensure_ascii=False, indent=4) # 更新项目状态 - self.current_project_path = project_path + self.current_project_path = os.path.normpath(project_path) self.project_config = project_config print(f"项目 '{project_path}' 加载成功!") return True except Exception as e: + self._restore_project_context( + locals().get("previous_project_path"), + locals().get("previous_project_config"), + reload_scripts=True, + ) error_msg = f"加载项目时发生错误:{str(e)}" print(error_msg) return False @@ -331,104 +449,270 @@ class ProjectManager: # ==================== 项目打包功能 ==================== def buildPackage(self, build_dir): - """打包项目为可执行文件 - 按照Panda3D官方标准方法 - - Args: - build_dir: 打包输出目录 - - Returns: - bool: 打包是否成功 - """ + """将当前项目打包为最终运行程序。""" try: - # 检查是否有当前项目路径 if not self.current_project_path: print("错误: 请先创建或打开一个项目!") return False - - project_path = self.current_project_path - scenes_path = os.path.join(project_path, "scenes") - scene_file = os.path.join(scenes_path, "scene.bam") - - # 检查场景文件是否存在 - if not os.path.exists(scene_file): - print("错误: 请先保存场景!") - return False - if hasattr(self.world,'selection') and self.world.selection: - self.world.selection.clearSelection() - print("已取消场景中的物体选中状态") - - if self.world.scene_manager.saveScene(scene_file, project_path): - print("场景保存成功") - config_file = os.path.join(project_path,"project.json") - if os.path.exists(config_file): - with open(config_file,"r",encoding="utf-8") as f: - project_config = json.load(f) - - project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - project_config["scene_file"] = os.path.relpath(scene_file,project_path) - - with open(config_file,"w",encoding = "utf-8") as f: - json.dump(project_config,f,ensure_ascii = False,indent=4) - - self.project_config = project_config - - project_name = os.path.basename(project_path) - else: - print("错误: 保存场景失败!") - return False + project_path = os.path.normpath(self.current_project_path) + project_display_name = ( + (self.project_config or {}).get("name") + or os.path.basename(project_path) + or "EGProject" + ) + project_name = self._sanitize_build_name(project_display_name) if not build_dir: print("错误: 请指定打包输出目录!") return False - - # 创建构建目录 - build_dir = os.path.join(build_dir, "build") - if not os.path.exists(build_dir): - os.makedirs(build_dir) - - # 创建标准的打包文件 - self._createStandardBuildFiles(build_dir, project_path, scene_file) - - # 执行打包命令 - success = self._executeStandardBuild(build_dir) - - if success: - # 根据操作系统创建对应的启动脚本文件 - try: - import stat - # 检查操作系统类型 - if os.name == 'nt': # Windows系统 - run_bat_path = os.path.join(build_dir, "run.bat") - with open(run_bat_path, "w") as f: - f.write("@echo off\n") - f.write("python Start_Run.py %*\n") - else: # Unix-like系统 (Linux, macOS等) - run_sh_path = os.path.join(build_dir, "run.sh") - with open(run_sh_path, "w") as f: - f.write("#!/bin/bash\n") - f.write("python3.11 Start_Run.py \"$@\"\n") - - # 为Unix-like系统添加执行权限 - st = os.stat(run_sh_path) - os.chmod(run_sh_path, st.st_mode | stat.S_IEXEC) - except Exception as e: - print(f"创建启动脚本文件失败: {str(e)}") - - print("打包完成!可执行文件在 build 目录中。") - return True - else: + scene_file = os.path.join(project_path, "scenes", "scene.bam") + if not os.path.exists(scene_file): + print("错误: 请先保存场景!") return False + if hasattr(self.world, "selection") and self.world.selection: + self.world.selection.clearSelection() + print("已取消场景中的物体选中状态") + + self._sync_project_script_manager(project_path, reload_scripts=True) + + if not self.saveProject(): + print("错误: 保存场景失败!") + return False + + output_root = os.path.normpath(build_dir) + staging_root = os.path.join(output_root, f".{project_name}_staging") + source_root = os.path.join(staging_root, "source") + assets_root = os.path.join(staging_root, "assets") + dist_dir = os.path.join(output_root, project_name) + + if os.path.exists(staging_root): + shutil.rmtree(staging_root) + os.makedirs(source_root, exist_ok=True) + os.makedirs(assets_root, exist_ok=True) + + if os.path.exists(dist_dir): + shutil.rmtree(dist_dir) + + shutil.copy2(scene_file, os.path.join(assets_root, "scene.bam")) + self.copy_folder(os.path.join(project_path, "scenes", "resources"), assets_root) + self._merge_folder_contents( + os.path.join(project_path, "Resources"), + os.path.join(assets_root, "resources"), + ) + if not self._saveGUIElementsToJSON(assets_root, project_path): + print("错误: 导出 GUI 数据失败!") + return False + self._saveCameraStateToJSON(assets_root) + self._copyScriptsToBuild(assets_root, project_path) + self._copyRenderPipelineRuntime(assets_root) + + runtime_entry = self._createProjectRuntimeEntry(source_root, project_display_name) + if not runtime_entry: + print("错误: 生成项目运行时入口失败") + return False + + success = self._executeProjectBuild( + source_root=source_root, + assets_root=assets_root, + build_root=output_root, + project_name=project_name, + ) + if not success: + return False + + if os.path.exists(staging_root): + shutil.rmtree(staging_root, ignore_errors=True) + + exe_path = os.path.join(dist_dir, f"{project_name}.exe") + print(f"打包完成!项目运行程序: {exe_path}") + return True + except Exception as e: print(f"打包过程出错:{str(e)}") return False - + + def _merge_folder_contents(self, source_folder, destination_folder): + """将 source_folder 的内容合并到 destination_folder。""" + if not source_folder or not os.path.exists(source_folder): + return False + + os.makedirs(destination_folder, exist_ok=True) + for root, _, files in os.walk(source_folder): + rel_root = os.path.relpath(root, source_folder) + target_root = destination_folder if rel_root == "." else os.path.join(destination_folder, rel_root) + os.makedirs(target_root, exist_ok=True) + for file_name in files: + source_file = os.path.join(root, file_name) + target_file = os.path.join(target_root, file_name) + shutil.copy2(source_file, target_file) + return True + + def _load_runtime_template(self): + template_path = os.path.join(self._get_repo_root(), "templates", "main_template.py") + with open(template_path, "r", encoding="utf-8") as f: + return f.read() + + def _createProjectRuntimeEntry(self, source_root, project_name): + try: + template_content = self._load_runtime_template() + safe_project_name = project_name.replace("\\", "\\\\").replace('"', '\\"') + runtime_source = template_content.replace("__EG_PROJECT_NAME__", safe_project_name) + runtime_entry = os.path.join(source_root, "main.py") + with open(runtime_entry, "w", encoding="utf-8") as f: + f.write(runtime_source) + return runtime_entry + except Exception as e: + print(f"创建项目运行时入口失败: {e}") + return "" + + def _executeProjectBuild(self, source_root, assets_root, build_root, project_name): + repo_root = self._get_repo_root() + work_root = os.path.join(build_root, f".{project_name}_work") + spec_root = os.path.join(build_root, f".{project_name}_spec") + + for directory in (work_root, spec_root): + if os.path.exists(directory): + shutil.rmtree(directory) + os.makedirs(directory, exist_ok=True) + + pyinstaller_runner = None + pyinstaller_candidates = [ + [sys.executable, "-m", "PyInstaller"], + [sys.executable, "-m", "pyinstaller"], + ["pyinstaller"], + ] + for candidate in pyinstaller_candidates: + pyinstaller_probe = subprocess.run( + [*candidate, "--version"], + capture_output=True, + text=True, + check=False, + ) + if pyinstaller_probe.returncode == 0: + pyinstaller_runner = candidate + break + + if not pyinstaller_runner: + print("错误: 当前环境未安装 PyInstaller,无法执行项目打包。") + return False + + sep = ";" if os.name == "nt" else ":" + command = [ + *pyinstaller_runner, + "--noconfirm", + "--clean", + "--windowed", + "--name", + project_name, + "--distpath", + build_root, + "--workpath", + work_root, + "--specpath", + spec_root, + "--paths", + source_root, + "--paths", + repo_root, + "--paths", + os.path.join(repo_root, "RenderPipelineFile"), + "--exclude-module", + "imgui_bundle", + "--exclude-module", + "p3dimgui", + "--hidden-import", + "core", + "--hidden-import", + "core.script_system", + "--hidden-import", + "core.CustomMouseController", + "--hidden-import", + "ssbo_component.runtime_importer", + "--hidden-import", + "ssbo_component.ssbo_controller", + "--hidden-import", + "rpcore", + "--collect-submodules", + "rpcore", + "--collect-submodules", + "rpplugins", + "--collect-submodules", + "rplibs", + ] + + panda_binary_dir = self._get_panda3d_binary_dir() + panda_display_binaries = [ + "libpandagl.dll", + "libp3windisplay.dll", + "libpandadx9.dll", + "libp3tinydisplay.dll", + "cgGL.dll", + "cgD3D9.dll", + "d3dx9_43.dll", + ] + if panda_binary_dir: + for binary_name in panda_display_binaries: + binary_path = os.path.join(panda_binary_dir, binary_name) + if os.path.exists(binary_path): + command.extend(["--add-binary", f"{binary_path}{sep}."]) + + data_mappings = [ + (os.path.join(assets_root, "scene.bam"), "."), + (os.path.join(assets_root, "camera_state.json"), "."), + (os.path.join(assets_root, "gui"), "gui"), + (os.path.join(assets_root, "resources"), "resources"), + (os.path.join(assets_root, "scripts"), "scripts"), + (os.path.join(assets_root, "RenderPipelineFile"), "RenderPipelineFile"), + ] + + for source_path, target_path in data_mappings: + if os.path.exists(source_path): + command.extend(["--add-data", f"{source_path}{sep}{target_path}"]) + + command.append(os.path.join(source_root, "main.py")) + + result = subprocess.run( + command, + cwd=source_root, + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + print("项目打包失败。") + output = (result.stdout or "") + "\n" + (result.stderr or "") + print(output[-4000:]) + return False + + for directory in (work_root, spec_root): + if os.path.exists(directory): + shutil.rmtree(directory, ignore_errors=True) + + print(f"✓ 项目已打包到: {os.path.join(build_root, project_name)}") + return True + + def _get_panda3d_binary_dir(self): + try: + panda3d_spec = importlib.util.find_spec("panda3d.core") + if panda3d_spec and panda3d_spec.origin: + panda_package_dir = os.path.dirname(os.path.dirname(os.path.abspath(panda3d_spec.origin))) + candidate_dirs = [ + os.path.join(panda_package_dir, "bin"), + os.path.join(os.path.dirname(panda_package_dir), "bin"), + ] + for candidate_dir in candidate_dirs: + if os.path.isdir(candidate_dir): + return candidate_dir + except Exception as e: + print(f"⚠️ 获取 Panda3D 二进制目录失败: {e}") + return "" + def _createStandardBuildFiles(self, build_dir, project_path, scene_file): """创建标准的Panda3D打包文件""" project_name = os.path.basename(project_path) - # 确保构建目录存在 if not os.path.exists(build_dir): os.makedirs(build_dir) @@ -472,38 +756,108 @@ class ProjectManager: self._createRequirementsFile(build_dir) def _copyScriptsToBuild(self, build_dir, project_path): - """复制脚本文件到构建目录的scripts文件夹""" + """将项目脚本编译后复制到构建目录,避免直接暴露源码。""" try: - # 创建目标scripts目录 scripts_dest = os.path.join(build_dir, "scripts") - - # 正确的源scripts目录路径 - scripts_src = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "scripts") - - # 如果上面的路径不存在,尝试项目路径下的scripts目录 - if not os.path.exists(scripts_src): - scripts_src = os.path.join(project_path, "scripts") + scripts_src = self.get_project_scripts_dir(project_path) + if os.path.exists(scripts_dest): + shutil.rmtree(scripts_dest) + os.makedirs(scripts_dest, exist_ok=True) if os.path.exists(scripts_src): - # 直接复制整个scripts目录 - if os.path.exists(scripts_dest): - shutil.rmtree(scripts_dest) + compiled_count = 0 + fallback_count = 0 + for root, _, files in os.walk(scripts_src): + rel_root = os.path.relpath(root, scripts_src) + target_root = scripts_dest if rel_root == "." else os.path.join(scripts_dest, rel_root) + os.makedirs(target_root, exist_ok=True) - shutil.copytree( - scripts_src, - scripts_dest, - ignore=shutil.ignore_patterns('__pycache__', '*.pyc', '.git', '.vscode', '*.log') - ) - print("✓ Scripts目录已复制到build/scripts") + for file_name in files: + if file_name.startswith("__"): + continue + + source_file = os.path.join(root, file_name) + if file_name.endswith(".py"): + compiled_file = os.path.join( + target_root, + f"{os.path.splitext(file_name)[0]}.pyc", + ) + try: + py_compile.compile(source_file, cfile=compiled_file, doraise=True) + compiled_count += 1 + except py_compile.PyCompileError as e: + fallback_file = os.path.join(target_root, file_name) + shutil.copy2(source_file, fallback_file) + fallback_count += 1 + print(f"⚠️ 编译脚本失败,已回退复制源码: {source_file} -> {fallback_file} ({e})") + elif not file_name.endswith((".pyc", ".pyo", ".log")): + shutil.copy2(source_file, os.path.join(target_root, file_name)) + + print(f"✓ Scripts已导出到 build/scripts,编译 {compiled_count} 个脚本") + if fallback_count: + print(f"⚠️ {fallback_count} 个脚本因编译失败保留为 .py 源码") else: - # 创建空的scripts目录 - if not os.path.exists(scripts_dest): - os.makedirs(scripts_dest) print("⚠️ 项目中没有scripts目录") except Exception as e: print(f"⚠️ 复制脚本文件时出错: {str(e)}") + def _copyRenderPipelineRuntime(self, build_dir): + """复制运行时需要的 RenderPipeline 资源,并将源码编译为 pyc。""" + try: + source_root = os.path.join(self._get_repo_root(), "RenderPipelineFile") + target_root = os.path.join(build_dir, "RenderPipelineFile") + + if os.path.exists(target_root): + shutil.rmtree(target_root) + + if not os.path.exists(source_root): + print("⚠️ RenderPipelineFile 文件夹未找到") + return False + + blocked_dirs = {"__pycache__", ".git", ".vscode", "samples", "toolkit"} + blocked_root_files = {"setup.py", "start_daytime_editor.py", "start_plugin_configurator.py"} + blocked_relative_dirs = { + os.path.normpath(os.path.join("rplibs", "yaml", "yaml_py2")), + } + + for root, dirs, files in os.walk(source_root): + relative_root = os.path.relpath(root, source_root) + normalized_relative_root = os.path.normpath(relative_root) + dirs[:] = [ + name for name in dirs + if name not in blocked_dirs + and os.path.normpath(os.path.join(normalized_relative_root, name)) not in blocked_relative_dirs + ] + target_dir = target_root if relative_root == "." else os.path.join(target_root, relative_root) + os.makedirs(target_dir, exist_ok=True) + + for file_name in files: + if file_name.endswith((".pyc", ".pyo", ".log")): + continue + if relative_root == "." and file_name in blocked_root_files: + continue + + source_file = os.path.join(root, file_name) + if file_name.endswith(".py"): + compiled_file = os.path.join( + target_dir, + f"{os.path.splitext(file_name)[0]}.pyc", + ) + try: + py_compile.compile(source_file, cfile=compiled_file, doraise=True) + except py_compile.PyCompileError as e: + print(f"⚠️ RenderPipeline 脚本编译失败,保留源码: {source_file} ({e})") + shutil.copy2(source_file, os.path.join(target_dir, file_name)) + else: + shutil.copy2(source_file, os.path.join(target_dir, file_name)) + + print("✓ RenderPipeline 运行时资源已导出") + return True + except Exception as e: + print(f"⚠️ 导出 RenderPipeline 运行时资源失败: {e}") + return False + def _copyScriptSystemToBuild(self,build_dir): core_files = [ "script_system.py", @@ -559,22 +913,90 @@ class ProjectManager: traceback.print_exc() return False + def _saveCameraStateToJSON(self, build_dir): + """保存当前主相机状态,供打包运行时恢复。""" + try: + camera = getattr(self.world, "camera", None) or getattr(self.world, "cam", None) + if not camera or camera.isEmpty(): + return False + + camera_state = { + "position": list(camera.getPos()), + "rotation": list(camera.getHpr()), + "camera_control_enabled": bool(getattr(self.world, "camera_control_enabled", True)), + } + + camera_state_path = os.path.join(build_dir, "camera_state.json") + with open(camera_state_path, "w", encoding="utf-8") as f: + json.dump(camera_state, f, ensure_ascii=False, indent=4) + + print(f"✓ 主相机状态已保存到 {camera_state_path}") + return True + except Exception as e: + print(f"⚠️ 保存主相机状态失败: {e}") + return False + def _updateResourcePaths(self,gui_info,project_path,build_dir): + project_path = os.path.normpath(project_path) + project_resources_dir = os.path.join(project_path, "Resources") + scene_resources_dir = os.path.join(project_path, "scenes", "resources") + def normalize_resource_path(old_path): if not old_path: return old_path - if old_path.startswith(("http://","https://")): + old_path = str(old_path).strip() + if not old_path: return old_path - if os.path.isabs(old_path): - try: - rel_path = os.path.relpath(old_path,project_path) - return os.path.join("resources",os.path.basename(rel_path)).replace("\\","/") - except Exception: - return os.path.join("resources",os.path.basename(old_path)).replace("\\","/") + if old_path.startswith(("http://", "https://")): + return old_path + + normalized_path = os.path.normpath(old_path.replace("/", os.sep)) + if normalized_path == "resources" or normalized_path.startswith(f"resources{os.sep}"): + return normalized_path.replace("\\", "/") + + candidate_paths = [] + if os.path.isabs(normalized_path): + candidate_paths.append(normalized_path) else: - return os.path.join("resources",os.path.basename(old_path)).replace("\\","/") + candidate_paths.extend( + [ + os.path.join(project_resources_dir, normalized_path), + os.path.join(scene_resources_dir, normalized_path), + os.path.join(project_path, normalized_path), + ] + ) + + resolved_path = "" + for candidate in candidate_paths: + if os.path.exists(candidate): + resolved_path = os.path.normpath(candidate) + break + + if not resolved_path and os.path.isabs(normalized_path): + resolved_path = normalized_path + + if resolved_path: + for base_dir in (project_resources_dir, scene_resources_dir): + try: + if os.path.exists(base_dir) and os.path.commonpath([resolved_path, base_dir]) == os.path.normpath(base_dir): + relative_path = os.path.relpath(resolved_path, base_dir) + return os.path.join("resources", relative_path).replace("\\", "/") + except ValueError: + continue + + try: + relative_path = os.path.relpath(resolved_path, project_path) + if not relative_path.startswith(".."): + return os.path.join("resources", relative_path).replace("\\", "/") + except ValueError: + pass + + return os.path.join("resources", os.path.basename(resolved_path)).replace("\\", "/") + + return os.path.join("resources", os.path.basename(normalized_path)).replace("\\", "/") + if "image_path" in gui_info and gui_info["image_path"]: gui_info["image_path"] = normalize_resource_path(gui_info["image_path"]) if "video_path" in gui_info and gui_info["video_path"]: diff --git a/scene/scene_manager_io_mixin.py b/scene/scene_manager_io_mixin.py index 0e15dae1..a67e0848 100644 --- a/scene/scene_manager_io_mixin.py +++ b/scene/scene_manager_io_mixin.py @@ -191,16 +191,12 @@ class SceneManagerIOMixin: for script_component in scripts: try: script_name = script_component.script_name - # 获取脚本路径 - script_class = script_component.script_instance.__class__ - script_file = self._get_script_file_path(script_class, script_name) + script_file = script_manager.loader.find_script_file(script_name) or "" + script_reference = script_manager.build_script_reference(script_name, script_file) # 只有当脚本文件存在时才保存 - if script_file and os.path.exists(script_file): - gui_info["scripts"].append({ - "name": script_name, - "file": script_file - }) - print(f"收集脚本信息: {script_name} from {script_file}") + if script_reference.get("file") or script_reference.get("project_relative_path"): + gui_info["scripts"].append(script_reference) + print(f"收集脚本信息: {script_name} from {script_reference.get('file', '')}") else: print(f"警告: 脚本文件不存在: {script_file}") except Exception as e: @@ -419,15 +415,10 @@ class SceneManagerIOMixin: for script_component in scripts: script_name = script_component.script_name print(f"保存脚本信息: {script_name}") - - # 获取脚本类的文件路径 - script_class = script_component.script_instance.__class__ - script_file = self._get_script_file_path(script_class, script_name) - - script_info_list.append({ - "name": script_name, - "file": script_file - }) + script_file = script_manager.loader.find_script_file(script_name) or "" + script_info_list.append( + script_manager.build_script_reference(script_name, script_file) + ) # 将脚本信息保存为JSON字符串 import json @@ -955,7 +946,7 @@ class SceneManagerIOMixin: script_manager = self.world.script_manager for script_info in scripts_info: script_name = script_info["name"] - script_file = script_info.get("file","") + script_file = script_manager.resolve_script_path(script_info) print(f"尝试重新挂载脚本{script_name}from {script_file}") @@ -1272,14 +1263,14 @@ class SceneManagerIOMixin: if os.path.exists(scripts_dir): # 首先精确匹配 for file_name in os.listdir(scripts_dir): - if file_name.endswith('.py'): + if file_name.endswith(('.py', '.pyc')): base_name = os.path.splitext(file_name)[0] if base_name == script_name: return os.path.join(scripts_dir, file_name) # 如果没有精确匹配,尝试模糊匹配 for file_name in os.listdir(scripts_dir): - if file_name.endswith('.py'): + if file_name.endswith(('.py', '.pyc')): base_name = os.path.splitext(file_name)[0] if script_name.lower() in base_name.lower() or base_name.lower() in script_name.lower(): return os.path.join(scripts_dir, file_name) diff --git a/ssbo_component/__init__.py b/ssbo_component/__init__.py new file mode 100644 index 00000000..a8c789a6 --- /dev/null +++ b/ssbo_component/__init__.py @@ -0,0 +1,2 @@ +"""SSBO runtime helpers and editor components.""" + diff --git a/ssbo_component/runtime_importer.py b/ssbo_component/runtime_importer.py new file mode 100644 index 00000000..f9f2d345 --- /dev/null +++ b/ssbo_component/runtime_importer.py @@ -0,0 +1,356 @@ +import os + +from panda3d.core import Filename, GeomNode, Material, MaterialAttrib, NodePath + +from .ssbo_controller import ObjectController + + +class RuntimeSSBOSceneImporter: + """Minimal runtime-only SSBO scene importer without editor UI dependencies.""" + + def __init__(self, base_app): + self.base = base_app + self.controller = None + self.model = None + self.source_model = None + self.source_model_root = None + self.static_scene_root = None + self.interactive_scene_root = None + self.interactive_nodes = {} + + def load_scene(self, scene_path, interactive_root_names=None): + source_model = self._load_source_model_from_path(scene_path) + self._clear_runtime_state() + + source_root = self._ensure_source_model_root() + imported_root = source_model.copyTo(source_root) + self._set_node_name(imported_root, os.path.basename(scene_path) or "scene.bam") + self.source_model = imported_root + interactive_root_names = {str(name).strip() for name in (interactive_root_names or []) if str(name).strip()} + + top_level_children = self._get_scene_top_level_children(imported_root) + if interactive_root_names and top_level_children: + self._build_release_split_scene(top_level_children, interactive_root_names) + return self.interactive_scene_root or self.static_scene_root + + working_holder = NodePath("ssbo_source_scene_work") + working_root = source_root.copyTo(working_holder) + + self.controller = ObjectController() + self.controller.bake_ids_and_collect(working_root) + self.model = self.controller.model + self.model.reparentTo(self.base.render) + + try: + if not working_holder.isEmpty(): + working_holder.removeNode() + except Exception: + pass + + return self.model + + def _clear_runtime_state(self): + for node in (self.model, self.static_scene_root, self.interactive_scene_root, self.source_model_root): + try: + if node and not node.isEmpty(): + node.removeNode() + except Exception: + continue + self.controller = None + self.model = None + self.source_model = None + self.source_model_root = None + self.static_scene_root = None + self.interactive_scene_root = None + self.interactive_nodes = {} + + def get_runtime_node_for_name(self, node_name): + return self.interactive_nodes.get(str(node_name or "").strip()) + + def _get_scene_top_level_children(self, imported_root): + children = [] + try: + for child in imported_root.getChildren(): + if child and not child.isEmpty(): + children.append(child) + except Exception: + children = [] + + if children: + return children + return [imported_root] if imported_root and not imported_root.isEmpty() else [] + + def _build_release_split_scene(self, top_level_children, interactive_root_names): + static_holder = NodePath("runtime_static_scene") + interactive_holder = NodePath("runtime_interactive_scene") + interactive_count = 0 + static_count = 0 + + for child in top_level_children: + child_name = self._get_node_name(child, "") + if child_name in interactive_root_names: + runtime_child = child.copyTo(interactive_holder) + self._set_node_name(runtime_child, child_name) + self.interactive_nodes[child_name] = runtime_child + interactive_count += 1 + else: + child.copyTo(static_holder) + static_count += 1 + + if static_count: + self.static_scene_root = static_holder + self.static_scene_root.reparentTo(self.base.render) + try: + self.static_scene_root.flattenStrong() + except Exception as e: + print(f"静态场景合批失败,继续使用未合批结果: {e}") + else: + try: + if not static_holder.isEmpty(): + static_holder.removeNode() + except Exception: + pass + + if interactive_count: + self.interactive_scene_root = interactive_holder + self.interactive_scene_root.reparentTo(self.base.render) + else: + try: + if not interactive_holder.isEmpty(): + interactive_holder.removeNode() + except Exception: + pass + + print(f"[RuntimeSSBO] 运行时分层导入完成: 交互对象 {interactive_count} 个, 静态合批对象 {static_count} 个") + + def _load_source_model_from_path(self, model_path): + source_model = None + last_error = None + for fn in self._build_filename_candidates(model_path): + try: + source_model = self.base.loader.loadModel(fn) + if source_model and not source_model.isEmpty(): + break + except Exception as e: + last_error = e + source_model = None + + if not source_model or source_model.isEmpty(): + if last_error: + raise RuntimeError(f"Failed to load model '{model_path}': {last_error}") + raise RuntimeError(f"Failed to load model '{model_path}'") + + self._fix_black_materials(source_model) + self._repair_missing_textures(source_model, model_path) + return source_model + + def _build_filename_candidates(self, path_text): + candidates = [] + seen = set() + for ctor_name in ("fromOsSpecificW", "from_os_specific_w", "fromOsSpecific", "from_os_specific"): + ctor = getattr(Filename, ctor_name, None) + if not ctor: + continue + try: + fn = ctor(path_text) + key = fn.getFullpath() if hasattr(fn, "getFullpath") else fn.get_fullpath() + if key in seen: + continue + seen.add(key) + candidates.append(fn) + except Exception: + continue + if not candidates: + try: + candidates.append(Filename(path_text)) + except Exception: + pass + return candidates + + def _ensure_source_model_root(self): + root = self.source_model_root + if root and not root.isEmpty(): + return root + self.source_model_root = NodePath("ssbo_source_scene_root") + return self.source_model_root + + def _set_node_name(self, node, name): + if not node: + return + try: + node.set_name(name) + return + except Exception: + pass + try: + node.setName(name) + except Exception: + pass + + def _get_node_name(self, node, default_name=None): + if not node: + return default_name + try: + return node.get_name() + except Exception: + pass + try: + return node.getName() + except Exception: + return default_name + + def _fix_black_materials(self, model): + try: + for geom_path in model.findAllMatches("**/+GeomNode"): + geom_node = geom_path.node() + node_state = geom_path.getState() + if node_state.hasAttrib(MaterialAttrib.getClassType()): + mat_attrib = node_state.getAttrib(MaterialAttrib.getClassType()) + mat = mat_attrib.getMaterial() + if mat and self._is_dark_material(mat): + new_mat = Material(mat) + new_mat.setBaseColor((0.8, 0.8, 0.8, 1.0)) + new_mat.setDiffuse((0.8, 0.8, 0.8, 1.0)) + geom_path.setState(node_state.setAttrib(MaterialAttrib.make(new_mat))) + + for i in range(geom_node.getNumGeoms()): + geom_state = geom_node.getGeomState(i) + if geom_state.hasAttrib(MaterialAttrib.getClassType()): + mat_attrib = geom_state.getAttrib(MaterialAttrib.getClassType()) + mat = mat_attrib.getMaterial() + if mat and self._is_dark_material(mat): + new_mat = Material(mat) + new_mat.setBaseColor((0.8, 0.8, 0.8, 1.0)) + new_mat.setDiffuse((0.8, 0.8, 0.8, 1.0)) + geom_node.setGeomState(i, geom_state.setAttrib(MaterialAttrib.make(new_mat))) + else: + new_mat = Material() + new_mat.setBaseColor((0.8, 0.8, 0.8, 1.0)) + new_mat.setDiffuse((0.8, 0.8, 0.8, 1.0)) + new_mat.setSpecular((0.2, 0.2, 0.2, 1.0)) + new_mat.setRoughness(0.8) + geom_node.setGeomState(i, geom_state.addAttrib(MaterialAttrib.make(new_mat))) + model.clearColor() + except Exception as e: + print(f"修复黑色模型材质时出错: {e}") + + def _is_dark_material(self, material): + try: + if material.hasBaseColor(): + c = material.getBaseColor() + elif material.hasDiffuse(): + c = material.getDiffuse() + else: + return True + return c.x <= 0.05 and c.y <= 0.05 and c.z <= 0.05 + except Exception: + return False + + def _repair_missing_textures(self, model_np, model_path): + if not model_np or model_np.isEmpty(): + return + + search_dirs = self._build_texture_search_dirs(model_path) + texture_index = self._index_texture_files(search_dirs) + + for node in [model_np] + list(model_np.findAllMatches("**")): + if not node or node.isEmpty(): + continue + try: + stages = node.findAllTextureStages() + stage_count = stages.getNumTextureStages() + stage_at = stages.getTextureStage + except Exception: + continue + + for i in range(stage_count): + stage = stage_at(i) + if not stage: + continue + try: + tex = node.getTexture(stage) + except Exception: + tex = None + if not tex or self._texture_is_valid(tex): + continue + + basename = self._extract_texture_basename(tex) + if not basename: + continue + + replacement = texture_index.get(basename.lower()) + if replacement: + new_tex = self._load_texture_from_path(replacement) + if new_tex: + try: + node.setTexture(stage, new_tex, 1) + continue + except Exception: + pass + try: + node.clearTexture(stage) + except Exception: + pass + + def _build_texture_search_dirs(self, model_path): + dirs = [] + model_dir = os.path.dirname(os.path.abspath(model_path)) + project_root = getattr(self.base, "project_path", "") or os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + def add_dir(path): + if path and os.path.isdir(path): + normalized = os.path.normpath(path) + if normalized not in dirs: + dirs.append(normalized) + + add_dir(model_dir) + for sub in ("textures", "texture", "tex", "assets", "materials"): + add_dir(os.path.join(model_dir, sub)) + add_dir(os.path.join(project_root, "resources", sub)) + add_dir(os.path.join(project_root, "resources")) + return dirs + + def _index_texture_files(self, dirs, limit=30000): + texture_exts = {".png", ".jpg", ".jpeg", ".tga", ".bmp", ".dds", ".ktx", ".ktx2", ".webp"} + index = {} + scanned = 0 + for root_dir in dirs: + try: + for root, _, files in os.walk(root_dir): + for filename in files: + if os.path.splitext(filename)[1].lower() not in texture_exts: + continue + index.setdefault(filename.lower(), os.path.join(root, filename)) + scanned += 1 + if scanned >= limit: + return index + except Exception: + continue + return index + + def _load_texture_from_path(self, texture_path): + for fn in self._build_filename_candidates(texture_path): + try: + tex = self.base.loader.loadTexture(fn) + if tex: + return tex + except Exception: + continue + return None + + def _texture_is_valid(self, texture): + try: + x_size = texture.getXSize() + y_size = texture.getYSize() + return bool(x_size and y_size) + except Exception: + return False + + def _extract_texture_basename(self, texture): + try: + path = texture.getFilename().toOsSpecific() + except Exception: + path = "" + if not path: + return "" + return os.path.basename(path) diff --git a/templates/main_template.py b/templates/main_template.py index cb226813..0de26220 100644 --- a/templates/main_template.py +++ b/templates/main_template.py @@ -1,1566 +1,820 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -打包 -""" - -from __future__ import print_function - -import json - -from direct.actor.Actor import Actor -from direct.showbase.ShowBaseGlobal import globalClock -from panda3d.core import TextNode, CardMaker, TextureStage, NodePath, Texture, TransparencyAttrib, CollisionTraverser, \ - Point3 -# 获取渲染管线路径 -# 在文件开头添加sys导入(如果还没有的话) -import sys -import os - -# 修改工作目录设置部分 -if getattr(sys, 'frozen', False): - # 打包后的环境 - project_root = os.path.dirname(sys.executable) -else: - # 开发环境 - try: - project_root = os.path.dirname(os.path.abspath(__file__)) - except NameError: - project_root = os.getcwd() - -os.chdir(project_root) - -render_pipeline_path = 'RenderPipelineFile' -sys.path.insert(0, render_pipeline_path) - -# 改进路径设置逻辑 -pipeline_path = os.path.join(project_root, "RenderPipelineFile") -if os.path.exists(pipeline_path) and pipeline_path not in sys.path: - sys.path.insert(0, pipeline_path) - # 同时添加子目录以确保所有模块都能正确导入 - for root, dirs, files in os.walk(pipeline_path): - if root not in sys.path: - sys.path.insert(0, root) - - -import math -from random import random, randint, seed -from panda3d.core import Vec3, load_prc_file_data, Filename -from direct.showbase.ShowBase import ShowBase -from direct.task.TaskManagerGlobal import taskMgr - -# os.chdir(os.path.dirname(os.path.realpath(__file__))) -from core.script_system import ScriptManager -from core.CustomMouseController import CustomMouseController -from panda3d.core import CollisionTraverser - -class MainApp(ShowBase): - def __init__(self): - # 在调用父类构造函数前确保必要的属性存在 - if not hasattr(self, 'appRunner'): - self.appRunner = None - - load_prc_file_data("", """ - win-size 1380 750 - window-title - support-threads #t - """) - - # 简化 sys.path 设置逻辑 - pipeline_path = os.path.join(project_root, "RenderPipelineFile") - if os.path.exists(pipeline_path): - if pipeline_path not in sys.path: - sys.path.insert(0, pipeline_path) - else: - print(f"错误: 找不到渲染管线目录: {pipeline_path}") - return - - try: - from rpcore import RenderPipeline - self.render_pipeline = RenderPipeline() - self.render_pipeline.create(self) - - except ImportError as e: - - print(f"导入RenderPipeline模块失败: {e}") - import traceback - traceback.print_exc() - ShowBase.__init__(self) - self.render_pipeline = None - return - - self.script_manager = ScriptManager(self) - self.script_manager.start_system() - - # 加载所有脚本e - self.script_manager.load_all_scripts_from_directory() - - try: - # 再导入controller模块 - from rpcore.util.movement_controller import MovementController - - self.render_pipeline._showbase.camera = self.render_pipeline._showbase.cam - - self.controller = MovementController(self) - self.camLens.set_fov(80) - self.controller.set_initial_position( - Vec3(0, -50, 20), Vec3(0, 0, 0)) - self.controller.setup() - except ImportError as e: - print(f"导入MovementController失败: {e}") - self.controller = None - - self._last_click_time = 0 - self._last_clicked_node = None - self._double_click_threshold = 0.3 - - self.cameraSpeed = 20.0 - self.cameraRotateSpeed=10.0 - - self.lastMouseX=0 - self.lastMouseY=0 - self.mouseRightPressed=False - - self._loadFont() - self.loadFullScene() - self.loadGUIFromJSON() - self.setupMouseClickHandler() - self.cTrav = CollisionTraverser() - - self.createFrameRateDisplay() - - self.women_actor = None - self.current_actor = None - - if hasattr(self, 'accept'): - base.accept("l", self.tour) - - def _loadFont(self): - """加载中文字体""" - self.chinese_font = None - try: - self.chinese_font = self.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc') - if not self.chinese_font: - print("警告: 无法加载中文字体,将使用默认字体") - else: - print("✓ 中文字体加载成功") - except: - print("警告: 无法加载中文字体,将使用默认字体") - self.chinese_font = None - - def getChineseFont(self): - """获取中文字体""" - return self.chinese_font - - def getResourcePath(self,relative_path): - if getattr(sys,'frozen',False): - base_path = os.path.dirname(sys.executable) - else: - base_path = os.path.dirname(os.path.abspath(__file__)) - - return os.path.join(base_path,relative_path) - - def loadFullScene(self): - if not hasattr(self, 'loader') or not hasattr(self, 'render'): - print("错误: Panda3D核心组件未正确初始化") - return - try: - scene_file = self.getResourcePath("scene.bam") - - if os.path.exists(scene_file): - # 使用readBamFile加载完整场景 - from panda3d.core import BamCache - BamCache.getGlobalPtr().setActive(False) # 禁用缓存以避免问题 - - scene = self.loader.loadModel(Filename.fromOsSpecific(scene_file)) - if scene: - scene.reparentTo(self.render) - self.render_pipeline.prepare_scene(scene) - print("✓ 完整场景加载成功") - - # 检测并播放模型动画 - #self._processModelAnimations(scene) - - # 处理场景中的各种元素 - self.processSceneElements(scene) - else: - print("⚠️ 场景文件加载失败") - else: - print("⚠️ 未找到场景文件") - except Exception as e: - print(f"加载完整场景时出错: {str(e)}") - import traceback - traceback.print_exc() - - def checkAnimationStatus(self): - """检查场景中所有Actor的动画状态""" - try: - all_actors = self.render.findAllMatches("**/+ActorNode") - print(f"=== 动画状态检查 ===") - print(f"场景中总共有 {len(all_actors)} 个Actor") - - for i, actor_np in enumerate(all_actors): - actor_node = actor_np.node() - if isinstance(actor_node, Actor): - # 确保Actor可见 - actor_np.show() - - current_anim = actor_node.getCurrentAnim() - anim_names = actor_node.getAnimNames() - print(f"Actor {i + 1} ({actor_np.getName()}):") - print(f" 位置: {actor_np.getPos()}") - print(f" 可见性: {actor_np.isHidden()}") - print(f" 可用动画: {anim_names}") - print(f" 当前播放: {current_anim}") - if current_anim: - frame = actor_node.getCurrentFrame(current_anim) - num_frames = actor_node.getNumFrames(current_anim) - play_rate = actor_node.getPlayRate(current_anim) - print(f" 播放进度: {frame}/{num_frames} 帧") - print(f" 播放速度: {play_rate}") - else: - print(f"节点 {actor_np.getName()} 不是Actor类型") - print("==================") - except Exception as e: - print(f"检查动画状态时出错: {str(e)}") - - def _processModelAnimations(self, node_path): - """处理节点中的动画模型""" - try: - # 查找场景中所有可能的动画模型 - char_nodes = node_path.findAllMatches("**/+Character") - - for char_node in char_nodes: - try: - # 获取父节点(通常是模型根节点) - model_root = char_node.getParent() - model_name = model_root.getName() - - print(f"检测到可能的动画模型: {model_name}") - - # 尝试创建Actor - actor = Actor(model_root) - actor.reparentTo(self.render) - - actor.setPos(node_path.getPos()) - actor.setHpr(node_path.getHpr()) - actor.setScale(node_path.getScale()) - - - - # 获取动画名称 - anim_names = actor.getAnimNames() - if anim_names: - print(f"✓ 成功创建动画模型: {model_name}") - print(f" 可用动画: {anim_names}") - - # 循环播放所有动画 - for anim_name in anim_names: - print(f" 循环播放动画: {anim_name}") - actor.loop(anim_name) - break # 只播放第一个动画,避免同时播放多个动画 - - # 替换原始模型 - model_root.detachNode() - - else: - # 没有动画,使用原始模型 - print(f"模型 {model_name} 不包含动画") - actor.detachNode() # 移除创建的Actor - - except Exception as e: - print(f"处理动画模型 {char_node.getName()} 时出错: {str(e)}") - - except Exception as e: - print(f"处理模型动画时出错: {str(e)}") - import traceback - traceback.print_exc() - - def processSceneElements(self, scene): - """处理场景中的各种元素""" - try: - processed_lights = [] - loaded_nodes = {} - - def processNode(nodePath, depth=0): - loaded_nodes[nodePath.getName()] = nodePath - - # 为模型添加碰撞体 - 修复版本 - if nodePath.hasTag("is_scene_element") and not nodePath.hasTag("is_gizmo"): - if nodePath.hasTag("gui_type"): - gui_type = nodePath.getTag("gui_type") - if gui_type in ["video_screen"]: - print(f"移除GUI视频节点: {nodePath.getName()}") - nodePath.removeNode() # 移除视频屏幕节点 - return - # 使用更精确的包围盒 - from panda3d.core import CollisionNode, CollisionBox - bounds = nodePath.getBounds() - if not bounds.isEmpty(): - min_point = bounds.getMin() - max_point = bounds.getMax() - - # 创建碰撞节点 - collision_node = CollisionNode(f'collision_{nodePath.getName()}') - collision_node.addSolid(CollisionBox(min_point, max_point)) - collision_np = nodePath.attachNewNode(collision_node) - # 隐藏碰撞体 - collision_np.hide() - - if nodePath.hasTag("scripts_info"): - try: - import json - scripts_info = json.loads(nodePath.getTag("scripts_info")) - self.processScripts(nodePath, scripts_info) - except Exception as e: - print(f"处理节点 {nodePath.getName()} 的脚本时出错: {str(e)}") - - if nodePath.hasTag("light_type"): - light_type = nodePath.getTag("light_type") - - if nodePath.hasTag("is_auxiliary_light") and nodePath.getTag("is_auxiliary_light").lower() == "true": - print(f"跳过辅助灯光节点:{nodePath.getName()}") - return - - if nodePath not in processed_lights: - if light_type == "spot_light": - self._recreateSpotLight(nodePath) - elif light_type == "point_light": - self._recreatePointLight(nodePath) - processed_lights.append(nodePath) - - for child in nodePath.getChildren(): - processNode(child, depth + 1) - - processNode(scene) - - except Exception as e: - print(f"处理场景元素时出错: {str(e)}") - - def _recreateSpotLight(self,light_node): - try: - from RenderPipelineFile.rpcore import SpotLight - from panda3d.core import Vec3 - - light = SpotLight() - light.direction = Vec3(0,0,-1) - light.fov = 70 - light.set_color_from_temperature(5*1000.0) - - if light_node.hasTag("light_energy"): - light.energy = float(light_node.getTag("light_energy")) - else: - light.energy = 5000 - - light.radius = 1000 - light.casts_shadows = True - light.shadow_map_resolution = 256 - - light_pos = light_node.getPos() - light.setPos(light_pos) - - self.render_pipeline.add_light(light) - except Exception as e: - print(f"创建点光源 {light_node.getName()} 失败: {str(e)}") - import traceback - traceback.print_exc() - - def _recreatePointLight(self,light_node): - try: - from RenderPipelineFile.rpcore import PointLight - - light = PointLight() - - if light_node.hasTag("light_energy"): - light.energy = float(light_node.getTag("light_energy")) - else: - light.energy = 5000 - - light.radius = 1000 - light.inner_radius = 0.4 - light.set_color_from_temperature(5*1000.0) - light.casts_shadows = True - light.shadow_map_resolution = 256 - - light.setPos(light_node.getPos()) - - self.render_pipeline.add_light(light) - except Exception as e: - print(f"创建点光源 {light_node.getName()} 失败: {str(e)}") - import traceback - traceback.print_exc() - - def processGUIElements(self, scene): - """处理场景中的GUI元素""" - try: - # 查找并处理2D图像 - images_2d = scene.findAllMatches("**/=gui_type=image_2d") - for img_node in images_2d: - try: - # GUI元素通常在场景加载时自动处理 - print(f"✓ 2D图像 {img_node.getName()} 已加载") - except Exception as e: - print(f"处理2D图像 {img_node.getName()} 失败: {str(e)}") - - except Exception as e: - print(f"处理GUI元素时出错: {str(e)}") - - def tour(self): - mopath = ( - (Vec3(-10.8645000458, 9.76458263397, 2.13306283951), Vec3(-133.556228638, -4.23447799683, 0.0)), - (Vec3(-10.6538448334, -5.98406457901, 1.68028640747), Vec3(-59.3999938965, -3.32706642151, 0.0)), - (Vec3(9.58458328247, -5.63625621796, 2.63269257545), Vec3(58.7906494141, -9.40668964386, 0.0)), - (Vec3(6.8135137558, 11.0153560638, 2.25509500504), Vec3(148.762527466, -6.41223621368, 0.0)), - (Vec3(-9.07093334198, 3.65908527374, 1.42396306992), Vec3(245.362503052, -3.59927511215, 0.0)), - (Vec3(-8.75390911102, -3.82727789879, 0.990055501461), Vec3(296.090484619, -0.604830980301, 0.0)), - ) - self.controller.play_motion_path(mopath, 3.0) - - def loadGUIFromJSON(self): - try: - gui_json_path = self.getResourcePath("gui/gui_elements.json") - if os.path.exists(gui_json_path): - with open(gui_json_path, 'r', encoding='utf-8') as f: - content = f.read().strip() - if content: - gui_data = json.loads(content) - self.createGUIElement(gui_data) - else: - print("GUI配置文件为空 ") - except Exception as e: - print(f"加载GUI元素时出错: {str(e)}") - import traceback - traceback.print_exc() - - def createGUIElement(self, element_data): - try: - processed_names = set() - element_original_data = {} - - for i, gui_info in enumerate(element_data): - name = gui_info.get("name", f"gui_element_{i}") - element_original_data[name] = { - "scale": gui_info.get("scale", [1, 1, 1]), - "position": gui_info.get("position", [0, 0, 0]), - "parent_name": gui_info.get("parent_name") - } - valid_parents = set() - for gui_info in element_data: - name = gui_info.get("name", f"gui_element_{gui_info.get('index', 0)}") - valid_parents.add(name) - - for i, gui_info in enumerate(element_data): - try: - gui_type = gui_info.get("type", "unknown") - name = gui_info.get("name", f"gui_element_{i}") - position = gui_info.get("position", [0, 0, 0]) - scale = gui_info.get("scale", [1, 1, 1]) - tags = gui_info.get("tags", {}) - text = gui_info.get("text", "") - image_path = gui_info.get("image_path", "") - video_path = gui_info.get("video_path", "") - bg_image_path = gui_info.get("bg_image_path", "") - parent_name = gui_info.get("parent_name") - - if name in processed_names: - continue - - processed_names.add(name) - - absolute_position = list(position) - absolute_scale = list(scale) - - if parent_name and parent_name in element_original_data: - parent_data = element_original_data[parent_name] - parent_scale = parent_data["scale"] - - if gui_type in ["3d_text", "3d_image", "button", "label", "entry", "2d_image", - "2d_video_screen"]: - # 位置需要乘以父级缩放来得到绝对位置 - for j in range(min(len(absolute_position), len(parent_scale))): - absolute_position[j] *= parent_scale[j] if len(parent_scale) > j else parent_scale[0] - - # 缩放需要乘以父级缩放来得到绝对缩放 - for j in range(min(len(absolute_scale), len(parent_scale))): - absolute_scale[j] *= parent_scale[j] if len(parent_scale) > j else parent_scale[0] - - new_element = None - - if gui_type == "3d_text": - size = absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 0.5 - new_element = self.createGUI3DText( - pos=tuple(absolute_position), - text=text, - size=size - ) - elif gui_type == "button": - new_element = self.createGUIButton( - pos=tuple(absolute_position), - text=text, - size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0, - command=self.playModelAnimation - ) - elif gui_type == "label": - new_element = self.createGUILabel( - pos=tuple(absolute_position), - text=text, - size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0 - ) - elif gui_type == "entry": - new_element = self.createGUIEntry( - pos=tuple(absolute_position), - placeholder=text, - size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0, - command=self.onGUIEntrySubmit - ) - elif gui_type == "2d_image": - - scale_value = absolute_scale[0] - print(f"2d_image{scale_value}") - new_element = self.createGUI2DImage( - pos=tuple(absolute_position), - image_path=image_path, - size=absolute_scale - ) - elif gui_type == "2d_video_screen": - new_element = self.createGUI2DVideoScreen( - pos=tuple(absolute_position), - video_path=video_path, - size=absolute_scale - ) - elif gui_type == "video_screen": - new_element = self.createGUIVideoScreen( - pos=tuple(absolute_position), - video_path=video_path, - size=absolute_scale - ) - - if "scripts" in gui_info and new_element: - self.processScripts(new_element,gui_info["scripts"]) - - except Exception as e: - print(f"重建GUI元素失败 {name}: {e}") - import traceback - traceback.print_exc() - continue - - except Exception as e: - print(f"创建GUI元素时出错: {str(e)}") - - def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1, command=None): - from direct.gui.DirectGui import DirectButton - - button = DirectButton( - text=text, - pos=(pos[0], pos[1], pos[2]), # 保持正确的坐标格式 - scale=size, # size 应该是数值而不是元组 - frameColor=(0.2, 0.6, 0.8, 1), - text_font=self.getChineseFont() if self.getChineseFont() else None, - rolloverSound=None, - clickSound=None, - parent=None, - command=command - ) - - def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5): - """创建3D文本GUI元素""" - try: - # 创建文本节点 - text_node = TextNode("gui_3d_text") - text_node.setText(text) - text_node.setAlign(TextNode.ACenter) - - # 设置字体(如果可用) - if self.getChineseFont(): - text_node.setFont(self.getChineseFont()) - - # 创建节点路径并添加到场景 - text_np = self.render.attachNewNode(text_node) - - # 设置位置和大小 - text_np.setPos(Vec3(pos[0], pos[1], pos[2])) - text_np.setScale(size) - - # 设置面向摄像机 - # text_np.setBillboardPointEye() - - # 设置渲染属性 - text_np.setBin("fixed", 40) - text_np.setDepthWrite(False) - - return text_np - except Exception as e: - print(f"❌ 创建3D文本失败: {str(e)}") - import traceback - traceback.print_exc() - return None - - def createGUILabel(self, pos=(0, 0, 0), text="标签", size=0.08): - from direct.gui.DirectGui import DirectLabel - label = DirectLabel( - text=text, - pos=(pos[0], pos[1], pos[2]), - scale=size, - frameColor=(0, 0, 0, 0), - text_fg=(1, 1, 1, 1), - text_font=self.getChineseFont() if self.getChineseFont() else None, - text_align=TextNode.ACenter, - text_wordwrap=None, - text_mayChange=True, - parent=None - ) - - def createGUIEntry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08, command=None): - from direct.gui.DirectGui import DirectEntry - - entry = DirectEntry( - text="", - pos=(pos[0], pos[1], pos[2]), - scale=size, - command=command, - initialText=placeholder, - numLines=1, - width=12, - focus=0, - frameColor=(0, 0, 0, 0), - text_fg=(1, 1, 1, 1), - text_font=self.getChineseFont() if self.getChineseFont() else None, - text_align=TextNode.ACenter, - text_wordwrap=None, - text_mayChange=True, - parent=None, - rolloverSound=None, - clickSound=None, - # 添加焦点管理命令 - focusInCommand=self.disableCameraControl, - focusOutCommand=self.enableCameraControl, - # 确保输入框能正确捕获所有键盘事件 - suppressKeys=True, # 这个参数很重要,它会阻止按键事件传播到其他处理器 - suppressMouse=True - ) - return entry - - def disableCameraControl(self): - """禁用相机控制""" - try: - if hasattr(self, 'controller'): - # 如果控制器有内置的禁用方法 - if hasattr(self.controller, 'disable'): - self.controller.disable() - else: - # 否则手动禁用事件监听 - self.controller.ignoreAll() # 忽略所有已注册的事件 - print("相机控制已禁用") - except Exception as e: - print(f"禁用相机控制时出错: {e}") - - def enableCameraControl(self): - """启用相机控制""" - try: - if hasattr(self, 'controller'): - # 如果控制器有内置的启用方法 - if hasattr(self.controller, 'enable'): - self.controller.enable() - else: - # 重新设置控制器 - self.controller.setup() - print("相机控制已启用") - except Exception as e: - print(f"启用相机控制时出错: {e}") - - def onGUIEntrySubmit(self, text, entry_id=None): - """GUI输入框提交事件处理""" - try: - print(f"GUI输入框提交: {entry_id} = {text}") - - # 重新启用相机控制 - self.enableCameraControl() - - # 清除输入框焦点(如果需要) - # base.win.focus() - - # 在这里添加您需要的文本处理逻辑 - # 例如保存文本、更新UI等 - - except Exception as e: - print(f"处理输入框提交时出错: {e}") - import traceback - traceback.print_exc() - - def createGUI2DImage(self, pos=(0, 0, 0), image_path=None, size=0.2): - # 添加属性检查 - if not hasattr(self, 'aspect2d'): - print("错误: aspect2d未初始化") - return None - if image_path and not os.path.isabs(image_path): - image_path = self.getResourcePath(image_path) - # 处理非均匀缩放 - if isinstance(size, (list, tuple)) and len(size) >= 2: - # 分别处理宽度和高度的缩放 - width_scale = size[0] * 0.2 - height_scale = size[2] * 0.2 - else: - # 如果只提供了一个缩放值,则使用相同值 - width_scale = size * 0.1 if isinstance(size, (int, float)) else 0.2 - height_scale = width_scale - - cm = CardMaker("gui-2d-image") - cm.setFrame(-width_scale, width_scale, -height_scale, height_scale) - #cm.setFrame(-size, size, -size, size) - - image_node = self.aspect2d.attachNewNode(cm.generate()) - image_node.setPos(pos) - image_node.setBin("fixed", 0) - image_node.setDepthWrite(False) - image_node.setDepthTest(False) - image_node.setColor(1, 1, 1, 1) - - # 设置透明度支持 - image_node.setTransparency(TransparencyAttrib.MAlpha) - - if image_path: - try: - texture = self.loader.loadTexture(image_path) - if texture: - image_node.setTexture(texture, 1) - texture.setWrapU(Texture.WM_clamp) - texture.setWrapV(Texture.WM_clamp) - texture.setMinfilter(Texture.FT_linear) - texture.setMagfilter(Texture.FT_linear) - image_node.setColor(1, 1, 1, 1) - else: - print(f"无法加载图片: {image_path}") - except Exception as e: - print(f"加载图片时出错: {e}") - return image_node - - def createGUIVideoScreen(self,pos=(0,0,0),size=1,video_path=None): - import os - from panda3d.core import TransparencyAttrib,Texture,TextureStage - - if isinstance(size,(list,tuple)) and len(size) >= 2: - width_scale = size[0] - height_scale = size[2] - else: - width_scale = size * 0.1 if isinstance(size, (int, float)) else 0.2 - height_scale = width_scale - - cm = CardMaker("gui-video-screen") - cm.setFrame(-width_scale,width_scale,-height_scale,height_scale) - - video_screen = self.render.attachNewNode(cm.generate()) - video_screen.setPos(pos) - - video_screen.setBin("fixed", 0) - - #video_screen.setTransparency(TransparencyAttrib.MAlpha) - - video_screen.setColor(1, 1, 1, 1) - - self._ensureVideoScreenMaterial(video_screen) - - if video_path: - if video_path.startswith(("http://", "https://")): - try: - success = self._loadVideoFromURLWithOpenCV3D(video_screen, video_path) - if success: - print("视频已从URL加载成功") - else: - print("从URL加载视频时出错") - except Exception as e: - print(f"从URL加载视频时出错: {e}") - elif os.path.exists(video_path): - movie_texture = self._loadMovieTexture(video_path) - if movie_texture: - video_screen.setTexture(movie_texture, 1) - else: - print(f"视频文件不存在: {video_path}") - else: - # 设置占位符纹理 - placeholder_texture = Texture() - placeholder_texture.setup2dTexture(1, 1, Texture.TUnsignedByte, Texture.FRgb) - placeholder_data = b'\x19\x19\x4c' # 深蓝色占位符 - placeholder_texture.setRamImage(placeholder_data) - video_screen.setTexture(placeholder_texture, 1) - - return video_screen - - def createGUI2DVideoScreen(self, pos=(0, 0, 0), size=0.2, video_path=None): - import os - from direct.gui.DirectGui import DirectFrame - from panda3d.core import TransparencyAttrib, Texture, TextureStage - - if isinstance(size,(list,tuple)) and len(size) >= 2: - width_scale = size[0]*0.2 - height_scale = size[2]*0.2 - else: - width_scale = size*0.1 if isinstance(size,(int,float)) else 0.2 - height_scale = width_scale - - video_screen = DirectFrame( - frameSize=(-width_scale, width_scale, -height_scale, height_scale), - frameColor=(1, 1, 1, 1), - pos=pos, - parent=None, - suppressMouse=True - ) - video_screen.setTransparency(TransparencyAttrib.MAlpha) - - placeholder_texture = Texture() - placeholder_texture.setup2dTexture(1, 1, Texture.TUnsignedByte, Texture.FRgb) - placeholder_data = b'\x19\x19\x4c' - placeholder_texture.setRamImage(placeholder_data) - - if video_path: - if video_path.startswith(("http://","https://")): - try: - success = self._loadVideoFromURLWithOpenCV(video_screen,video_path) - if success: - print("视频已从URL加载成功") - else: - print("从URL加载视频时出错") - except Exception as e: - print(f"从URL加载视频时出错: {e}") - elif os.path.exists(video_path): - movie_texture = self._loadMovieTexture(video_path) - if movie_texture: - video_screen["frameTexture"] = movie_texture - - return video_screen - - def _loadMovieTexture(self, video_path): - from panda3d.core import Texture, MovieTexture, Filename - import os - - # Convert Windows path to Panda3D compatible path format - panda_path = Filename.fromOsSpecific(video_path) - converted_path = str(panda_path) - - movie_texture = MovieTexture(converted_path) - if movie_texture.read(converted_path): - self._configureVideoTexture(movie_texture) - return movie_texture - - def _configureVideoTexture(self, texture): - from panda3d.core import Texture - - texture.setWrapU(Texture.WM_clamp) - texture.setWrapV(Texture.WM_clamp) - texture.setMinfilter(Texture.FT_linear) - texture.setMagfilter(Texture.FT_linear) - - texture.setFormat(Texture.FRgb8) - - if hasattr(texture, 'set_loop') and hasattr(texture, 'set_play_rate'): - texture.set_loop(True) - texture.set_play_rate(1.0) - - def _loadVideoFromURLWithOpenCV(self, video_screen, url): - try: - import cv2 - import threading - from panda3d.core import Texture,PNMImage - import numpy as np - import time - - def video_stream_thread(): - cap = cv2.VideoCapture(url) - - if not cap.isOpened(): - print(f"无法打开视频流: {url}") - return False - - cap.set(cv2.CAP_PROP_BUFFERSIZE,1) - - fps = cap.get(cv2.CAP_PROP_FPS) - frame_delay = 1.0 / fps if fps > 0 else 0.033 # 默认30fps - - while hasattr(self, 'video_stream_active') and self.video_stream_active: - ret, frame = cap.read() - - if not ret: - print("视频流读取失败,尝试重新连接...") - cap.release() - time.sleep(1) - cap = cv2.VideoCapture(url) - continue - - frame_height,frame_width = frame.shape[:2] - if frame_width > 256: - scale = 256.0/frame_width - new_width = int(frame_width * scale) - new_height = int(frame_height * scale) - frame = cv2.resize(frame,(new_width,new_height)) - - frame_rgb = cv2.cvtColor(frame,cv2.COLOR_BGR2RGB) - - height,width = frame_rgb.shape[:2] - - img = PNMImage(width,height,3) - img.set_maxval(255) - - for y in range(height): - for x in range(width): - r,g,b = frame_rgb[y,x] - img.setXelVal(x,y,r,g,b) - - texture = Texture("video_texture") - texture.setMagfilter(Texture.FTLinear) - texture.setMinfilter(Texture.FTLinear) - texture.setWrapU(Texture.WMClamp) - texture.setWrapV(Texture.WMClamp) - texture.load(img) - - # 在主线程中更新GUI纹理 - def update_texture(): - if hasattr(self, 'aspect2d') and not video_screen.isEmpty(): - video_screen["frameTexture"] = texture - - # 使用taskMgr在主线程中更新纹理 - if hasattr(self, 'taskMgr'): - self.taskMgr.doMethodLater(0, lambda task: update_texture() or task.done, - f"updateVideoTexture_{time.time()}") - - # 控制帧率 - time.sleep(frame_delay) - - cap.release() - print("视频流线程结束") - return True - - # 启动视频流线程 - self.video_stream_active = True - self.video_thread = threading.Thread(target=video_stream_thread, daemon=True) - self.video_thread.start() - - return True - - except Exception as e: - print(f"加载视频流失败: {e}") - import traceback - traceback.print_exc() - return False - - def _loadVideoFromURLWithOpenCV3D(self,video_screen,url): - try: - import cv2 - import threading - from panda3d.core import Texture,PNMImage - import time - - def video_stream_thread(): - cap = cv2.VideoCapture(url) - - if not cap.isOpened(): - return False - - cap.set(cv2.CAP_PROP_BUFFERSIZE,1) - - fps = cap.get(cv2.CAP_PROP_FPS) - frame_delay = 1.0 / fps if fps > 0 else 0.033 - - while hasattr(self,'video_stream_active') and self.video_stream_active: - ret,frame = cap.read() - - if not ret: - cap.release() - time.sleep(1) - cap = cv2.VideoCapture(url) - continue - - frame_height,frame_width = frame.shape[:2] - if frame_width > 256: - scale = 256.0/frame_width - new_width = int(frame_width*scale) - new_height = int(frame_height*scale) - frame = cv2.resize(frame,(new_width,new_height)) - - frame_rgb = cv2.cvtColor(frame,cv2.COLOR_BGR2RGB) - - height,width = frame_rgb.shape[:2] - - img = PNMImage(width,height,3) - img.set_maxval(255) - - for y in range(height): - for x in range(width): - r,g,b = frame_rgb[y,x] - img.setXelVal(x,y,r,g,b) - - texture = Texture("video_texture_3d") - texture.setMagfilter(Texture.FTLinear) - texture.setMinfilter(Texture.FTLinear) - texture.setWrapU(Texture.WMClamp) - texture.setWrapV(Texture.WMClamp) - texture.load(img) - - def update_texture(): - if hasattr(self,'render') and not video_screen.isEmpty(): - video_screen.setTexture(texture,1) - - if hasattr(self,'taskMgr'): - self.taskMgr.doMethodLater(0,lambda task:update_texture() or task.done,f"updateVideoTexture_{time.time()}") - - time.sleep(frame_delay) - - cap.release() - return True - self.video_stream_active = True - self.video_thread_3d = threading.Thread(target=video_stream_thread,daemon=True) - self.video_thread_3d.start() - - return True - except Exception as e: - print(f"加载3D视频流失败: {e}") - import traceback - traceback.print_exc() - return False - - def _ensureVideoScreenMaterial(self, video_screen): - """确保视频屏幕有正确的材质设置""" - try: - from panda3d.core import Material, LColor - - if not video_screen.hasMaterial(): - material = Material(f"video-material-{video_screen.getName()}") - material.setBaseColor(LColor(1, 1, 1, 1)) - material.setDiffuse(LColor(1, 1, 1, 1)) - material.setAmbient(LColor(1, 1, 1, 1)) # 确保环境光为白色 - material.setEmission(LColor(0, 0, 0, 1)) - material.setSpecular(LColor(0, 0, 0, 1)) - material.setShininess(0) - video_screen.setMaterial(material, 1) - else: - # 更新现有材质确保正确设置 - material = video_screen.getMaterial() - material.setBaseColor(LColor(1, 1, 1, 1)) - material.setAmbient(LColor(1, 1, 1, 1)) # 确保环境光为白色 - video_screen.setMaterial(material, 1) - - except Exception as e: - print(f"⚠️ 设置视频屏幕材质时出错: {e}") - - def playModelAnimation(self): - """播放场景中所有 .glb 模型的动画(仅转换有动画的模型为 Actor)""" - try: - glb_models = self.render.findAllMatches("**/*.glb*") - - # 修复过滤逻辑,确保正确排除碰撞体节点 - filtered_models = [] - for model in glb_models: - model_name = model.getName().lower() - # 排除碰撞体节点 - if ("collision_" not in model_name and - "modelcollision_" not in model_name and - not model_name.endswith("_bounds")): - filtered_models.append(model) - - if not filtered_models: - print("⚠️ 场景中没有找到 .glb 模型") - return - - print(f"找到 {len(filtered_models)} 个 glb 模型") - - self.actors = [] # 存储所有 Actor,避免被垃圾回收 - - for model in filtered_models: - print(f"正在处理模型: {model.getName()}") - - actor = None - - # 尝试把模型当作Actor加载,判断是否有动画 - try: - # 在创建Actor之前先检查节点是否可能包含动画 - if not self._isValidAnimationNode(model): - print(f"⚠️ {model.getName()} 不是有效的动画节点,跳过") - continue - - test_actor = Actor(model) - anim_names = test_actor.getAnimNames() - except Exception as e: - print(f"⚠️ {model.getName()} 无法作为Actor加载: {str(e)}") - anim_names = [] - - if anim_names: # ✅ 只有有动画才转为Actor - if not isinstance(model, Actor): - model_parent = model.getParent() - model_pos = model.getPos() - model_hpr = model.getHpr() - model_scale = model.getScale() - - actor = Actor(model) - actor.reparentTo(model_parent) - actor.setPos(model_pos) - actor.setHpr(model_hpr) - actor.setScale(model_scale) - - model.detachNode() - else: - actor = model - - # 播放动画 - actor.show() - self.actors.append(actor) - - print(f"✓ {actor.getName()} 可用动画: {anim_names}") - first_anim = anim_names[0] - - actor.stop() - actor.setPlayRate(1.0, first_anim) - actor.loop(first_anim) - print(f"✓ {actor.getName()} 正在播放动画: {first_anim}") - - actor.update() - - else: # 没有动画 - print(f"⚠️ {model.getName()} 没有动画,不转为Actor") - - except Exception as e: - print(f"播放模型动画时出错: {str(e)}") - import traceback - traceback.print_exc() - - def _isValidAnimationNode(self, nodePath): - """检查节点是否可能是有效的动画节点""" - # 排除明显不是动画节点的节点 - name = nodePath.getName().lower() - if ("collision_" in name or - "modelcollision_" in name or - "bound" in name or - name.endswith("_bounds")): - return False - - # 检查节点是否有网格数据(简单判断) - from panda3d.core import GeomNode - geom_nodes = nodePath.findAllMatches("**/+GeomNode") - if not geom_nodes: - return False - - return True - - def focusOnWomenModel(self): - """定位并聚焦到Women模型""" - try: - women_models = self.render.findAllMatches("**/Women_1.glb*") - if women_models: - model = women_models[0] - # 确保模型可见 - model.show() - - # 将模型放置在原点附近 - model.setPos(0, 0, 0) - model.setScale(1.0) - - # 确保是Actor的话设置动画 - if isinstance(model, Actor): - anim_names = model.getAnimNames() - if anim_names: - model.loop(anim_names[0]) - model.setPlayRate(1.0, anim_names[0]) - print(f"为模型设置动画: {anim_names[0]}") - - print(f"已定位模型: {model.getName()}") - else: - print("未找到Women模型") - except Exception as e: - print(f"定位模型时出错: {str(e)}") - - def processScripts(self, element, script_info_list): - """处理元素上挂载的脚本 - 使用新的脚本系统""" - try: - print(f"正在为元素 {element.getName()} 挂载脚本") - print(f"可用脚本列表: {self.script_manager.get_available_scripts()}") - - if not hasattr(self,'script_manager'): - print("脚本管理器未初始化") - return - - for script_info in script_info_list: - script_name = script_info["name"] - script_file = script_info.get("file", "") - - if script_name: - script_component = self.script_manager.add_script_to_object(element,script_name) - - if script_component: - print(f"✓ 脚本 {script_name} 已挂载到元素 {element.getName()}") - else: - print(f"⚠️ 脚本 {script_name} 挂载失败") - # 列出可用脚本帮助调试 - available_scripts = self.script_manager.get_available_scripts() - print(f"当前可用脚本: {available_scripts}") - else: - print(f"⚠️ 脚本信息不完整: {script_info}") - - # # 从文件路径中提取脚本类名 - # if script_file: - # # 获取脚本文件名(不含路径和扩展名) - # script_filename = os.path.basename(script_file) - # script_class_name = os.path.splitext(script_filename)[0] - # print(f"尝试挂载脚本: {script_class_name} (来自文件: {script_file})") - # - # # 使用脚本管理器为元素添加脚本 - # script_component = self.script_manager.add_script_to_object(element, script_class_name) - # - # if script_component: - # print(f"✓ 脚本 {script_class_name} 已挂载到元素 {element.getName()}") - # else: - # print(f"⚠️ 脚本 {script_class_name} 挂载失败") - # # 列出可用脚本帮助调试 - # available_scripts = self.script_manager.get_available_scripts() - # print(f"当前可用脚本: {available_scripts}") - # else: - # print(f"⚠️ 脚本信息不完整: {script_name}") - - except Exception as e: - print(f"挂载脚本到元素 {element.getName()} 失败: {str(e)}") - import traceback - traceback.print_exc() - - def checkDoubleClick(self, nodePath): - """检查是否为双击,返回布尔值 - 改进版本""" - try: - import time - current_time = time.time() - - # 必须是同一节点且在时间阈值内 - is_double_click = (self._last_clicked_node is not None and - self._last_clicked_node == nodePath and - nodePath is not None and - current_time - self._last_click_time < self._double_click_threshold) - - if is_double_click: - # 双击成功,重置状态 - self._last_click_time = 0 - self._last_clicked_node = None - print(f"✓ 检测到双击: {nodePath.getName()}") - return True - else: - # 单击,更新状态 - self._last_click_time = current_time - self._last_clicked_node = nodePath - return False - - except Exception as e: - print(f"双击检测失败: {e}") - return False - - def focusCameraOnNode(self, nodePath): - """带动画效果的聚焦到节点方法""" - try: - if not nodePath or nodePath.isEmpty(): - print("无效的节点") - return - - minPoint = Point3() - maxPoint = Point3() - - if not nodePath.calcTightBounds(minPoint, maxPoint,self.render): - print("无法计算选中节点的边界框,使用节点为位置作为替代方案") - node_pos = nodePath.getPos(self.render) - optimal_distance = 10.0 - current_cam_pos = self.cam.getPos() - view_direction = node_pos - current_cam_pos - if view_direction.length()<0.001: - view_direction = Vec3(5,-5,2) - view_direction.normalize() - target_cam_pos = node_pos-(view_direction * optimal_distance) - - temp_node = self.render.attachNewNode("temp_lookat_target") - temp_node.setPos(node_pos) - dummy_cam = self.render.attachNewNode("dummy_camera") - dummy_cam.setPos(target_cam_pos) - dummy_cam.lookAt(temp_node) - target_cam_hpr = Vec3(dummy_cam.getHpr()) - - temp_node.removeNode() - dummy_cam.removeNode() - - current_cam_pos = Point3(self.cam.getPos()) - current_cam_hpr = Vec3(self.cam.getHpr()) - self._startCameraFocusAnimation(current_cam_pos,target_cam_pos,current_cam_hpr,target_cam_hpr) - print(f"开始聚焦到节点(使用位置): {nodePath.getName()}") - - return - - center = Point3( - (minPoint.x + maxPoint.x)*0.5, - (minPoint.y+maxPoint.y)*0.5, - (minPoint.z+maxPoint.z)*0.5 - ) - - size = (maxPoint - minPoint).length() - if size < 0.01: - size = 1.0 - - current_cam_pos = Point3(self.cam.getPos()) - current_cam_hpr = Vec3(self.cam.getHpr()) - - view_direction = current_cam_pos - center - if view_direction.length() < 0.001: - view_direction = Vec3(5,-5,2) - - view_direction.normalize() - - optimal_distance = max(size * 2.5,1.0) - - target_cam_pos = center + (view_direction * optimal_distance) - - temp_node = self.render.attachNewNode("temp_lookat_target") - temp_node.setPos(center) - - dummy_cam = self.render.attachNewNode("dummy_camera") - dummy_cam.setPos(target_cam_pos) - dummy_cam.lookAt(temp_node) - target_cam_hpr = Vec3(dummy_cam.getHpr()) - - temp_node.removeNode() - dummy_cam.removeNode() - - self._startCameraFocusAnimation(current_cam_pos,target_cam_pos, - current_cam_hpr,target_cam_hpr) - - print(f"开始聚焦到节点: {nodePath.getName()}") - return True - - except Exception as e: - print(f"聚焦相机失败: {str(e)}") - import traceback - traceback.print_exc() - - def _startCameraFocusAnimation(self, start_pos, end_pos, start_hpr, end_hpr): - """启动相机聚焦动画 - 改进版本""" - try: - class CameraFocusData: - def __init__(self, start_pos, end_pos, start_hpr, end_hpr): - self.start_pos = Point3(start_pos) - self.end_pos = Point3(end_pos) - self.start_hpr = Vec3(start_hpr) - self.end_hpr = Vec3(end_hpr) - self.elapsed_time = 0.0 - self.duration = 0.5 # 增加动画时间使移动更平滑 - - self._camera_focus_data = CameraFocusData(start_pos, end_pos, start_hpr, end_hpr) - - from direct.task.TaskManagerGlobal import taskMgr - taskMgr.remove("cameraFocusTask") - - taskMgr.add(self._cameraFocusTask, "cameraFocusTask") - print(f"开始相机聚焦动画: {start_pos} -> {end_pos}") - - except Exception as e: - print(f"相机聚焦动画启动失败: {str(e)}") - - def _normalizeAngle(self, angle): - """规范化角度到-180到180度之间""" - while angle > 180: - angle -= 360 - while angle < -180: - angle += 360 - return angle - - def _cameraFocusTask(self, task): - """摄像机聚焦动画任务""" - try: - if not hasattr(self, '_camera_focus_data'): - return task.done - - data = self._camera_focus_data - from direct.showbase.ShowBaseGlobal import globalClock - dt = globalClock.getDt() - - if dt <= 0: - dt = 0.016 - - data.elapsed_time += dt - t = min(1.0, data.elapsed_time / data.duration) - - # 使用平滑插值 - smooth_t = t * t * (3 - 2 * t) - - # 插值位置和朝向 - current_pos = data.start_pos + (data.end_pos - data.start_pos) * smooth_t - current_hpr = data.start_hpr + (data.end_hpr - data.start_hpr) * smooth_t - - self.cam.setPos(current_pos) - self.cam.setHpr(current_hpr) - - if t >= 1.0: - self.cam.setPos(data.end_pos) - self.cam.setHpr(data.end_hpr) - - print(f"聚焦完成: 位置 {data.end_pos}, 朝向 {data.end_hpr}") - - if hasattr(self, '_camera_focus_data'): - delattr(self, '_camera_focus_data') - - return task.done - - return task.cont - - except Exception as e: - print(f"摄像机聚焦动画任务失败: {e}") - import traceback - traceback.print_exc() - return task.done - - def setupMouseClickHandler(self): - """设置鼠标点击处理器""" - try: - # 设置鼠标监听 - self.accept("mouse1", self.onMouseClick) - - # 启用鼠标观察器 - if not hasattr(self, 'mouseWatcherNode'): - from panda3d.core import MouseWatcher - self.mouseWatcherNode = MouseWatcher() - - except Exception as e: - print(f"设置鼠标点击处理器失败: {e}") - - def onMouseClick(self): - """处理鼠标左键点击""" - try: - if not hasattr(self, 'mouseWatcherNode') or not self.mouseWatcherNode.hasMouse(): - return - - # 获取鼠标位置 - mouse_pos = self.mouseWatcherNode.getMouse() - - # 创建射线进行点击检测 - from panda3d.core import CollisionRay, CollisionNode, CollisionHandlerQueue, BitMask32 - - # 创建射线 - ray = CollisionRay() - ray.setFromLens(self.camNode, mouse_pos.x, mouse_pos.y) - - # 创建碰撞节点 - ray_node = CollisionNode('mouseRay') - ray_node.addSolid(ray) - ray_node.setFromCollideMask(BitMask32.allOn()) - - # 附加到相机 - ray_np = self.camera.attachNewNode(ray_node) - - # 创建碰撞队列 - handler = CollisionHandlerQueue() - - # 确保碰撞遍历器存在 - if not hasattr(self, 'cTrav'): - self.cTrav = CollisionTraverser() - - self.cTrav.addCollider(ray_np, handler) - self.cTrav.traverse(self.render) - - # 检查碰撞结果 - target_node = None - if handler.getNumEntries() > 0: - # 按距离排序 - handler.sortEntries() - - # 遍历所有碰撞结果,找到最近的有效节点 - for i in range(handler.getNumEntries()): - entry = handler.getEntry(i) - clicked_node = entry.getIntoNodePath() - - # 向上遍历找到可选择的父节点 - current_node = clicked_node - found_valid_node = False - - while current_node and not current_node.isEmpty(): - # 检查节点是否应该被选择 - if (current_node.hasTag("is_scene_element") and - not current_node.hasTag("is_gizmo") and - current_node.getName() not in ["render", "camera", "ambient_light", - "directional_light", "mouseRay"] and - not current_node.getName().startswith("collision_") and - "ground" not in current_node.getName().lower()): - target_node = current_node - found_valid_node = True - break - - # 如果当前节点是碰撞体,获取其父节点作为目标 - if current_node.getName().startswith("collision_"): - parent = current_node.getParent() - if (parent.hasTag("is_scene_element") and - not parent.hasTag("is_gizmo") and - parent.getName() not in ["render", "camera", "ambient_light", - "directional_light"] and - "ground" not in parent.getName().lower()): - target_node = parent - found_valid_node = True - break - - current_node = current_node.getParent() - if current_node.getName() == "render": - break - - if found_valid_node: - break - - # 如果找到了目标节点,则进行双击检测 - if target_node: - print(f"找到可选择节点: {target_node.getName()}") - print(f"节点位置: {target_node.getPos()}") - print(f"节点边界: {target_node.getBounds()}") - # 检查是否为双击 - if self.checkDoubleClick(target_node): - print(f"双击节点: {target_node.getName()}") - self.focusCameraOnNode(target_node) - else: - print(f"单击节点: {target_node.getName()}") - else: - print("未找到有效的目标节点") - - # 清理碰撞器 - ray_np.removeNode() - - except Exception as e: - print(f"处理鼠标点击失败: {e}") - import traceback - traceback.print_exc()\ - - def createFrameRateDisplay(self): - from direct.gui.DirectGui import DirectLabel - from panda3d.core import TextNode - - self.fps_label = DirectLabel( - text="FPS:0", - pos=(-1.8,0,0.9), - scale=0.05, - text_fg=(1,1,1,1), - text_align=TextNode.ALeft, - frameSize=(-0.1,0.3,-0.05,0.05), - frameColor=(0,0,0,0.5), - text_font=self.getChineseFont() if self.getChineseFont() else None, - #parent=self.a2dTopLeft - ) - - taskMgr.add(self.updateFrameRate,"updateFrameRateTask") - - def updateFrameRate(self,task): - try: - fps = globalClock.getAverageFrameRate() - - if self.fps_label: - self.fps_label.setText(f"FPS:{fps:.1f}") - except Exception as e: - print(f"更新帧率失败: {e}") - return task.cont - - -# 在 main.py 的最后部分,修改为: -if __name__ == "__main__": - try: - app = MainApp() - if hasattr(app, 'run'): - app.run() - else: - print("应用程序初始化失败") - except Exception as e: - print(f"应用程序启动失败: {str(e)}") - import traceback - traceback.print_exc() - - +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +"""EG packaged project runtime template.""" + +from __future__ import annotations + +import importlib +import json +import os +import sys +import traceback + +from direct.showbase.ShowBase import ShowBase +from panda3d.core import ( + CardMaker, + Filename, + Material, + MaterialAttrib, + MovieTexture, + Vec4, + TextNode, + TransparencyAttrib, + Vec3, + load_prc_file_data, +) + +PROJECT_NAME = "__EG_PROJECT_NAME__" + + +def _bootstrap_paths(): + if getattr(sys, "frozen", False): + project_root = getattr(sys, "_MEIPASS", "") or os.path.join(os.path.dirname(sys.executable), "_internal") + if not os.path.isdir(project_root): + project_root = os.path.dirname(sys.executable) + else: + source_root = os.path.dirname(os.path.abspath(__file__)) + assets_root = os.path.normpath(os.path.join(source_root, "..", "assets")) + project_root = assets_root if os.path.isdir(assets_root) else source_root + + os.chdir(project_root) + + search_paths = [ + project_root, + os.path.join(project_root, "third_party"), + os.path.join(project_root, "RenderPipelineFile"), + ] + for path in search_paths: + if os.path.isdir(path) and path not in sys.path: + sys.path.insert(0, path) + + return project_root + + +PROJECT_ROOT = _bootstrap_paths() + + +class MainApp(ShowBase): + def __init__(self): + self.project_path = PROJECT_ROOT + self.gui_elements = [] + self.chinese_font = None + self.script_manager = None + self.render_pipeline = None + self._movie_textures = [] + self.camera_control_enabled = True + self.mouse_controller = None + self.use_ssbo_mouse_picking = True + self.use_ssbo_scene_import = True + self.ssbo_runtime_importer = None + self._ssbo_visible_scene = None + + load_prc_file_data( + "", + f""" + win-size 1380 750 + window-title {PROJECT_NAME} + load-display pandagl + aux-display p3tinydisplay + sync-video false + show-frame-rate-meter false + support-threads false + """, + ) + + from rpcore import RenderPipeline + + self.render_pipeline = RenderPipeline() + self.render_pipeline.pre_showbase_init() + ShowBase.__init__(self) + self.render_pipeline.create(self) + self.render_pipeline._showbase.camera = self.render_pipeline._showbase.cam + self.disableMouse() + + self._load_font() + self._init_script_manager() + self._init_ssbo_runtime_importer() + self._load_camera_state() + self._init_camera_controller() + self.load_full_scene() + self.load_gui_from_json() + + def _init_script_manager(self): + script_module = importlib.import_module("core.script_system") + self.script_manager = script_module.ScriptManager(self) + self.script_manager.hot_reload_enabled = False + + scripts_dir = self.get_resource_path("scripts") + if hasattr(self.script_manager, "set_scripts_directory"): + self.script_manager.set_scripts_directory( + scripts_dir, + create=False, + reload_scripts=False, + ) + + self.script_manager.start_system() + self.script_manager.set_hot_reload_enabled(False) + + def _load_font(self): + font_candidates = [ + "C:/Windows/Fonts/msyh.ttc", + "C:/Windows/Fonts/simhei.ttf", + ] + for font_path in font_candidates: + if os.path.exists(font_path): + try: + self.chinese_font = self.loader.loadFont(font_path) + if self.chinese_font: + print(f"✓ 中文字体加载成功: {font_path}") + return + except Exception: + continue + print("⚠ 未找到可用中文字体,继续使用默认字体") + + def _init_ssbo_runtime_importer(self): + if not (self.use_ssbo_mouse_picking and self.use_ssbo_scene_import): + return + try: + importer_module = importlib.import_module("ssbo_component.runtime_importer") + importer_cls = getattr(importer_module, "RuntimeSSBOSceneImporter", None) + if importer_cls: + self.ssbo_runtime_importer = importer_cls(self) + print("✓ 运行时 SSBO 场景导入器已启用") + except Exception as e: + print(f"⚠ 初始化运行时 SSBO 导入器失败: {e}") + + def get_chinese_font(self): + return self.chinese_font + + def get_resource_path(self, relative_path): + return os.path.normpath(os.path.join(PROJECT_ROOT, relative_path)) + + def _load_json_file(self, file_path, default_value): + if not os.path.exists(file_path): + return default_value + try: + with open(file_path, "r", encoding="utf-8-sig") as f: + return json.load(f) + except Exception as e: + print(f"⚠ 读取 JSON 失败 {file_path}: {e}") + return default_value + + def _load_camera_state(self): + camera_state_path = self.get_resource_path("camera_state.json") + camera_state = self._load_json_file(camera_state_path, {}) + if not isinstance(camera_state, dict): + camera_state = {} + + position = camera_state.get("position") or [0, -20, 5] + rotation = camera_state.get("rotation") or [0, 0, 0] + self.camera_control_enabled = bool(camera_state.get("camera_control_enabled", True)) + + try: + if len(position) >= 3: + self.camera.setPos(position[0], position[1], position[2]) + if len(rotation) >= 3: + self.camera.setHpr(rotation[0], rotation[1], rotation[2]) + except Exception as e: + print(f"⚠ 恢复主相机状态失败: {e}") + + def _init_camera_controller(self): + if not self.camera_control_enabled: + return + try: + controller_module = importlib.import_module("core.CustomMouseController") + controller_cls = getattr(controller_module, "CustomMouseController", None) + if not controller_cls: + return + + self.mouse_controller = controller_cls(self) + self.mouse_controller.setUp(mouse_speed=25, move_speed=20) + self.accept("wheel_up", self._on_wheel_up) + self.accept("wheel_down", self._on_wheel_down) + except Exception as e: + print(f"⚠ 初始化主相机控制失败: {e}") + + def _on_wheel_up(self): + if not self.camera_control_enabled: + return + try: + forward = self.camera.getMat().getRow3(1) + distance = 20.0 * self.clock.dt + self.camera.setPos(self.camera.getPos() + forward * distance) + except Exception as e: + print(f"滚轮前进失败: {e}") + + def _on_wheel_down(self): + if not self.camera_control_enabled: + return + try: + forward = self.camera.getMat().getRow3(1) + distance = -20.0 * self.clock.dt + self.camera.setPos(self.camera.getPos() + forward * distance) + except Exception as e: + print(f"滚轮后退失败: {e}") + + def _resolve_media_path(self, relative_path): + if not relative_path: + return "" + if str(relative_path).startswith(("http://", "https://")): + return relative_path + if os.path.isabs(relative_path): + return relative_path + return self.get_resource_path(relative_path.replace("/", os.sep)) + + def load_full_scene(self): + scene_file = self.get_resource_path("scene.bam") + if not os.path.exists(scene_file): + print(f"⚠ 未找到场景文件: {scene_file}") + return + + metadata_scene = self.loader.loadModel(Filename.fromOsSpecific(scene_file)) + if not metadata_scene: + print("⚠ 场景文件加载失败") + return + + visible_scene = metadata_scene + use_ssbo_scene = bool(self.ssbo_runtime_importer and self.use_ssbo_scene_import) + interactive_root_names = self._collect_runtime_interactive_model_names(metadata_scene) + if use_ssbo_scene: + try: + visible_scene = self.ssbo_runtime_importer.load_scene( + scene_file, + interactive_root_names=interactive_root_names, + ) + self._ssbo_visible_scene = visible_scene + except Exception as e: + print(f"⚠ 运行时 SSBO 场景导入失败,回退普通加载: {e}") + traceback.print_exc() + visible_scene = metadata_scene + use_ssbo_scene = False + + if not use_ssbo_scene: + visible_scene.reparentTo(self.render) + + self._prepare_scene_for_render_pipeline(visible_scene) + self.process_scene_elements(metadata_scene, skip_model_nodes=use_ssbo_scene) + print("✓ 场景加载完成") + + def _collect_runtime_interactive_model_names(self, metadata_scene): + interactive_names = set() + try: + for node in metadata_scene.find_all_matches("**"): + if not node.hasTag("is_model_root"): + continue + node_name = node.getName() + if not node_name: + continue + + has_runtime_tag = ( + node.hasTag("runtime_interactive") + and node.getTag("runtime_interactive").lower() == "true" + ) + has_scripts = ( + node.hasTag("has_scripts") + and node.getTag("has_scripts").lower() == "true" + ) or node.hasTag("scripts_info") + + if has_runtime_tag or has_scripts: + interactive_names.add(node_name) + except Exception as e: + print(f"收集运行时交互模型失败: {e}") + if interactive_names: + print(f"✓ 运行时交互模型数量: {len(interactive_names)}") + return interactive_names + + def _prepare_scene_for_render_pipeline(self, scene): + self._bake_effective_geom_materials(scene) + self._ensure_geom_materials(scene) + + try: + self.render_pipeline.prepare_scene(scene) + except Exception as e: + print(f"⚠ RenderPipeline 场景预处理失败,继续使用原始场景: {e}") + traceback.print_exc() + + def _build_default_material(self): + material = Material() + material.setAmbient(Vec4(0.2, 0.2, 0.2, 1.0)) + material.setDiffuse(Vec4(0.8, 0.8, 0.8, 1.0)) + material.setSpecular(Vec4(0.0, 0.0, 0.0, 1.0)) + material.setEmission(Vec4(0.0, 0.0, 0.0, 1.0)) + material.setShininess(0.0) + return material + + def _ensure_geom_materials(self, scene): + for geom_np in scene.find_all_matches("**/+GeomNode"): + try: + geom_node = geom_np.node() + except Exception: + continue + + fallback_material = None + try: + if geom_np.hasMaterial(): + fallback_material = geom_np.getMaterial() + except Exception: + fallback_material = None + + for geom_index in range(geom_node.getNumGeoms()): + try: + geom_state = geom_node.getGeomState(geom_index) + except Exception: + continue + + material = None + try: + if geom_state.hasAttrib(MaterialAttrib): + material_attrib = geom_state.getAttrib(MaterialAttrib) + material = material_attrib.getMaterial() if material_attrib else None + except Exception: + material = None + + if material is None: + material = fallback_material or self._build_default_material() + + try: + geom_node.setGeomState( + geom_index, + geom_state.setAttrib(MaterialAttrib.make(material)), + ) + except Exception: + continue + + def _bake_effective_geom_materials(self, scene): + for geom_np in scene.find_all_matches("**/+GeomNode"): + try: + geom_node = geom_np.node() + net_state = geom_np.getNetState() + except Exception: + continue + + for geom_index in range(geom_node.getNumGeoms()): + try: + geom_state = geom_node.getGeomState(geom_index) + except Exception: + continue + + material = None + try: + if geom_state.hasAttrib(MaterialAttrib): + material_attrib = geom_state.getAttrib(MaterialAttrib) + material = material_attrib.getMaterial() if material_attrib else None + except Exception: + material = None + + if material is None: + try: + if net_state.hasAttrib(MaterialAttrib): + material_attrib = net_state.getAttrib(MaterialAttrib) + material = material_attrib.getMaterial() if material_attrib else None + except Exception: + material = None + + if material is None: + try: + if geom_np.hasMaterial(): + material = geom_np.getMaterial() + except Exception: + material = None + + if material is None: + continue + + try: + geom_node.setGeomState( + geom_index, + geom_state.setAttrib(MaterialAttrib.make(material)), + ) + except Exception: + continue + + def process_scene_elements(self, root_node, skip_model_nodes=False): + processed_lights = set() + + def walk(node_path): + self._apply_user_visibility(node_path) + + is_model_root = node_path.hasTag("is_model_root") + if skip_model_nodes and is_model_root: + runtime_target = None + if self.ssbo_runtime_importer: + runtime_target = self.ssbo_runtime_importer.get_runtime_node_for_name(node_path.getName()) + if runtime_target is None: + runtime_target = self._ssbo_visible_scene + + if node_path.hasTag("scripts_info") and runtime_target: + try: + scripts_info = json.loads(node_path.getTag("scripts_info")) + self.process_scripts(runtime_target, scripts_info) + except Exception as e: + print(f"处理 SSBO 模型根脚本失败 {node_path.getName()}: {e}") + return + + if node_path.hasTag("scripts_info"): + try: + scripts_info = json.loads(node_path.getTag("scripts_info")) + self.process_scripts(node_path, scripts_info) + except Exception as e: + print(f"处理节点脚本失败 {node_path.getName()}: {e}") + + if node_path.hasTag("light_type") and node_path not in processed_lights: + if node_path.hasTag("is_auxiliary_light") and node_path.getTag("is_auxiliary_light").lower() == "true": + return + + light_type = node_path.getTag("light_type") + if light_type == "spot_light": + self._recreate_spot_light(node_path) + elif light_type == "point_light": + self._recreate_point_light(node_path) + processed_lights.add(node_path) + + for child in node_path.getChildren(): + walk(child) + + walk(root_node) + + def _apply_user_visibility(self, node_path): + if not node_path.hasTag("user_visible"): + return + + user_visible = node_path.getTag("user_visible").lower() == "true" + node_path.setPythonTag("user_visible", user_visible) + if user_visible: + node_path.show() + else: + node_path.hide() + + def _recreate_spot_light(self, light_node): + try: + from rpcore import SpotLight + + light = SpotLight() + light.direction = Vec3(0, 0, -1) + light.fov = float(light_node.getTag("light_fov")) if light_node.hasTag("light_fov") else 70.0 + light.energy = float(light_node.getTag("light_energy")) if light_node.hasTag("light_energy") else 5000.0 + light.radius = float(light_node.getTag("light_radius")) if light_node.hasTag("light_radius") else 1000.0 + light.casts_shadows = True + light.shadow_map_resolution = 256 + light.setPos(light_node.getPos()) + self.render_pipeline.add_light(light) + except Exception as e: + print(f"创建聚光灯失败 {light_node.getName()}: {e}") + + def _recreate_point_light(self, light_node): + try: + from rpcore import PointLight + + light = PointLight() + light.energy = float(light_node.getTag("light_energy")) if light_node.hasTag("light_energy") else 5000.0 + light.radius = float(light_node.getTag("light_radius")) if light_node.hasTag("light_radius") else 1000.0 + light.inner_radius = 0.4 + light.casts_shadows = True + light.shadow_map_resolution = 256 + light.setPos(light_node.getPos()) + self.render_pipeline.add_light(light) + except Exception as e: + print(f"创建点光源失败 {light_node.getName()}: {e}") + + def process_scripts(self, node_path, script_info_list): + if not self.script_manager: + return + + for script_info in script_info_list or []: + script_name = str(script_info.get("name", "") or "").strip() + if not script_name: + continue + + try: + if script_name not in self.script_manager.loader.script_classes: + script_path = "" + if hasattr(self.script_manager, "resolve_script_path"): + script_path = self.script_manager.resolve_script_path(script_info) + if script_path: + self.script_manager.load_script_from_file(script_path) + + script_component = self.script_manager.add_script_to_object(node_path, script_name) + if script_component: + print(f"✓ 脚本 {script_name} 已挂载到 {node_path.getName()}") + else: + print(f"⚠ 脚本 {script_name} 挂载失败") + except Exception as e: + print(f"挂载脚本失败 {script_name}: {e}") + + def load_gui_from_json(self): + gui_json_path = self.get_resource_path(os.path.join("gui", "gui_elements.json")) + if not os.path.exists(gui_json_path): + return + + gui_data = self._load_json_file(gui_json_path, []) + if not gui_data: + return + self.create_gui_elements(gui_data) + + def create_gui_elements(self, element_data): + processed_names = set() + created_elements = {} + pending_parent_links = [] + + for index, gui_info in enumerate(element_data or []): + try: + gui_type = gui_info.get("type", "unknown") + name = gui_info.get("name", f"gui_element_{index}") + if name in processed_names: + continue + processed_names.add(name) + + position = list(gui_info.get("position", [0, 0, 0])) + scale = list(gui_info.get("scale", [1, 1, 1])) + parent_name = gui_info.get("parent_name") + + text = gui_info.get("text", "") + raw_image_path = str(gui_info.get("image_path", "") or "") + raw_video_path = str(gui_info.get("video_path", "") or "") + image_path = self._resolve_media_path(raw_image_path) + video_path = self._resolve_media_path(raw_video_path) + + new_element = None + if gui_type == "3d_text": + new_element = self.create_gui_3d_text(tuple(position), text, scale[0] if scale else 1.0) + elif gui_type == "3d_image": + new_element = self.create_gui_3d_image(tuple(position), image_path, scale) + elif gui_type == "button": + new_element = self.create_gui_button(tuple(position), text, scale[0] if scale else 1.0) + elif gui_type == "label": + new_element = self.create_gui_label(tuple(position), text, scale[0] if scale else 1.0) + elif gui_type == "entry": + new_element = self.create_gui_entry(tuple(position), text, scale[0] if scale else 1.0) + elif gui_type == "2d_image": + new_element = self.create_gui_2d_image(tuple(position), image_path, scale) + elif gui_type == "video_screen": + new_element = self.create_gui_video_screen(tuple(position), scale, video_path) + elif gui_type == "2d_video_screen": + new_element = self.create_gui_2d_video_screen(tuple(position), scale, video_path) + + if not new_element: + continue + + self._apply_gui_metadata(new_element, gui_info, gui_type, text, raw_image_path, raw_video_path) + self.gui_elements.append(new_element) + created_elements[name] = new_element + + if parent_name: + pending_parent_links.append((new_element, parent_name)) + + if gui_info.get("scripts"): + self.process_scripts(new_element, gui_info["scripts"]) + except Exception as e: + print(f"重建 GUI 元素失败: {e}") + traceback.print_exc() + + for child, parent_name in pending_parent_links: + parent = created_elements.get(parent_name) + if not parent: + continue + try: + child.reparentTo(parent) + except Exception as e: + print(f"恢复 GUI 父子关系失败 {parent_name}: {e}") + + def _apply_gui_metadata(self, node, gui_info, gui_type, text, image_path, video_path): + try: + name = gui_info.get("name") + if name and hasattr(node, "setName"): + node.setName(name) + except Exception: + pass + + try: + position = gui_info.get("position", []) + if len(position) >= 3 and hasattr(node, "setPos"): + node.setPos(position[0], position[1], position[2]) + except Exception: + pass + + try: + rotation = gui_info.get("rotation", []) + if len(rotation) >= 3 and hasattr(node, "setHpr"): + node.setHpr(rotation[0], rotation[1], rotation[2]) + except Exception: + pass + + try: + scale = gui_info.get("scale", []) + if hasattr(node, "setScale"): + if isinstance(scale, (list, tuple)): + if len(scale) >= 3: + node.setScale(scale[0], scale[1], scale[2]) + elif len(scale) == 1: + node.setScale(scale[0]) + elif scale: + node.setScale(scale) + except Exception: + pass + + if hasattr(node, "setTag"): + node.setTag("gui_type", gui_type) + node.setTag("saved_gui_type", gui_type) + node.setTag("is_gui_element", "true") + if text: + node.setTag("gui_text", str(text)) + if image_path: + node.setTag("gui_image_path", str(image_path)) + node.setTag("image_path", str(image_path)) + if video_path: + node.setTag("video_path", str(video_path)) + for key, value in (gui_info.get("tags") or {}).items(): + try: + node.setTag(str(key), str(value)) + except Exception: + continue + + user_visible = bool(gui_info.get("user_visible", True)) + if hasattr(node, "setPythonTag"): + node.setPythonTag("user_visible", user_visible) + if hasattr(node, "show") and hasattr(node, "hide"): + if user_visible: + node.show() + else: + node.hide() + + def create_gui_button(self, pos=(0, 0, 0), text="按钮", size=0.1): + from direct.gui.DirectGui import DirectButton + + return DirectButton( + text=text, + pos=(pos[0], pos[1], pos[2]), + scale=size, + frameColor=(0.2, 0.6, 0.8, 1), + text_font=self.get_chinese_font() or None, + rolloverSound=None, + clickSound=None, + parent=self.aspect2d, + command=None, + ) + + def create_gui_label(self, pos=(0, 0, 0), text="标签", size=0.08): + from direct.gui.DirectGui import DirectLabel + + return DirectLabel( + text=text, + pos=(pos[0], pos[1], pos[2]), + scale=size, + frameColor=(0, 0, 0, 0), + text_fg=(1, 1, 1, 1), + text_font=self.get_chinese_font() or None, + text_align=TextNode.ACenter, + text_mayChange=True, + parent=self.aspect2d, + ) + + def create_gui_entry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08): + from direct.gui.DirectGui import DirectEntry + + return DirectEntry( + text="", + pos=(pos[0], pos[1], pos[2]), + scale=size, + command=self.on_gui_entry_submit, + initialText=placeholder, + numLines=1, + width=12, + focus=0, + frameColor=(0, 0, 0, 0), + text_fg=(1, 1, 1, 1), + text_font=self.get_chinese_font() or None, + text_align=TextNode.ACenter, + text_mayChange=True, + parent=self.aspect2d, + rolloverSound=None, + clickSound=None, + suppressKeys=True, + suppressMouse=True, + ) + + def on_gui_entry_submit(self, text, *_args): + print(f"GUI 输入框提交: {text}") + + def create_gui_2d_image(self, pos=(0, 0, 0), image_path="", size=(1, 1, 1)): + if isinstance(size, (list, tuple)) and len(size) >= 3: + width_scale = float(size[0]) * 0.2 + height_scale = float(size[2]) * 0.2 + else: + scalar = float(size) if isinstance(size, (int, float)) else 0.2 + width_scale = scalar * 0.1 + height_scale = width_scale + + cm = CardMaker("gui_2d_image") + cm.setFrame(-width_scale, width_scale, -height_scale, height_scale) + image_node = self.aspect2d.attachNewNode(cm.generate()) + image_node.setPos(pos) + image_node.setBin("fixed", 0) + image_node.setDepthWrite(False) + image_node.setDepthTest(False) + image_node.setTransparency(TransparencyAttrib.MAlpha) + if image_path: + texture = self.loader.loadTexture(image_path) + if texture: + image_node.setTexture(texture, 1) + return image_node + + def create_gui_3d_text(self, pos=(0, 0, 0), text="3D文本", size=0.5): + text_node = TextNode("gui_3d_text") + text_node.setText(text) + text_node.setAlign(TextNode.ACenter) + if self.get_chinese_font(): + text_node.setFont(self.get_chinese_font()) + + text_np = self.render.attachNewNode(text_node) + text_np.setPos(Vec3(pos[0], pos[1], pos[2])) + text_np.setScale(size) + text_np.setBin("fixed", 40) + text_np.setDepthWrite(False) + return text_np + + def create_gui_3d_image(self, pos=(0, 0, 0), image_path="", size=(1, 1, 1)): + if isinstance(size, (list, tuple)) and len(size) >= 3: + width_scale = float(size[0]) + height_scale = float(size[2]) + else: + width_scale = float(size) if isinstance(size, (int, float)) else 1.0 + height_scale = width_scale + + cm = CardMaker("gui_3d_image") + cm.setFrame(-width_scale, width_scale, -height_scale, height_scale) + image_node = self.render.attachNewNode(cm.generate()) + image_node.setPos(pos) + image_node.setTransparency(TransparencyAttrib.MAlpha) + if image_path: + texture = self.loader.loadTexture(image_path) + if texture: + image_node.setTexture(texture, 1) + return image_node + + def _load_movie_texture(self, name, video_path): + if not video_path: + return None + if str(video_path).startswith(("http://", "https://")): + print(f"⚠ 当前运行时仅支持本地视频文件: {video_path}") + return None + movie_texture = MovieTexture(name) + if not movie_texture.read(Filename.fromOsSpecific(video_path)): + print(f"⚠ 无法加载视频: {video_path}") + return None + movie_texture.play() + return movie_texture + + def create_gui_video_screen(self, pos=(0, 0, 0), size=(1, 1, 1), video_path=""): + if isinstance(size, (list, tuple)) and len(size) >= 3: + width_scale = float(size[0]) + height_scale = float(size[2]) + else: + width_scale = float(size) if isinstance(size, (int, float)) else 1.0 + height_scale = width_scale + + cm = CardMaker("gui_video_screen") + cm.setFrame(-width_scale, width_scale, -height_scale, height_scale) + video_node = self.render.attachNewNode(cm.generate()) + video_node.setPos(pos) + video_node.setTransparency(TransparencyAttrib.MAlpha) + + movie_texture = self._load_movie_texture("gui_video_texture_3d", video_path) + if movie_texture: + video_node.setTexture(movie_texture, 1) + video_node.setPythonTag("movie_texture", movie_texture) + self._movie_textures.append(movie_texture) + return video_node + + def create_gui_2d_video_screen(self, pos=(0, 0, 0), size=(1, 1, 1), video_path=""): + if isinstance(size, (list, tuple)) and len(size) >= 3: + width_scale = float(size[0]) * 0.2 + height_scale = float(size[2]) * 0.2 + else: + scalar = float(size) if isinstance(size, (int, float)) else 0.2 + width_scale = scalar * 0.1 + height_scale = width_scale + + cm = CardMaker("gui_2d_video_screen") + cm.setFrame(-width_scale, width_scale, -height_scale, height_scale) + video_node = self.aspect2d.attachNewNode(cm.generate()) + video_node.setPos(pos) + video_node.setTransparency(TransparencyAttrib.MAlpha) + video_node.setDepthWrite(False) + video_node.setDepthTest(False) + + movie_texture = self._load_movie_texture("gui_video_texture_2d", video_path) + if movie_texture: + video_node.setTexture(movie_texture, 1) + video_node.setPythonTag("movie_texture", movie_texture) + self._movie_textures.append(movie_texture) + return video_node + + +if __name__ == "__main__": + try: + app = MainApp() + app.run() + except Exception as e: + print(f"应用程序启动失败: {e}") + traceback.print_exc() diff --git a/ui/panels/app_actions.py b/ui/panels/app_actions.py index 0fecf763..11603a68 100644 --- a/ui/panels/app_actions.py +++ b/ui/panels/app_actions.py @@ -270,6 +270,38 @@ class AppActions: self.add_info_message("打开另存为项目对话框") self.show_save_as_dialog = True + def _on_build_project(self): + """处理打包项目菜单项。""" + current_project_path = getattr(getattr(self, "project_manager", None), "current_project_path", None) + if current_project_path: + self.build_output_path = os.path.dirname(current_project_path) + else: + self.build_output_path = os.getcwd() + self.add_info_message("打开打包项目对话框") + self.show_build_project_dialog = True + + def _build_project_impl(self, output_path): + """执行项目打包。""" + if not hasattr(self, "project_manager") or not self.project_manager: + self.add_error_message("项目管理器未初始化") + return False + + if not output_path or not output_path.strip(): + self.add_warning_message("请选择打包输出目录") + return False + + try: + build_success = self.project_manager.buildPackage(output_path.strip()) + if build_success: + self.add_success_message("项目打包成功") + return True + + self.add_error_message("项目打包失败") + return False + except Exception as e: + self.add_error_message(f"项目打包失败: {e}") + return False + def _show_about_dialog(self): """显示关于对话框。""" @@ -1033,71 +1065,16 @@ class AppActions: def _open_project_impl(self, project_path): """打开项目的具体实现(不依赖Qt)""" - import json - import datetime - try: - # 检查项目管理器是否已初始化 if not hasattr(self, 'project_manager') or not self.project_manager: print("✗ 项目管理器未初始化") self.add_error_message("项目管理器未初始化") return False - - # 检查场景管理器是否已初始化 - if not hasattr(self, 'scene_manager') or not self.scene_manager: - print("✗ 场景管理器未初始化") - self.add_error_message("场景管理器未初始化") - return False - - # 检查是否是有效的项目文件夹 - config_file = os.path.join(project_path, "project.json") - if not os.path.exists(config_file): - print(f"⚠ 选择的不是有效的项目文件夹: {project_path}") - self.add_warning_message(f"选择的不是有效的项目文件夹: {project_path}") + + if not self.project_manager.openProject(project_path): + self.add_error_message(f"项目打开失败: {project_path}") return False - # 读取项目配置 - try: - with open(config_file, "r", encoding="utf-8") as f: - project_config = json.load(f) - except Exception as e: - print(f"✗ 读取项目配置文件失败: {e}") - self.add_error_message(f"读取项目配置文件失败: {e}") - return False - - # 检查场景文件 - scene_file = os.path.join(project_path, "scenes", "scene.bam") - if os.path.exists(scene_file): - if getattr(self, "use_ssbo_mouse_picking", False) and callable(getattr(self, "_import_model_for_runtime", None)): - self.use_ssbo_scene_import = True - # 加载场景 - try: - if self.scene_manager.loadScene(scene_file): - # 更新项目配置 - project_config["scene_file"] = os.path.relpath(scene_file, project_path) - print(f"✓ 场景加载成功: {scene_file}") - else: - print(f"⚠ 场景加载失败: {scene_file}") - self.add_warning_message(f"场景加载失败: {scene_file}") - except Exception as e: - print(f"✗ 加载场景时发生错误: {e}") - self.add_error_message(f"加载场景时发生错误: {e}") - # 继续执行,不阻止项目打开 - - # 更新项目配置 - project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - try: - with open(config_file, "w", encoding="utf-8") as f: - json.dump(project_config, f, ensure_ascii=False, indent=4) - except Exception as e: - print(f"✗ 保存项目配置失败: {e}") - self.add_error_message(f"保存项目配置失败: {e}") - - # 更新项目状态 - self.project_manager.current_project_path = project_path - self.project_manager.project_config = project_config - - # 更新窗口标题 project_name = os.path.basename(project_path) self._update_window_title(project_name) @@ -1113,45 +1090,12 @@ class AppActions: def _create_new_project_impl(self, name, path): """创建新项目的具体实现(不依赖Qt)""" - full_project_path = os.path.normpath(os.path.join(path, name)) - print(f"创建项目路径: {full_project_path}") - try: - # 创建项目文件夹结构 - os.makedirs(full_project_path) - os.makedirs(os.path.join(full_project_path, "models")) # 模型文件夹 - os.makedirs(os.path.join(full_project_path, "textures")) # 贴图文件夹 - scenes_path = os.path.join(full_project_path, "scenes") # 场景文件夹 - os.makedirs(scenes_path) - - # 创建项目配置文件 - project_config = { - "name": name, - "path": full_project_path, - "created": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "last_modified": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "version": "1.0", - "scene_file": "scenes/scene.bam" - } - - # 保存项目配置 - config_file = os.path.join(full_project_path, "project.json") - with open(config_file, "w", encoding="utf-8") as f: - json.dump(project_config, f, ensure_ascii=False, indent=4) - - # 保存初始场景 - scene_file = os.path.join(scenes_path, "scene.bam") - self.scene_manager.saveScene(scene_file, full_project_path) - - # 更新项目管理器状态 - self.project_manager.current_project_path = full_project_path - self.project_manager.project_config = project_config - - # 更新窗口标题 + if not self.project_manager.createNewProject(path, name): + return False + self._update_window_title(name) - return True - except Exception as e: print(f"创建项目失败: {e}") return False @@ -1192,9 +1136,11 @@ class AppActions: os.makedirs(os.path.join(full_project_path, "models"), exist_ok=True) os.makedirs(os.path.join(full_project_path, "textures"), exist_ok=True) os.makedirs(os.path.join(full_project_path, "scenes"), exist_ok=True) + os.makedirs(os.path.join(full_project_path, "scripts"), exist_ok=True) previous_project_path = self.project_manager.current_project_path previous_project_config = dict(self.project_manager.project_config or {}) + previous_scripts_dir = getattr(getattr(self, "script_manager", None), "scripts_directory", "") timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") project_config = dict(previous_project_config) @@ -1217,12 +1163,25 @@ class AppActions: with open(config_file, "w", encoding="utf-8") as f: json.dump(project_config, f, ensure_ascii=False, indent=4) + source_scripts_dir = "" + if previous_project_path: + source_scripts_dir = self.project_manager.get_project_scripts_dir(previous_project_path) + if not source_scripts_dir: + source_scripts_dir = previous_scripts_dir + + target_scripts_dir = self.project_manager.get_project_scripts_dir(full_project_path) + if source_scripts_dir and os.path.exists(source_scripts_dir): + self._copy_directory_contents(source_scripts_dir, target_scripts_dir) + self.project_manager.current_project_path = full_project_path self.project_manager.project_config = project_config + self.project_manager._sync_project_script_manager(full_project_path, reload_scripts=True) if not self.project_manager.saveProject(): self.project_manager.current_project_path = previous_project_path self.project_manager.project_config = previous_project_config + if previous_project_path: + self.project_manager._sync_project_script_manager(previous_project_path, reload_scripts=True) self._update_window_title(os.path.basename(previous_project_path) if previous_project_path else "未命名项目") self.add_error_message("项目另存为失败") return False @@ -1233,6 +1192,23 @@ class AppActions: except Exception as e: self.add_error_message(f"项目另存为失败: {e}") return False + + def _copy_directory_contents(self, source_dir, target_dir): + """Copy a directory tree into target_dir, replacing existing files.""" + import shutil + + if not source_dir or not os.path.exists(source_dir): + return + + os.makedirs(target_dir, exist_ok=True) + for root, _, files in os.walk(source_dir): + rel_root = os.path.relpath(root, source_dir) + destination_root = target_dir if rel_root == "." else os.path.join(target_dir, rel_root) + os.makedirs(destination_root, exist_ok=True) + for file_name in files: + source_file = os.path.join(root, file_name) + target_file = os.path.join(destination_root, file_name) + shutil.copy2(source_file, target_file) def _update_window_title(self, project_name): diff --git a/ui/panels/dialog_panels.py b/ui/panels/dialog_panels.py index f32e52c3..aa794337 100644 --- a/ui/panels/dialog_panels.py +++ b/ui/panels/dialog_panels.py @@ -203,6 +203,51 @@ class DialogPanels: imgui.same_line() if imgui.button("取消"): self.show_save_as_dialog = False + + def _draw_build_project_dialog(self): + """绘制项目打包对话框。""" + if not getattr(self, "show_build_project_dialog", False): + return + + if not getattr(self, "build_output_path", "").strip(): + current_project_path = getattr(getattr(self, "project_manager", None), "current_project_path", None) + self.build_output_path = os.path.dirname(current_project_path) if current_project_path else os.getcwd() + + flags = ( + imgui.WindowFlags_.no_resize + | imgui.WindowFlags_.no_collapse + | imgui.WindowFlags_.modal + ) + self.style_manager.prepare_centered_dialog(480, 220) + + with imgui_ctx.begin("打包项目", True, flags) as window: + if not window.opened: + self.show_build_project_dialog = False + return + + imgui.text("生成当前项目的最终运行程序") + imgui.separator() + + changed, output_path = imgui.input_text("输出目录", self.build_output_path, 512) + if changed: + self.build_output_path = output_path + + imgui.same_line() + if imgui.button("浏览..."): + self.path_browser_mode = "build_project" + self.path_browser_current_path = self.build_output_path if self.build_output_path else os.getcwd() + self.show_path_browser = True + self._refresh_path_browser() + + imgui.separator() + + if imgui.button("开始打包"): + if self._build_project_impl(self.build_output_path): + self.show_build_project_dialog = False + + imgui.same_line() + if imgui.button("取消"): + self.show_build_project_dialog = False def _draw_path_browser(self): @@ -465,6 +510,9 @@ class DialogPanels: # 另存为项目模式:使用当前路径作为目标目录 self.save_as_project_path = self.path_browser_current_path self.add_info_message(f"已选择保存位置: {self.save_as_project_path}") + elif self.path_browser_mode == "build_project": + self.build_output_path = self.path_browser_current_path + self.add_info_message(f"已选择打包位置: {self.build_output_path}") elif self.path_browser_mode == "import_model": # 导入模型模式:使用选择的文件路径 self.import_file_path = self.path_browser_selected_path diff --git a/ui/panels/editor_panels_top.py b/ui/panels/editor_panels_top.py index 0103f702..f563f8cb 100644 --- a/ui/panels/editor_panels_top.py +++ b/ui/panels/editor_panels_top.py @@ -187,6 +187,8 @@ class EditorPanelsTopMixin: self.app._on_save_project() if imgui.menu_item("另存为", "", False, True)[1]: self.app._on_save_as_project() + if imgui.menu_item("打包项目", "", False, True)[1]: + self.app._on_build_project() imgui.separator() if imgui.menu_item("退出", "Alt+F4", False, True)[1]: self.app._on_exit() diff --git a/ui/panels/panel_delegates.py b/ui/panels/panel_delegates.py index 36929428..b83104b6 100644 --- a/ui/panels/panel_delegates.py +++ b/ui/panels/panel_delegates.py @@ -459,6 +459,12 @@ class PanelDelegates: def _on_save_as_project(self, *args, **kwargs): return self.app_actions._on_save_as_project(*args, **kwargs) + def _on_build_project(self, *args, **kwargs): + return self.app_actions._on_build_project(*args, **kwargs) + + def _build_project_impl(self, *args, **kwargs): + return self.app_actions._build_project_impl(*args, **kwargs) + def _show_about_dialog(self, *args, **kwargs): return self.app_actions._show_about_dialog(*args, **kwargs) @@ -588,6 +594,9 @@ class PanelDelegates: def _draw_save_as_project_dialog(self, *args, **kwargs): return self.dialog_panels._draw_save_as_project_dialog(*args, **kwargs) + def _draw_build_project_dialog(self, *args, **kwargs): + return self.dialog_panels._draw_build_project_dialog(*args, **kwargs) + def _draw_path_browser(self, *args, **kwargs): return self.dialog_panels._draw_path_browser(*args, **kwargs)