feat: Lity3D Editor + file guide
- editor.py: tkinter IDE with explorer, code editor, output console - Toolbar: Run (F5), Stop, Build (F7), Scene management - 引擎文件说明.md: complete engine architecture documentation
This commit is contained in:
1 parent
4f8fd9d95b
commit
9d7773ffcd
2 files changed
+532
No files matched your search
@@ -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("<Control-n>", lambda e: self._new_file())
|
||||
self.root.bind("<Control-o>", lambda e: self._open_file())
|
||||
self.root.bind("<Control-s>", lambda e: self._save_file())
|
||||
self.root.bind("<F5>", lambda e: self._run_game())
|
||||
self.root.bind("<F7>", lambda e: self._build_exe())
|
||||
self.root.bind("<Shift-F5>", 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("<Double-1>", 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()
|
||||
Reference in new issue
Block a user