""" LityPy - lity3d 引擎 Python 脚本库 用法: from lity import * scene = Scene("My Game") player = scene.create("hero", "cube", pos=(0, 1, 0), color=Color.GREEN) ground = create_ground(scene) @scene.on_key("W") def move_forward(dt): player.z += 5 * dt scene.run() """ import math import time import sys import os import struct import json from dataclasses import dataclass, field from typing import List, Dict, Tuple, Optional, Callable, Any from functools import wraps # ============================================================================ # 数学 # ============================================================================ @dataclass class Vec3: x: float = 0.0 y: float = 0.0 z: float = 0.0 def __add__(self, o): return Vec3(self.x+o.x, self.y+o.y, self.z+o.z) def __sub__(self, o): return Vec3(self.x-o.x, self.y-o.y, self.z-o.z) def __mul__(self, s): return Vec3(self.x*s, self.y*s, self.z*s) def __rmul__(self, s): return self * s def __neg__(self): return Vec3(-self.x, -self.y, -self.z) def length(self): return math.sqrt(self.x**2 + self.y**2 + self.z**2) def normalize(self): l = self.length() return Vec3(self.x/l, self.y/l, self.z/l) if l > 0.0001 else Vec3() def dot(self, o): return self.x*o.x + self.y*o.y + self.z*o.z def cross(self, o): return Vec3(self.y*o.z-self.z*o.y, self.z*o.x-self.x*o.z, self.x*o.y-self.y*o.x) def lerp(self, o, t): return Vec3(self.x+(o.x-self.x)*t, self.y+(o.y-self.y)*t, self.z+(o.z-self.z)*t) def distance(self, o): return (self - o).length() def tuple(self): return (self.x, self.y, self.z) def __repr__(self): return f"Vec3({self.x:.2f}, {self.y:.2f}, {self.z:.2f})" @dataclass class Color: r: float = 1.0 g: float = 1.0 b: float = 1.0 a: float = 1.0 @classmethod def from_hex(cls, hex_str): h = hex_str.lstrip('#') if len(h) == 6: return cls(int(h[0:2],16)/255, int(h[2:4],16)/255, int(h[4:6],16)/255) if len(h) == 8: return cls(int(h[0:2],16)/255, int(h[2:4],16)/255, int(h[4:6],16)/255, int(h[6:8],16)/255) return cls() RED = None; GREEN = None; BLUE = None; WHITE = None; BLACK = None YELLOW = None; CYAN = None; MAGENTA = None; ORANGE = None; GRAY = None Color.RED = Color(1, 0.2, 0.15) Color.GREEN = Color(0.2, 0.8, 0.3) Color.BLUE = Color(0.2, 0.4, 0.9) Color.WHITE = Color(1, 1, 1) Color.BLACK = Color(0.1, 0.1, 0.1) Color.YELLOW = Color(1, 0.9, 0.2) Color.CYAN = Color(0.2, 0.8, 0.8) Color.MAGENTA = Color(0.8, 0.2, 0.8) Color.ORANGE = Color(1, 0.5, 0.1) Color.GRAY = Color(0.5, 0.5, 0.5) def lerp(a, b, t): return a + (b - a) * t def clamp(v, lo, hi): return max(lo, min(hi, v)) def sin_val(v): return math.sin(v) def cos_val(v): return math.cos(v) def sqrt_val(v): return math.sqrt(v) def abs_val(v): return abs(v) def rand(lo=0, hi=1): import random; return random.uniform(lo, hi) def rand_int(lo, hi): import random; return random.randint(lo, hi) def distance(a, b): return (a - b).length() if isinstance(a, Vec3) else abs(a - b) def time_s(): return time.time() # ============================================================================ # 组件系统 # ============================================================================ @dataclass class Transform: position: Vec3 = field(default_factory=Vec3) rotation: Vec3 = field(default_factory=Vec3) # euler angles in radians scale: Vec3 = field(default_factory=lambda: Vec3(1, 1, 1)) @property def x(self): return self.position.x @x.setter def x(self, v): self.position.x = v @property def y(self): return self.position.y @y.setter def y(self, v): self.position.y = v @property def z(self): return self.position.z @z.setter def z(self, v): self.position.z = v @dataclass class MeshRenderer: mesh_type: str = "cube" color: Color = field(default_factory=Color) visible: bool = True @dataclass class RigidBody: velocity: Vec3 = field(default_factory=Vec3) mass: float = 1.0 use_gravity: bool = False drag: float = 0.98 @dataclass class Collider: shape: str = "box" # box, sphere size: Vec3 = field(default_factory=lambda: Vec3(1, 1, 1)) @dataclass class Script: update: Optional[Callable] = None on_start: Optional[Callable] = None on_collision: Optional[Callable] = None # ============================================================================ # Entity # ============================================================================ _next_id = 0 class Entity: def __init__(self, name="entity", mesh="cube", pos=None, color=None, **kwargs): global _next_id _next_id += 1 self.id = _next_id self.name = name self.tags: List[str] = kwargs.get("tags", []) self.active = True if pos and not isinstance(pos, Vec3): pos = Vec3(*pos) if isinstance(pos, (list, tuple)) else Vec3(pos, 0, 0) self.transform = Transform(position=pos or Vec3()) self.renderer = MeshRenderer(mesh_type=mesh, color=color or Color.WHITE) self.rigid_body = None self.collider = None self.script = None self.children: List['Entity'] = [] self.parent: Optional['Entity'] = None self._props: Dict[str, Any] = {} @property def pos(self): return self.transform.position @pos.setter def pos(self, v): if isinstance(v, (list, tuple)): v = Vec3(*v) self.transform.position = v @property def x(self): return self.transform.position.x @x.setter def x(self, v): self.transform.position.x = v @property def y(self): return self.transform.position.y @y.setter def y(self, v): self.transform.position.y = v @property def z(self): return self.transform.position.z @z.setter def z(self, v): self.transform.position.z = v @property def rot(self): return self.transform.rotation @rot.setter def rot(self, v): self.transform.rotation = v if isinstance(v, Vec3) else Vec3(*v) @property def scl(self): return self.transform.scale @scl.setter def scl(self, v): self.transform.scale = v if isinstance(v, Vec3) else Vec3(*v) @property def color(self): return self.renderer.color @color.setter def color(self, v): self.renderer.color = v if isinstance(v, Color) else Color(*v) def move(self, dx=0, dy=0, dz=0): self.transform.position = self.transform.position + Vec3(dx, dy, dz) def look_at(self, target): if isinstance(target, Entity): target = target.pos d = (target - self.pos).normalize() self.transform.rotation = Vec3(math.atan2(d.z, d.x), 0, 0) def add_rigidbody(self, mass=1.0, gravity=False): self.rigid_body = RigidBody(mass=mass, use_gravity=gravity) return self def add_collider(self, shape="box", size=None): self.collider = Collider(shape=shape, size=size or Vec3(1, 1, 1)) return self def set_script(self, update=None, on_start=None, on_collision=None): self.script = Script(update=update, on_start=on_start, on_collision=on_collision) if on_start: on_start(self) return self def child(self, name, mesh="cube", **kwargs): e = Entity(name, mesh, **kwargs) e.parent = self self.children.append(e) return e def __getitem__(self, key): return self._props.get(key) def __setitem__(self, key, val): self._props[key] = val def __contains__(self, tag): return tag in self.tags # ============================================================================ # 场景 # ============================================================================ class Scene: def __init__(self, title="Lity3D", width=1280, height=720): self.title = title self.width = width self.height = height self.entities: List[Entity] = [] self._key_handlers: Dict[int, List[Callable]] = {} self._key_down_handlers: Dict[int, List[Callable]] = {} self._frame_handlers: List[Callable] = [] self._update_handlers: List[Callable] = [] self._collision_handlers: List[Callable] = [] self._mouse_handlers: List[Callable] = [] self._dt = 0.016 self._time = 0.0 self._running = False self._last_time = 0.0 self._camera_pos = Vec3(0, 5, -10) self._camera_target = Vec3(0, 0, 0) self._sky_color = Color(0.15, 0.18, 0.25) self._gravity = Vec3(0, -9.8, 0) self._paused = False self._custom_data: Dict[str, Any] = {} self._log_file = None def create(self, name="entity", mesh="cube", pos=None, color=None, scale=None, **kwargs) -> Entity: e = Entity(name, mesh, pos=pos, color=color, **kwargs) if scale: e.transform.scale = Vec3(*scale) if not isinstance(scale, Vec3) else scale self.entities.append(e) return e def find(self, name) -> Optional[Entity]: for e in self.entities: if e.name == name: return e return None def find_by_tag(self, tag) -> List[Entity]: return [e for e in self.entities if tag in e.tags] def destroy(self, entity): if entity in self.entities: self.entities.remove(entity) for c in entity.children: self.destroy(c) def destroy_all(self, tag=None): if tag: self.entities = [e for e in self.entities if tag not in e.tags] else: self.entities.clear() def on_frame(self, func): self._frame_handlers.append(func) return func def on_update(self, func): self._update_handlers.append(func) return func def on_key(self, key): def decorator(func): if isinstance(key, str): k = ord(key.upper()) else: k = key self._key_handlers.setdefault(k, []).append(func) return func return decorator def on_key_down(self, key): def decorator(func): if isinstance(key, str): k = ord(key.upper()) else: k = key self._key_down_handlers.setdefault(k, []).append(func) return func return decorator def on_collision(self, func): self._collision_handlers.append(func) return func def on_mouse(self, func): self._mouse_handlers.append(func) return func def log(self, *args): msg = " ".join(str(a) for a in args) print(f"[{self._time:.2f}] {msg}") if self._log_file: self._log_file.write(f"[{self._time:.2f}] {msg}\n") self._log_file.flush() def save(self, path="scene.json"): data = [] for e in self.entities: d = { "name": e.name, "mesh": e.renderer.mesh_type, "pos": e.pos.tuple(), "rot": e.rot.tuple(), "scl": e.scl.tuple(), "color": (e.color.r, e.color.g, e.color.b, e.color.a), "visible": e.renderer.visible, "tags": e.tags, "props": e._props, } data.append(d) with open(path, 'w') as f: json.dump(data, f, indent=2) self.log(f"Scene saved to {path} ({len(data)} entities)") def load(self, path="scene.json"): if not os.path.exists(path): self.log(f"No {path} found"); return with open(path, 'r') as f: data = json.load(f) for d in data: e = self.create( d["name"], d.get("mesh", "cube"), pos=Vec3(*d.get("pos", (0,0,0))), color=Color(*d.get("color", (1,1,1))), tags=d.get("tags", []) ) e.rot = Vec3(*d.get("rot", (0,0,0))) e.scl = Vec3(*d.get("scl", (1,1,1))) e.renderer.visible = d.get("visible", True) e._props = d.get("props", {}) self.log(f"Scene loaded from {path} ({len(data)} entities)") def run(self, headless=True, duration=None): self._running = True self._last_time = time.time() start = self._last_time self.log(f"Scene '{self.title}' starting ({len(self.entities)} entities)") if headless: self.log("Running in headless/script mode") else: self.log("Running with engine rendering") for e in self.entities: if e.script and e.script.on_start: e.script.on_start(e) try: while self._running: now = time.time() self._dt = now - (start if self._time == 0 else self._last_time) self._last_time = now self._time += self._dt if self._paused: continue self._update(self._dt) if duration and self._time >= duration: self.log(f"Duration {duration}s reached") break except KeyboardInterrupt: self.log("Stopped by user") finally: self.log(f"Scene ended (ran for {self._time:.2f}s)") def stop(self): self._running = False def pause(self): self._paused = True; self.log("Paused") def resume(self): self._paused = False; self.log("Resumed") def _update(self, dt): for e in self.entities: if not e.active: continue if e.rigid_body and e.rigid_body.use_gravity: e.rigid_body.velocity = e.rigid_body.velocity + self._gravity * dt if e.rigid_body: e.rigid_body.velocity = e.rigid_body.velocity * e.rigid_body.drag e.pos = e.pos + e.rigid_body.velocity * dt if e.script and e.script.update: e.script.update(e, dt) for c in e.children: if c.script and c.script.update: c.script.update(c, dt) for h in self._update_handlers: h(self, dt) for h in self._frame_handlers: h(dt) # ============================================================================ # 摄像机 # ============================================================================ class Camera: def __init__(self, pos=None, target=None, fov=60, near=0.1, far=200): self.pos = pos or Vec3(0, 5, -10) self.target = target or Vec3(0, 0, 0) self.fov = fov self.near = near self.far = far def follow(self, entity, offset=None, smooth=0.1): off = offset or Vec3(0, 3, -6) target = entity.pos + off self.pos = self.pos.lerp(target, smooth) self.target = entity.pos def orbit(self, center, distance=10, yaw=0, pitch=0.3): self.pos = Vec3( center.x + math.sin(yaw) * distance * math.cos(pitch), center.y + math.sin(pitch) * distance, center.z + math.cos(yaw) * distance * math.cos(pitch) ) self.target = center # ============================================================================ # 物理辅助 # ============================================================================ def physics_raycast(origin, direction, max_dist=100): return None # placeholder for engine integration def physics_overlap(box_a, box_b): if not box_a.collider or not box_b.collider: return False ax, ay, az = box_a.pos.x, box_a.pos.y, box_a.pos.z bx, by, bz = box_b.pos.x, box_b.pos.y, box_b.pos.z dx = abs(ax - bx) dy = abs(ay - by) dz = abs(az - bz) sx = (box_a.collider.size.x + box_b.collider.size.x) * 0.5 sy = (box_a.collider.size.y + box_b.collider.size.y) * 0.5 sz = (box_a.collider.size.z + box_b.collider.size.z) * 0.5 return dx < sx and dy < sy and dz < sz # ============================================================================ # 预设 # ============================================================================ def create_ground(scene, size=40, color=None): return scene.create("ground", "plane", pos=Vec3(0, -0.01, 0), scale=Vec3(size, 1, size), color=color or Color(0.3, 0.5, 0.25)) def create_skybox(scene, color=None): scene._sky_color = color or Color(0.15, 0.18, 0.25) def create_tree(scene, x, z, trunk_color=None, leaves_color=None): trunk = scene.create("trunk", "cube", pos=Vec3(x, 0.5, z), scale=Vec3(0.2, 1, 0.2), color=trunk_color or Color(0.45, 0.28, 0.15)) leaves = scene.create("leaves", "sphere", pos=Vec3(x, 1.8, z), scale=Vec3(0.8, 0.7, 0.8), color=leaves_color or Color(0.15, 0.45, 0.12)) return trunk, leaves def create_building(scene, x, z, w=2, h=2, d=2, wall_color=None, roof_color=None): wall = scene.create("wall", "cube", pos=Vec3(x, h*0.5, z), scale=Vec3(w, h, d), color=wall_color or Color(0.8, 0.3, 0.2)) roof = scene.create("roof", "cube", pos=Vec3(x, h+0.15, z), scale=Vec3(w+0.3, 0.3, d+0.3), color=roof_color or Color(0.35, 0.35, 0.38)) return wall, roof # ============================================================================ # 动画/tween # ============================================================================ class Tween: def __init__(self, entity, prop, target, duration, ease="linear"): self.entity = entity self.prop = prop self.start = getattr(entity, prop) if hasattr(entity, prop) else 0 self.target = target self.duration = duration self.elapsed = 0 self.done = False self.ease = ease def update(self, dt): if self.done: return self.elapsed += dt t = min(self.elapsed / self.duration, 1.0) if self.ease == "ease_in": t = t * t elif self.ease == "ease_out": t = 1 - (1-t)**2 elif self.ease == "ease_in_out": t = 3*t**2 - 2*t**3 val = lerp(self.start, self.target, t) if hasattr(self.entity, self.prop): setattr(self.entity, self.prop, val) if t >= 1.0: self.done = True def tween_to(entity, prop, target, duration=1.0, ease="linear"): return Tween(entity, prop, target, duration, ease) # ============================================================================ # 序列化/导出 # ============================================================================ def export_scene_ly(scene, path="scene.ly"): lines = [f"# LityPy exported scene: {scene.title}"] for e in scene.entities: p = e.pos r = e.rot s = e.scl c = e.color lines.append(f'world.create("{e.name}", "{e.renderer.mesh_type}", ' f'pos=({p.x:.2f},{p.y:.2f},{p.z:.2f}), ' f'color=({c.r:.2f},{c.g:.2f},{c.b:.2f}))') with open(path, 'w') as f: f.write('\n'.join(lines)) print(f"Exported {len(scene.entities)} entities to {path}") # ============================================================================ # 游戏模板 # ============================================================================ def game_2d_platformer(): """创建2D平台跳跃游戏模板""" scene = Scene("2D Platformer", 1280, 720) player = scene.create("player", "cube", pos=Vec3(0, 1, 0), color=Color.GREEN) player.add_rigidbody(mass=1.0, gravity=True) player.add_collider(size=Vec3(0.8, 1.0, 0.8)) player.speed = 5.0 player.jump_force = 8.0 player.grounded = False ground = create_ground(scene, 100) for i in range(-5, 6): scene.create(f"platform_{i}", "cube", pos=Vec3(i*3, 0.5, 0), scale=Vec3(2, 0.3, 2), color=Color.GRAY) @scene.on_frame def update(dt): scene.log(f"Player at ({player.x:.1f}, {player.y:.1f})") return scene def game_topdown_rpg(): """创建俯视角RPG模板""" scene = Scene("Top-Down RPG") player = scene.create("player", "cube", pos=Vec3(0, 0, 0), color=Color.GREEN) player.speed = 5.0 create_ground(scene, 60) for i in range(20): x, z = rand(-25, 25), rand(-25, 25) create_tree(scene, x, z) for i in range(5): x, z = rand(-15, 15), rand(-15, 15) create_building(scene, x, z, rand(1.5, 3), rand(1.5, 3), rand(1.5, 3), wall_color=Color(rand(0.6,1), rand(0.2,0.8), rand(0.1,0.5))) return scene def game_fps_shooter(): """创建FPS射击模板""" scene = Scene("FPS Shooter") player = scene.create("player", "cube", pos=Vec3(0, 1.7, 0), color=Color.GREEN) cam = Camera(pos=Vec3(0, 1.7, 0)) player.speed = 8.0 create_ground(scene, 80) for i in range(30): x, z = rand(-30, 30), rand(-30, 30) scene.create(f"target_{i}", "sphere", pos=Vec3(x, 1, z), color=Color.RED, tags=["enemy"]) return scene, cam # ============================================================================ # CLI 入口 # ============================================================================ def run_script(path): """运行 .py 脚本文件""" with open(path) as f: code = f.read() namespace = {"__name__": "__litypy__", "__file__": path} exec(compile(code, path, "exec"), namespace) if __name__ == "__main__": if len(sys.argv) > 1: run_script(sys.argv[1]) else: print("LityPy - lity3d 引擎 Python 脚本库") print("用法: python lity.py ") print() print("示例:") print(" from lity import *") print(" scene = Scene()") print(" player = scene.create('hero', 'cube', pos=(0,1,0), color=Color.GREEN)") print(" create_ground(scene)") print(" scene.run()")