- 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
599 lines
21 KiB
Python
599 lines
21 KiB
Python
"""
|
|
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()")
|