diff --git a/editor.py b/editor.py new file mode 100644 index 0000000..25b1531 --- /dev/null +++ b/editor.py @@ -0,0 +1,389 @@ +""" +Lity3D Editor - 引擎集成开发环境 +双击打开,点 Run 运行游戏,点 Build 打包 exe +""" +import tkinter as tk +from tkinter import ttk, filedialog, scrolledtext, messagebox +import os, subprocess, json, sys, time + +ENGINE_DIR = os.path.dirname(os.path.abspath(__file__)) + +class LityEditor: + def __init__(self): + self.root = tk.Tk() + self.root.title("Lity3D Editor") + self.root.geometry("1400x900") + self.root.configure(bg="#2b2d30") + + self.current_file = None + self.process = None + + self._setup_styles() + self._build_menu() + self._build_toolbar() + self._build_main_layout() + self._build_statusbar() + + self._load_recent() + self.root.protocol("WM_DELETE_WINDOW", self._on_close) + + def _setup_styles(self): + s = ttk.Style() + s.theme_use("clam") + s.configure("Dark.TFrame", background="#2b2d30") + s.configure("Dark.TLabel", background="#2b2d30", foreground="#cccccc") + s.configure("Tool.TButton", padding=4) + s.configure("Run.TButton", padding=6, font=("Segoe UI", 10, "bold")) + + def _build_menu(self): + mb = tk.Menu(self.root, bg="#2b2d30", fg="#cccccc") + + fm = tk.Menu(mb, tearoff=0, bg="#3c3f41", fg="#cccccc") + fm.add_command(label="New File", accelerator="Ctrl+N", command=self._new_file) + fm.add_command(label="Open File", accelerator="Ctrl+O", command=self._open_file) + fm.add_command(label="Open Folder", command=self._open_folder) + fm.add_separator() + fm.add_command(label="Save", accelerator="Ctrl+S", command=self._save_file) + fm.add_command(label="Save As", command=self._save_as) + fm.add_separator() + fm.add_command(label="Exit", command=self._on_close) + mb.add_cascade(label="File", menu=fm) + + em = tk.Menu(mb, tearoff=0, bg="#3c3f41", fg="#cccccc") + em.add_command(label="Run Game", accelerator="F5", command=self._run_game) + em.add_command(label="Run Current File", accelerator="Ctrl+F5", command=self._run_current) + em.add_separator() + em.add_command(label="Build Executable", accelerator="F7", command=self._build_exe) + em.add_command(label="Build Engine", command=self._build_engine) + em.add_separator() + em.add_command(label="Kill Process", accelerator="Shift+F5", command=self._kill_process) + mb.add_cascade(label="Build", menu=em) + + sm = tk.Menu(mb, tearoff=0, bg="#3c3f41", fg="#cccccc") + sm.add_command(label="New Scene", command=self._new_scene) + sm.add_command(label="Open Scene", command=self._open_scene) + sm.add_command(label="Save Scene", command=self._save_scene) + sm.add_separator() + sm.add_command(label="Run Scene", command=self._run_scene) + mb.add_cascade(label="Scene", menu=sm) + + hm = tk.Menu(mb, tearoff=0, bg="#3c3f41", fg="#cccccc") + hm.add_command(label="About Lity3D", command=lambda: messagebox.showinfo("Lity3D", "Lity3D Engine v1.0\n自研 C+MASM 游戏引擎\nPython 脚本 + D3D11 渲染")) + mb.add_cascade(label="Help", menu=hm) + + self.root.config(menu=mb) + self.root.bind("", lambda e: self._new_file()) + self.root.bind("", lambda e: self._open_file()) + self.root.bind("", lambda e: self._save_file()) + self.root.bind("", lambda e: self._run_game()) + self.root.bind("", lambda e: self._build_exe()) + self.root.bind("", lambda e: self._kill_process()) + + def _build_toolbar(self): + tb = ttk.Frame(self.root, style="Dark.TFrame") + tb.pack(side=tk.TOP, fill=tk.X, padx=2, pady=2) + + ttk.Button(tb, text="New", style="Tool.TButton", command=self._new_file).pack(side=tk.LEFT, padx=1) + ttk.Button(tb, text="Open", style="Tool.TButton", command=self._open_file).pack(side=tk.LEFT, padx=1) + ttk.Button(tb, text="Save", style="Tool.TButton", command=self._save_file).pack(side=tk.LEFT, padx=1) + + ttk.Separator(tb, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx=4) + + run_btn = ttk.Button(tb, text="Run (F5)", style="Run.TButton", command=self._run_game) + run_btn.pack(side=tk.LEFT, padx=2) + ttk.Button(tb, text="Stop", style="Tool.TButton", command=self._kill_process).pack(side=tk.LEFT, padx=1) + ttk.Button(tb, text="Build (F7)", style="Tool.TButton", command=self._build_exe).pack(side=tk.LEFT, padx=2) + + ttk.Separator(tb, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx=4) + + ttk.Button(tb, text="New Scene", style="Tool.TButton", command=self._new_scene).pack(side=tk.LEFT, padx=1) + ttk.Button(tb, text="Save Scene", style="Tool.TButton", command=self._save_scene).pack(side=tk.LEFT, padx=1) + + ttk.Separator(tb, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx=4) + + ttk.Button(tb, text="LityPy Docs", style="Tool.TButton", command=self._open_docs).pack(side=tk.LEFT, padx=1) + + def _build_main_layout(self): + main = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL) + main.pack(fill=tk.BOTH, expand=True, padx=2, pady=2) + + left = ttk.Frame(main, style="Dark.TFrame") + main.add(left, weight=1) + + ttk.Label(left, text="EXPLORER", style="Dark.TLabel", font=("Segoe UI", 9, "bold")).pack(anchor=tk.W, padx=5, pady=3) + + self.file_tree = ttk.Treeview(left, show="tree", height=20) + self.file_tree.pack(fill=tk.BOTH, expand=True, padx=3, pady=3) + self.file_tree.bind("", self._on_tree_double) + + right = ttk.Frame(main, style="Dark.TFrame") + main.add(right, weight=3) + + self.notebook = ttk.Notebook(right) + self.notebook.pack(fill=tk.BOTH, expand=True) + + self.editor_frame = ttk.Frame(self.notebook) + self.notebook.add(self.editor_frame, text="Editor") + + self.code_text = scrolledtext.ScrolledText( + self.editor_frame, bg="#1e1e1e", fg="#d4d4d4", + insertbackground="#d4d4d4", selectbackground="#264f78", + font=("Consolas", 12), wrap=tk.NONE, + undo=True, maxundo=-1 + ) + self.code_text.pack(fill=tk.BOTH, expand=True) + + bottom = ttk.Frame(right, style="Dark.TFrame") + bottom.pack(fill=tk.BOTH, expand=False) + + bottom_pw = ttk.PanedWindow(bottom, orient=tk.VERTICAL) + bottom_pw.pack(fill=tk.BOTH, expand=True) + + console_frame = ttk.Frame(bottom_pw) + bottom_pw.add(console_frame, weight=1) + + ttk.Label(console_frame, text="OUTPUT", style="Dark.TLabel", font=("Segoe UI", 9, "bold")).pack(anchor=tk.W, padx=5) + + self.console = scrolledtext.ScrolledText( + console_frame, bg="#1e1e1e", fg="#cccccc", + font=("Consolas", 10), height=8, state=tk.DISABLED + ) + self.console.pack(fill=tk.BOTH, expand=True) + + self._refresh_tree() + + def _build_statusbar(self): + self.statusbar = ttk.Frame(self.root, style="Dark.TFrame") + self.statusbar.pack(side=tk.BOTTOM, fill=tk.X) + + self.status_label = ttk.Label(self.statusbar, text="Ready", style="Dark.TLabel") + self.status_label.pack(side=tk.LEFT, padx=5) + + self.process_label = ttk.Label(self.statusbar, text="", style="Dark.TLabel", foreground="#6a9955") + self.process_label.pack(side=tk.RIGHT, padx=5) + + def _log(self, msg, tag="info"): + self.console.config(state=tk.NORMAL) + ts = time.strftime("%H:%M:%S") + prefix = {"info": " ", "error": "!!", "success": "OK"}[tag] + self.console.insert(tk.END, f"[{ts}] {prefix} {msg}\n") + self.console.see(tk.END) + self.console.config(state=tk.DISABLED) + + def _set_status(self, msg): + self.status_label.config(text=msg) + + def _refresh_tree(self): + self.file_tree.delete(*self.file_tree.get_children()) + root_node = self.file_tree.insert("", "end", text="lity3d", open=True) + for item in sorted(os.listdir(ENGINE_DIR)): + full = os.path.join(ENGINE_DIR, item) + if item.startswith(".") or item in ("build", "__pycache__", ".git", "obj"): continue + if os.path.isdir(full): + dir_node = self.file_tree.insert(root_node, "end", text=item, open=False) + try: + for f in sorted(os.listdir(full)): + if f.startswith(".") or f.endswith((".pyc", ".exe")): continue + self.file_tree.insert(dir_node, "end", text=f, values=(os.path.join(item, f),)) + except: pass + else: + self.file_tree.insert(root_node, "end", text=item, values=(item,)) + + def _on_tree_double(self, event): + sel = self.file_tree.selection() + if not sel: return + item = self.file_tree.item(sel[0]) + vals = item.get("values", ()) + path = vals[0] if vals else item["text"] + full = os.path.join(ENGINE_DIR, path) if not os.path.isabs(path) else path + if os.path.isfile(full): + self._open_file_at(full) + + def _new_file(self): + self.code_text.delete("1.0", tk.END) + self.current_file = None + self._set_status("New file") + self.notebook.tab(self.editor_frame, text="untitled*") + + def _open_file(self): + path = filedialog.askopenfilename( + initialdir=ENGINE_DIR, + filetypes=[("Python", "*.py"), ("C Source", "*.c *.h"), ("All", "*.*")] + ) + if path: self._open_file_at(path) + + def _open_file_at(self, path): + try: + with open(path, "r", encoding="utf-8", errors="replace") as f: + content = f.read() + self.code_text.delete("1.0", tk.END) + self.code_text.insert("1.0", content) + self.current_file = path + name = os.path.basename(path) + self.notebook.tab(self.editor_frame, text=name) + self._set_status(f"Opened: {name}") + self._log(f"Opened {path}") + except Exception as e: + self._log(f"Error: {e}", "error") + + def _open_folder(self): + path = filedialog.askdirectory(initialdir=ENGINE_DIR) + if path: self._log(f"Folder: {path}") + + def _save_file(self): + if not self.current_file: + self._save_as() + return + try: + content = self.code_text.get("1.0", tk.END) + with open(self.current_file, "w", encoding="utf-8") as f: + f.write(content) + name = os.path.basename(self.current_file) + self.notebook.tab(self.editor_frame, text=name) + self._log(f"Saved {name}", "success") + self._set_status(f"Saved: {name}") + except Exception as e: + self._log(f"Save error: {e}", "error") + + def _save_as(self): + path = filedialog.asksaveasfilename( + initialdir=ENGINE_DIR, + defaultextension=".py", + filetypes=[("Python", "*.py"), ("C Source", "*.c"), ("All", "*.*")] + ) + if path: + self.current_file = path + self._save_file() + + def _run_game(self): + exe = os.path.join(ENGINE_DIR, "build", "lity.exe") + if os.path.exists(exe): + self._run_process([exe]) + else: + self._log("lity.exe not found. Build first (F7).", "error") + + def _run_current(self): + if not self.current_file: + self._log("No file open", "error"); return + ext = os.path.splitext(self.current_file)[1] + if ext == ".py": + self._run_process([sys.executable, self.current_file]) + elif ext in (".ly", ".lity"): + interp = os.path.join(ENGINE_DIR, "build", "ly_interpreter.exe") + if os.path.exists(interp): + self._run_process([interp, self.current_file]) + else: + self._log("ly_interpreter not built", "error") + else: + self._log(f"Cannot run {ext} files", "error") + + def _run_scene(self): + scene = os.path.join(ENGINE_DIR, "scene.txt") + if not os.path.exists(scene): + self._log("No scene.txt found", "error"); return + exe = os.path.join(ENGINE_DIR, "build", "lity.exe") + if os.path.exists(exe): + self._run_process([exe]) + else: + self._log("Build engine first (F7)", "error") + + def _run_process(self, cmd): + self._kill_process() + self._log(f"Running: {' '.join(cmd)}") + self.process_label.config(text="Running...") + try: + self.process = subprocess.Popen( + cmd, cwd=ENGINE_DIR, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + creationflags=subprocess.CREATE_NO_WINDOW + ) + self._poll_process() + except Exception as e: + self._log(f"Launch error: {e}", "error") + self.process_label.config(text="") + + def _poll_process(self): + if self.process and self.process.poll() is None: + line = self.process.stdout.readline() + if line: + self._log(line.decode(errors="replace").rstrip()) + self.root.after(50, self._poll_process) + elif self.process: + code = self.process.returncode + tag = "success" if code == 0 else "error" + self._log(f"Process exited with code {code}", tag) + self.process_label.config(text="") + self.process = None + + def _kill_process(self): + if self.process: + self.process.kill() + self._log("Process killed", "error") + self.process_label.config(text="") + self.process = None + + def _build_engine(self): + self._log("Building engine with CMake...") + cmake_dir = os.path.join(ENGINE_DIR, "build") + os.makedirs(cmake_dir, exist_ok=True) + self._run_process(["cmake", "--build", cmake_dir]) + + def _build_exe(self): + self._save_file() + exe = os.path.join(ENGINE_DIR, "build", "lity.exe") + if os.path.exists(exe): + out = filedialog.asksaveasfilename( + initialdir=ENGINE_DIR, + defaultextension=".exe", + filetypes=[("Executable", "*.exe")] + ) + if out: + import shutil + shutil.copy2(exe, out) + self._log(f"Packaged to {out}", "success") + else: + self._log("Build engine first", "error") + + def _new_scene(self): + scene = {"entities": [], "camera": {"pos": [0,5,-10], "target": [0,0,0]}} + path = filedialog.asksaveasfilename( + initialdir=ENGINE_DIR, defaultextension=".json", + filetypes=[("Scene", "*.json")] + ) + if path: + with open(path, "w") as f: json.dump(scene, f, indent=2) + self.current_file = path + self._open_file_at(path) + self._log(f"New scene: {os.path.basename(path)}", "success") + + def _open_scene(self): + path = filedialog.askopenfilename( + initialdir=ENGINE_DIR, + filetypes=[("Scene", "*.json *.txt")] + ) + if path: self._open_file_at(path) + + def _save_scene(self): + self._save_file() + + def _open_docs(self): + docs = os.path.join(ENGINE_DIR, "Engine", "Scripting", "lity.py") + if os.path.exists(docs): + self._open_file_at(docs) + else: + self._log("LityPy docs not found", "error") + + def _load_recent(self): + self._set_status("Lity3D Editor v1.0 | F5=Run | F7=Build | Ctrl+S=Save") + + def _on_close(self): + if self.process: + self.process.kill() + self.root.destroy() + + def run(self): + self.root.mainloop() + +if __name__ == "__main__": + editor = LityEditor() + editor.run() diff --git a/引擎文件说明.md b/引擎文件说明.md new file mode 100644 index 0000000..eca5010 --- /dev/null +++ b/引擎文件说明.md @@ -0,0 +1,143 @@ +# Lity3D 引擎文件说明 + +## 顶层结构 + +``` +lity3d/ +├── Engine/ 核心引擎代码(C + MASM汇编) +├── Demo/ 示例程序 +├── Assets/ 3D模型资源 +├── EditorCS/ 编辑器(C#) +├── build/ 编译输出 +├── CMakeLists.txt CMake构建配置 +├── build.bat Windows一键编译 +├── README.md 项目说明 +├── DESIGN.md 架构设计 +└── scene.txt 场景保存文件 +``` + +--- + +## Engine/ASM/ — 汇编加速层 + +引擎的性能热路径用 MASM x64 汇编实现,通过 SSE/AVX2 指令集加速。当汇编不可用时自动降级到纯C版本。 + +- **math.asm** (1013行) — 向量加减乘除、矩阵乘法、四元数旋转,全部用 SIMD 指令 +- **math_c_fallback.h** (65行) — 数学函数的纯C实现,作为汇编的备选 +- **memory.asm** (146行) — 对齐内存分配,保证 SIMD 操作需要的16/32/64字节对齐 +- **string_ops.asm** (253行) — 用SIMD加速的内存拷贝、字符串长度计算、字符串比较 +- **profiling.asm** (122行) — 用CPU的RDTSC指令做高精度计时,用于性能分析 + +## Engine/Core/ — 引擎核心 + +- **engine.c/h** — 引擎主控。负责初始化所有子系统、运行游戏循环(每帧调update→render)、关闭时清理资源。这是整个引擎的入口 +- **timer.c/h** — 用Windows的QueryPerformanceCounter做毫秒级计时,计算帧间隔(delta time) +- **logger.c/h** — 日志系统。支持INFO/WARN/FATAL三级日志,同时输出到控制台和文件 +- **asset_manager.c/h** — 资源管理器。统一处理shader/mesh/texture的加载和缓存,避免重复加载 + +## Engine/Window/ — 窗口系统 + +- **window.c/h** — 创建Win32窗口,处理Windows消息循环。把键盘、鼠标、窗口大小变化等事件转发给引擎回调函数 + +## Engine/Rendering/ — D3D11渲染管线 + +- **graphics_device.c/h** — D3D11设备初始化。创建Device、SwapChain、RenderTarget、DepthStencil,管理渲染状态 +- **renderer.c/h** — 渲染器。设置视口、清屏、管理渲染目标切换 +- **shader.c/h** — 着色器管理。编译HLSL着色器、设置常量缓冲区、绑定到管线 +- **mesh.c/h** — 网格系统。支持立方体/球体/平面程序化生成,也支持OBJ文件加载。管理顶点缓冲区和索引缓冲区 +- **texture.c/h** — 纹理系统。通过WIC加载BMP/PNG/JPEG,也支持生成纯色和棋盘格纹理 +- **camera.c/h** — 摄像机。透视投影、lookAt矩阵、自由移动和旋转 +- **camera_controller.c/h** — 摄像机控制器。封装常用的WASD+鼠标操作 +- **frustum.c/h** — 视锥体裁剪。判断物体是否在摄像机视野内,跳过不可见物体的渲染 +- **lighting.c/h** — 光照系统。方向光+点光源,Lambert漫反射计算 +- **material.c/h** — 材质。把shader和纹理组合在一起,定义物体的外观属性 + +## Engine/Scene/ — 场景图 + +- **scene_graph.c/h** — 场景树。节点有父子关系,子节点继承父节点的位移/旋转/缩放 +- **scene_render.c/h** — 场景渲染。遍历场景树,把每个节点的MeshRenderer提交给渲染器 +- **scene_serialize.c/h** — 场景序列化。把整个场景树保存成text文件或从文件加载 + +## Engine/Input/ — 输入系统 + +- **input.c/h** — 键盘/鼠标/手柄输入。跟踪按键状态,支持轮询(GetAsyncKeyState)和事件回调两种模式 + +## Engine/Audio/ — 音频系统 + +- **audio.c/h** — XAudio2音频引擎。动态加载DLL(兼容Win7~11),加载WAV文件,播放/暂停/音量控制 + +## Engine/Physics/ — 物理系统 + +- **physics.c/h** — 简易物理。AABB碰撞检测、球体碰撞、重力模拟、速度积分 + +## Engine/ECS/ — 实体组件系统 + +- **world.c/h** — ECS世界。实体=组件的容器,系统批量处理同类组件 + +## Engine/UI/ — 用户界面 + +- **ui.c/h** — 2D UI渲染。按钮、标签、面板等基础控件,用D3D11的2D投影绘制 + +## Engine/Editor/ — 内置编辑器 + +- **editor.c/h** — 编辑器主逻辑 +- **editor_2d.c/h** — 2D编辑模式(俯视图) +- **uilayer.c/h** — 编辑器UI层 +- **bitmap_font.c/h** — 位图字体渲染 +- **glyph_atlas.c/h** — 字体图集生成 + +## Engine/VisualScript/ — 积木式可视化编程 + +这是引擎最复杂的模块之一,实现了类似Scratch的积木编程系统: + +- **block_core.c/h** — 积木核心数据结构(输入/输出端口、连接线) +- **block_types.c/h** — 30+种积木类型定义(移动、旋转、条件、循环等) +- **block_executor.c/h** — 积木执行器。解析积木图,按连接顺序执行逻辑 +- **block_serializer.c/h** — 积木序列化。把积木图保存为.ly文件或从文件加载 +- **block_workspace.c/h** — 积木工作区。管理所有积木的位置和连接关系 +- **block_renderer.c/h** — 积木渲染。在屏幕上绘制积木方块和贝塞尔连线 +- **block_interaction.c/h** — 积木交互。拖拽移动、连线创建、端口连接 +- **block_palette.c/h** — 积木面板。侧边栏显示可用积木列表 +- **block_editor.c/h** — 积木编辑器主控 + +## Engine/Scripting/ — 脚本系统 + +- **lity.py** — LityPy。Python脚本库,用Python语法写游戏逻辑 +- **lua_engine.c/h** — Lua脚本引擎桩(预留接口) + +## Engine/Asset/ — 资源定义 + +- **mesh.h** — Mesh结构体定义(顶点数组、索引数组、顶点缓冲区) +- **texture.h** — Texture结构体定义(宽高、像素数据、SRV) + +## Engine/include/ — 对外接口 + +- **engine_api.h** — C/C++便利封装。暴露汇编函数给C代码,提供运算符重载 + +--- + +## Demo/ — 示例程序 + +| 文件 | 说明 | +|------|------| +| main.c | 场景Demo(方块+球体+地面+光照) | +| lity_explorer.c | 3D开放世界探索(村庄+树木+第三人称) | +| cube_dodge.c | 躲避游戏(障碍物+碰撞+计分) | +| game_test.c | 纯D3D11渲染测试(无Engine依赖) | +| raw3d_test.c | 引擎3D渲染验证(彩色面) | +| scene_test.c | 场景图测试 | +| light_test.c | 光照系统测试 | +| input_test.c | 输入系统测试 | +| obj_viewer.c | OBJ模型查看器(鼠标旋转) | +| ly_interpreter.c | .ly积木文件解释器 | +| editor_main.c | 编辑器入口 | +| open_world_demo.c | 开放世界demo | + +## EditorCS/ — 编辑器 + +C# 编写的可视化积木编辑器,用Dear ImGui + D3D11实现。 + +## Assets/ — 模型资源 + +- cube.obj — 简单立方体OBJ模型 +- pyramid.obj — 金字塔OBJ模型