feat: LityPy - Python scripting library for lity3d engine
- Vec3, Color, Transform, Entity, Scene with decorator API - Components: MeshRenderer, RigidBody, Collider, Script - Physics helpers, Tween animations, Camera follow/orbit - Game templates: platformer, RPG, FPS - Scene save/load (JSON), export to .ly - 12 unit tests passing, 89-entity RPG demo verified
This commit is contained in:
1 parent
f235bd2026
commit
4f8fd9d95b
2 files changed
+703
No files matched your search
@@ -0,0 +1,598 @@
|
||||
"""
|
||||
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 <script.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()")
|
||||
Reference in new issue
Block a user