Three-dimensional-Games 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- three_dimensional_games/__init__.py +256 -0
- three_dimensional_games/animation.py +321 -0
- three_dimensional_games/application.py +350 -0
- three_dimensional_games/audio.py +208 -0
- three_dimensional_games/builtin_textures.py +606 -0
- three_dimensional_games/camera.py +16 -0
- three_dimensional_games/color.py +160 -0
- three_dimensional_games/entity.py +494 -0
- three_dimensional_games/gamepad.py +330 -0
- three_dimensional_games/input.py +145 -0
- three_dimensional_games/mesh.py +3 -0
- three_dimensional_games/models.py +246 -0
- three_dimensional_games/network.py +722 -0
- three_dimensional_games/particles.py +191 -0
- three_dimensional_games/physics.py +430 -0
- three_dimensional_games/prefabs/__init__.py +11 -0
- three_dimensional_games/prefabs/first_person.py +153 -0
- three_dimensional_games/prefabs/second_person.py +154 -0
- three_dimensional_games/prefabs/sky.py +51 -0
- three_dimensional_games/prefabs/third_person.py +194 -0
- three_dimensional_games/raycaster.py +571 -0
- three_dimensional_games/saveload.py +219 -0
- three_dimensional_games/scene.py +141 -0
- three_dimensional_games/texture.py +6 -0
- three_dimensional_games/trigger.py +159 -0
- three_dimensional_games/ui.py +305 -0
- three_dimensional_games/util.py +215 -0
- three_dimensional_games/vector.py +383 -0
- three_dimensional_games/voice.py +490 -0
- three_dimensional_games-0.3.0.dist-info/METADATA +184 -0
- three_dimensional_games-0.3.0.dist-info/RECORD +34 -0
- three_dimensional_games-0.3.0.dist-info/WHEEL +5 -0
- three_dimensional_games-0.3.0.dist-info/licenses/LICENSE +21 -0
- three_dimensional_games-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Three-dimensional-Games 3D Game Engine
|
|
3
|
+
======================================
|
|
4
|
+
比 Ursina 更简洁:开头一行 init() 拿到 game,结尾一行 run() 启动;
|
|
5
|
+
同时又像 Panda3D 一样严谨实用,内置物理、联机、三种视角控制器、
|
|
6
|
+
大量贴图、补间动画、粒子、音效、触发区等模块。
|
|
7
|
+
|
|
8
|
+
标准写法(开头初始化 + 结尾启动):
|
|
9
|
+
from three_dimensional_games import *
|
|
10
|
+
|
|
11
|
+
game = init(title="我的 3D 游戏")
|
|
12
|
+
|
|
13
|
+
ground = Entity(model='plane', texture='grass', scale=50, collider='box')
|
|
14
|
+
player = FirstPerson()
|
|
15
|
+
player.y = 2
|
|
16
|
+
|
|
17
|
+
run()
|
|
18
|
+
|
|
19
|
+
也兼容“省略 init / run”的极简写法(引擎会自己兜底):
|
|
20
|
+
from three_dimensional_games import *
|
|
21
|
+
Entity(model='cube', collider='box', mass=1)
|
|
22
|
+
FirstPerson()
|
|
23
|
+
# 不用写 run(),脚本结束时也会自动运行
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
__version__ = "0.3.0"
|
|
27
|
+
__author__ = "Your Name"
|
|
28
|
+
|
|
29
|
+
import atexit
|
|
30
|
+
|
|
31
|
+
# ----------------------------------------------------------------------------
|
|
32
|
+
# 引擎实例:init() 显式创建,或按需自动创建
|
|
33
|
+
# ----------------------------------------------------------------------------
|
|
34
|
+
_app = None
|
|
35
|
+
_explicit_run = False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def init(title: str = "Three-dimensional Game", **kwargs):
|
|
39
|
+
"""
|
|
40
|
+
初始化游戏引擎,返回 game 对象(引擎实例)。写在脚本开头。
|
|
41
|
+
|
|
42
|
+
常用参数:
|
|
43
|
+
init(title='我的游戏', window_size=(1280, 720), fullscreen=False,
|
|
44
|
+
vsync=True, show_fps=True, background_color=(0.1, 0.1, 0.1, 1))
|
|
45
|
+
"""
|
|
46
|
+
global _app
|
|
47
|
+
from .application import Application
|
|
48
|
+
if Application._instance is not None:
|
|
49
|
+
_app = Application._instance
|
|
50
|
+
else:
|
|
51
|
+
_app = Application(title=title, **kwargs)
|
|
52
|
+
return _app
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def run():
|
|
56
|
+
"""启动游戏主循环。写在脚本最后一行。"""
|
|
57
|
+
global _app, _explicit_run
|
|
58
|
+
_app = _ensure_app()
|
|
59
|
+
_explicit_run = True
|
|
60
|
+
_app.run()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _ensure_app():
|
|
64
|
+
"""确保引擎已创建(第一次创建 Entity / UI 时自动调用)"""
|
|
65
|
+
global _app
|
|
66
|
+
if _app is None:
|
|
67
|
+
from .application import Application
|
|
68
|
+
if Application._instance is not None:
|
|
69
|
+
_app = Application._instance
|
|
70
|
+
else:
|
|
71
|
+
_app = Application()
|
|
72
|
+
return _app
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def get_app():
|
|
76
|
+
"""获取当前引擎实例;如果没有会自动创建一个"""
|
|
77
|
+
return _ensure_app()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ----------------------------------------------------------------------------
|
|
81
|
+
# 核心 API(导入即可用)
|
|
82
|
+
# ----------------------------------------------------------------------------
|
|
83
|
+
from .application import Application
|
|
84
|
+
from .entity import Entity
|
|
85
|
+
from .vector import Vec2, Vec3, Vec4, to_panda_vec3, to_panda_vec2, to_panda_vec4
|
|
86
|
+
from .color import Color, color
|
|
87
|
+
from .scene import scene, camera
|
|
88
|
+
from .input import input_handler, held_keys, mouse
|
|
89
|
+
from .physics import BoxCollider, SphereCollider, MeshCollider, RigidBody, raycast, RaycastHit, physics_world
|
|
90
|
+
from .models import load_model
|
|
91
|
+
from .builtin_textures import TextureManager, textures as texture_manager
|
|
92
|
+
from .ui import Text, Button, Slider, InputField, WindowPanel
|
|
93
|
+
|
|
94
|
+
# 补间动画
|
|
95
|
+
from .animation import (
|
|
96
|
+
Tween, Sequence, animate,
|
|
97
|
+
animate_position, animate_scale, animate_rotation, animate_color,
|
|
98
|
+
stop_all_tweens,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# 粒子
|
|
102
|
+
from .particles import Particles, ParticleSystem
|
|
103
|
+
|
|
104
|
+
# 音效
|
|
105
|
+
from .audio import Audio, Sound, Music, tone, beep, set_master_volume
|
|
106
|
+
|
|
107
|
+
# 触发区
|
|
108
|
+
from .trigger import Trigger, on_enter, on_exit, clear_triggers
|
|
109
|
+
|
|
110
|
+
# 存档 / 读档
|
|
111
|
+
from .saveload import (
|
|
112
|
+
save, load, save_slot, load_slot, slot_path,
|
|
113
|
+
has_save, has_slot, delete_save, list_saves,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# 三种视角控制器 + 天空盒:名字更短,直接 FirstPerson() / SecondPerson() / ThirdPerson()
|
|
117
|
+
from .prefabs import (
|
|
118
|
+
FirstPersonController,
|
|
119
|
+
SecondPersonController,
|
|
120
|
+
ThirdPersonController,
|
|
121
|
+
Sky,
|
|
122
|
+
)
|
|
123
|
+
FirstPerson = FirstPersonController
|
|
124
|
+
SecondPerson = SecondPersonController
|
|
125
|
+
ThirdPerson = ThirdPersonController
|
|
126
|
+
|
|
127
|
+
# ----------------------------------------------------------------------------
|
|
128
|
+
# 联机模块:host() 一行开房
|
|
129
|
+
# host(public=True) -> 公网联机
|
|
130
|
+
# host(public=False) -> 局域网联机
|
|
131
|
+
# ----------------------------------------------------------------------------
|
|
132
|
+
from .network import NetworkManager, NetworkPlayer
|
|
133
|
+
|
|
134
|
+
network = NetworkManager()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def host(public: bool = False, port: int = 5555, max_players: int = 16,
|
|
138
|
+
game_name: str = "Three-dimensional Game"):
|
|
139
|
+
"""开房间。public=True 公网联机;public=False 局域网联机"""
|
|
140
|
+
_ensure_app()
|
|
141
|
+
return network.host(public=public, port=port, max_players=max_players, game_name=game_name)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def connect(ip: str = "127.0.0.1", port: int = 5555, player_name: str = "Player"):
|
|
145
|
+
"""加入别人的房间"""
|
|
146
|
+
_ensure_app()
|
|
147
|
+
return network.join(host_ip=ip, port=port, player_name=player_name)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# ----------------------------------------------------------------------------
|
|
151
|
+
# 高级射线 / 虚拟碰撞:准星射线、穿透、boxcast/spherecast/capsulecast、调试线、Layer
|
|
152
|
+
# ----------------------------------------------------------------------------
|
|
153
|
+
from .raycaster import (
|
|
154
|
+
Raycaster, raycaster, Layer,
|
|
155
|
+
camera_ray, screen_ray, mouse_ray, raycast_all,
|
|
156
|
+
boxcast, spherecast, capsulecast,
|
|
157
|
+
overlap_box, overlap_sphere, linecast,
|
|
158
|
+
LineRenderer, draw_line, draw_ray,
|
|
159
|
+
hit_distance_to, nearest_hit, hits_with_tag,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# 手柄
|
|
163
|
+
from .gamepad import (
|
|
164
|
+
Gamepad, gamepad,
|
|
165
|
+
on_pad_press, on_pad_release, on_pad_axis, rumble, set_gamepad_deadzone,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# 语音:TTS 播报、麦克风录音、ASR 识别、联机语音
|
|
169
|
+
from .voice import (
|
|
170
|
+
speak, say,
|
|
171
|
+
record, stop_recording, recognize, listen,
|
|
172
|
+
VoiceChat, voice_chat,
|
|
173
|
+
is_mic_available, is_tts_available,
|
|
174
|
+
set_voice_volume, get_voice_volume,
|
|
175
|
+
VoiceRecorder,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# ----------------------------------------------------------------------------
|
|
179
|
+
# 常用工具函数
|
|
180
|
+
# ----------------------------------------------------------------------------
|
|
181
|
+
from .util import (
|
|
182
|
+
destroy,
|
|
183
|
+
invoke,
|
|
184
|
+
invoke_repeating,
|
|
185
|
+
random_range,
|
|
186
|
+
random_int,
|
|
187
|
+
random_color,
|
|
188
|
+
lerp,
|
|
189
|
+
lerp_color,
|
|
190
|
+
clamp,
|
|
191
|
+
distance,
|
|
192
|
+
sin,
|
|
193
|
+
cos,
|
|
194
|
+
tan,
|
|
195
|
+
time as get_time,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
__all__ = [
|
|
199
|
+
"init", "run", "get_app", "Application",
|
|
200
|
+
"Entity",
|
|
201
|
+
"Vec2", "Vec3", "Vec4", "Color", "color",
|
|
202
|
+
"scene", "camera", "input_handler", "held_keys", "mouse",
|
|
203
|
+
"BoxCollider", "SphereCollider", "MeshCollider", "RigidBody", "raycast", "RaycastHit", "physics_world",
|
|
204
|
+
"load_model", "TextureManager", "texture_manager",
|
|
205
|
+
"Text", "Button", "Slider", "InputField", "WindowPanel",
|
|
206
|
+
"Tween", "Sequence", "animate",
|
|
207
|
+
"animate_position", "animate_scale", "animate_rotation", "animate_color", "stop_all_tweens",
|
|
208
|
+
"Particles", "ParticleSystem",
|
|
209
|
+
"Audio", "Sound", "Music", "tone", "beep", "set_master_volume",
|
|
210
|
+
"Trigger", "on_enter", "on_exit", "clear_triggers",
|
|
211
|
+
"save", "load", "save_slot", "load_slot", "slot_path",
|
|
212
|
+
"has_save", "has_slot", "delete_save", "list_saves",
|
|
213
|
+
"FirstPerson", "SecondPerson", "ThirdPerson", "Sky",
|
|
214
|
+
"FirstPersonController", "SecondPersonController", "ThirdPersonController",
|
|
215
|
+
"network", "NetworkManager", "NetworkPlayer", "host", "connect",
|
|
216
|
+
# 高级射线
|
|
217
|
+
"Raycaster", "raycaster", "Layer",
|
|
218
|
+
"camera_ray", "screen_ray", "mouse_ray", "raycast_all",
|
|
219
|
+
"boxcast", "spherecast", "capsulecast",
|
|
220
|
+
"overlap_box", "overlap_sphere", "linecast",
|
|
221
|
+
"LineRenderer", "draw_line", "draw_ray",
|
|
222
|
+
"hit_distance_to", "nearest_hit", "hits_with_tag",
|
|
223
|
+
# 手柄
|
|
224
|
+
"Gamepad", "gamepad",
|
|
225
|
+
"on_pad_press", "on_pad_release", "on_pad_axis", "rumble", "set_gamepad_deadzone",
|
|
226
|
+
# 语音
|
|
227
|
+
"speak", "say",
|
|
228
|
+
"record", "stop_recording", "recognize", "listen",
|
|
229
|
+
"VoiceChat", "voice_chat",
|
|
230
|
+
"is_mic_available", "is_tts_available",
|
|
231
|
+
"set_voice_volume", "get_voice_volume", "VoiceRecorder",
|
|
232
|
+
# 工具
|
|
233
|
+
"destroy", "invoke", "invoke_repeating",
|
|
234
|
+
"random_range", "random_int", "random_color",
|
|
235
|
+
"lerp", "lerp_color", "clamp", "distance",
|
|
236
|
+
"sin", "cos", "tan", "get_time",
|
|
237
|
+
]
|
|
238
|
+
|
|
239
|
+
__version__ = "0.3.0"
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# ----------------------------------------------------------------------------
|
|
243
|
+
# 兜底:如果脚本没写 run(),程序结束时自动运行
|
|
244
|
+
# (写了 run() 就完全走显式流程,不会重复运行)
|
|
245
|
+
# ----------------------------------------------------------------------------
|
|
246
|
+
def _auto_run():
|
|
247
|
+
if _explicit_run:
|
|
248
|
+
return
|
|
249
|
+
if _app is not None and not getattr(_app, "running", False):
|
|
250
|
+
try:
|
|
251
|
+
_app.run()
|
|
252
|
+
except Exception as exc: # pragma: no cover
|
|
253
|
+
print(f"[Three-dimensional-Games] 自动运行结束:{exc}")
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
atexit.register(_auto_run)
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""
|
|
2
|
+
动画 / 补间系统(Tween / Animation)
|
|
3
|
+
=====================================
|
|
4
|
+
比 Ursina 的 animate_position / animate_scale 更通用,任意属性都能补间:
|
|
5
|
+
|
|
6
|
+
animate(entity, 'position', (5, 2, 0), duration=1.0, curve='ease_in_out')
|
|
7
|
+
animate_color(entity, color.red, duration=0.5)
|
|
8
|
+
animate_scale(entity, 3, duration=0.3)
|
|
9
|
+
|
|
10
|
+
也支持把多个动画串起来顺序播放:
|
|
11
|
+
|
|
12
|
+
Sequence(
|
|
13
|
+
Tween(entity, 'position', (5, 0, 0), 1.0),
|
|
14
|
+
Tween(entity, 'position', (5, 5, 0), 1.0),
|
|
15
|
+
).start()
|
|
16
|
+
|
|
17
|
+
支持的插值曲线:
|
|
18
|
+
linear / ease_in / ease_out / ease_in_out
|
|
19
|
+
ease_in_cubic / ease_out_cubic / ease_in_out_cubic
|
|
20
|
+
smoothstep / bounce / elastic
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
import math
|
|
25
|
+
from typing import Any, Callable, Optional, Union
|
|
26
|
+
|
|
27
|
+
from .vector import Vec2, Vec3
|
|
28
|
+
from .color import Color
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"Tween", "Sequence", "animate", "animate_position",
|
|
32
|
+
"animate_scale", "animate_rotation", "animate_color",
|
|
33
|
+
"stop_all_tweens",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
# 记录所有正在运行的补间,方便统一停止
|
|
37
|
+
_active_tweens = []
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# 曲线
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
def evaluate_curve(curve: str, t: float) -> float:
|
|
44
|
+
"""把 0~1 的进度 t 按曲线映射成 0~1 的插值参数"""
|
|
45
|
+
t = max(0.0, min(1.0, t))
|
|
46
|
+
if curve in (None, "linear"):
|
|
47
|
+
return t
|
|
48
|
+
if curve == "ease_in":
|
|
49
|
+
return t * t
|
|
50
|
+
if curve == "ease_out":
|
|
51
|
+
return 1 - (1 - t) * (1 - t)
|
|
52
|
+
if curve in ("ease_in_out", "smoothstep"):
|
|
53
|
+
return t * t * (3 - 2 * t)
|
|
54
|
+
if curve == "ease_in_cubic":
|
|
55
|
+
return t ** 3
|
|
56
|
+
if curve == "ease_out_cubic":
|
|
57
|
+
return 1 - (1 - t) ** 3
|
|
58
|
+
if curve == "ease_in_out_cubic":
|
|
59
|
+
return 4 * t ** 3 if t < 0.5 else 1 - (-2 * t + 2) ** 3 / 2
|
|
60
|
+
if curve == "bounce":
|
|
61
|
+
n1, d1 = 7.5625, 2.75
|
|
62
|
+
if t < 1 / d1:
|
|
63
|
+
return n1 * t * t
|
|
64
|
+
if t < 2 / d1:
|
|
65
|
+
t -= 1.5 / d1
|
|
66
|
+
return n1 * t * t + 0.75
|
|
67
|
+
if t < 2.5 / d1:
|
|
68
|
+
t -= 2.25 / d1
|
|
69
|
+
return n1 * t * t + 0.9375
|
|
70
|
+
t -= 2.625 / d1
|
|
71
|
+
return n1 * t * t + 0.984375
|
|
72
|
+
if curve == "elastic":
|
|
73
|
+
if t == 0 or t == 1:
|
|
74
|
+
return t
|
|
75
|
+
c4 = (2 * math.pi) / 3
|
|
76
|
+
return -(2 ** (10 * t - 10)) * math.sin((t * 10 - 10.75) * c4)
|
|
77
|
+
return t
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ---------------------------------------------------------------------------
|
|
81
|
+
# 值处理:把 float / Vec2 / Vec3 / Color / tuple 拆成能插值的形式
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
def _split(value):
|
|
84
|
+
if isinstance(value, Color):
|
|
85
|
+
return "color", (value.r, value.g, value.b, value.a)
|
|
86
|
+
if isinstance(value, Vec3):
|
|
87
|
+
return "vec3", (value.x, value.y, value.z)
|
|
88
|
+
if isinstance(value, Vec2):
|
|
89
|
+
return "vec2", (value.x, value.y)
|
|
90
|
+
if isinstance(value, (tuple, list)):
|
|
91
|
+
return "tuple", tuple(float(v) for v in value)
|
|
92
|
+
return "number", (float(value),)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _rebuild(kind, comps):
|
|
96
|
+
if kind == "color":
|
|
97
|
+
return Color(*comps)
|
|
98
|
+
if kind == "vec3":
|
|
99
|
+
return Vec3(*comps)
|
|
100
|
+
if kind == "vec2":
|
|
101
|
+
return Vec2(*comps)
|
|
102
|
+
if kind == "tuple":
|
|
103
|
+
return tuple(comps)
|
|
104
|
+
return comps[0]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _clone(value):
|
|
108
|
+
if isinstance(value, Color):
|
|
109
|
+
return Color(value.r, value.g, value.b, value.a)
|
|
110
|
+
if isinstance(value, Vec3):
|
|
111
|
+
return Vec3(value.x, value.y, value.z)
|
|
112
|
+
if isinstance(value, Vec2):
|
|
113
|
+
return Vec2(value.x, value.y)
|
|
114
|
+
if isinstance(value, (tuple, list)):
|
|
115
|
+
return tuple(value)
|
|
116
|
+
return value
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _interpolate(start, end, t):
|
|
120
|
+
kind_a, comps_a = _split(start)
|
|
121
|
+
kind_b, comps_b = _split(end)
|
|
122
|
+
if kind_a != kind_b:
|
|
123
|
+
# 类型不一致时不做插值,直接切换
|
|
124
|
+
return _clone(end) if t >= 1 else _clone(start)
|
|
125
|
+
comps = [comps_a[i] + (comps_b[i] - comps_a[i]) * t for i in range(len(comps_a))]
|
|
126
|
+
return _rebuild(kind_a, comps)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _dt() -> float:
|
|
130
|
+
from .util import time as _time
|
|
131
|
+
return _time.dt
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ---------------------------------------------------------------------------
|
|
135
|
+
# 单个补间
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
class Tween:
|
|
138
|
+
"""
|
|
139
|
+
对某个对象的某个属性做一段补间动画。
|
|
140
|
+
|
|
141
|
+
Tween(entity, 'position', (5, 2, 0), duration=1.0, curve='ease_in_out')
|
|
142
|
+
|
|
143
|
+
:param target: 要动的是谁(一般是一个 Entity)
|
|
144
|
+
:param attr: 要动哪个属性,如 'position' / 'scale' / 'rotation' / 'color'
|
|
145
|
+
:param end_value: 目标值
|
|
146
|
+
:param duration: 持续时间(秒)
|
|
147
|
+
:param curve: 缓动曲线
|
|
148
|
+
:param delay: 延迟多久开始
|
|
149
|
+
:param loop: 是否循环播放
|
|
150
|
+
:param ping_pong: 循环时是否来回往复(A→B→A)
|
|
151
|
+
:param on_complete: 播放结束后的回调
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
def __init__(self, target, attr: str, end_value, duration: float = 1.0,
|
|
155
|
+
curve: str = "linear", delay: float = 0.0, loop: bool = False,
|
|
156
|
+
ping_pong: bool = False, on_complete: Optional[Callable] = None):
|
|
157
|
+
self.target = target
|
|
158
|
+
self.attr = attr
|
|
159
|
+
self.end_value = end_value
|
|
160
|
+
self.duration = max(float(duration), 1e-6)
|
|
161
|
+
self.curve = curve
|
|
162
|
+
self.delay = float(delay)
|
|
163
|
+
self.loop = loop
|
|
164
|
+
self.ping_pong = ping_pong
|
|
165
|
+
self.on_complete = on_complete
|
|
166
|
+
|
|
167
|
+
self.elapsed = 0.0
|
|
168
|
+
self.is_running = False
|
|
169
|
+
self._forward = True
|
|
170
|
+
self._start_value = None
|
|
171
|
+
self._task = None
|
|
172
|
+
|
|
173
|
+
# -- 内部控制 ---------------------------------------------------------
|
|
174
|
+
def _current(self):
|
|
175
|
+
return getattr(self.target, self.attr)
|
|
176
|
+
|
|
177
|
+
def _step(self, task):
|
|
178
|
+
dt = _dt()
|
|
179
|
+
self.elapsed += dt
|
|
180
|
+
|
|
181
|
+
if self.elapsed < self.delay:
|
|
182
|
+
return task.cont
|
|
183
|
+
|
|
184
|
+
t = (self.elapsed - self.delay) / self.duration
|
|
185
|
+
if t >= 1.0:
|
|
186
|
+
t = 1.0
|
|
187
|
+
|
|
188
|
+
eased = evaluate_curve(self.curve, t)
|
|
189
|
+
a, b = (self._start_value, self.end_value) if self._forward \
|
|
190
|
+
else (self.end_value, self._start_value)
|
|
191
|
+
setattr(self.target, self.attr, _interpolate(a, b, eased))
|
|
192
|
+
|
|
193
|
+
if t >= 1.0:
|
|
194
|
+
if self.on_complete:
|
|
195
|
+
self.on_complete()
|
|
196
|
+
if self.loop:
|
|
197
|
+
self.elapsed = self.delay
|
|
198
|
+
if self.ping_pong:
|
|
199
|
+
self._forward = not self._forward
|
|
200
|
+
return task.cont
|
|
201
|
+
self.is_running = False
|
|
202
|
+
if self in _active_tweens:
|
|
203
|
+
_active_tweens.remove(self)
|
|
204
|
+
self._task = None
|
|
205
|
+
return task.done
|
|
206
|
+
return task.cont
|
|
207
|
+
|
|
208
|
+
# -- 对外 API ---------------------------------------------------------
|
|
209
|
+
def start(self):
|
|
210
|
+
"""开始播放"""
|
|
211
|
+
from .application import Application
|
|
212
|
+
app = Application._instance
|
|
213
|
+
if app is None:
|
|
214
|
+
# 引擎还没起来(比如在导入阶段调),先记下起点,稍后由 animate() 兜底
|
|
215
|
+
self._start_value = _clone(self._current())
|
|
216
|
+
return self
|
|
217
|
+
self._start_value = _clone(self._current())
|
|
218
|
+
self.elapsed = 0.0
|
|
219
|
+
self.is_running = True
|
|
220
|
+
self._forward = True
|
|
221
|
+
if self not in _active_tweens:
|
|
222
|
+
_active_tweens.append(self)
|
|
223
|
+
self._task = app.taskMgr.add(self._step, f"tdg_tween_{id(self)}")
|
|
224
|
+
return self
|
|
225
|
+
|
|
226
|
+
def stop(self):
|
|
227
|
+
"""立即停止"""
|
|
228
|
+
from .application import Application
|
|
229
|
+
app = Application._instance
|
|
230
|
+
if app is not None and self._task is not None:
|
|
231
|
+
try:
|
|
232
|
+
app.taskMgr.remove(self._task)
|
|
233
|
+
except Exception:
|
|
234
|
+
pass
|
|
235
|
+
self.is_running = False
|
|
236
|
+
self._task = None
|
|
237
|
+
if self in _active_tweens:
|
|
238
|
+
_active_tweens.remove(self)
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def finished(self) -> bool:
|
|
242
|
+
return not self.is_running
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class Sequence:
|
|
246
|
+
"""
|
|
247
|
+
把多个 Tween 串起来,一个播完接着播下一个。
|
|
248
|
+
|
|
249
|
+
Sequence(
|
|
250
|
+
Tween(entity, 'position', (5, 0, 0), 1.0),
|
|
251
|
+
Tween(entity, 'scale', 2, 0.5),
|
|
252
|
+
).start()
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
def __init__(self, *tweens, loop: bool = False):
|
|
256
|
+
self.tweens = list(tweens)
|
|
257
|
+
self.loop = loop
|
|
258
|
+
self._index = 0
|
|
259
|
+
self.is_running = False
|
|
260
|
+
|
|
261
|
+
def start(self):
|
|
262
|
+
self._index = 0
|
|
263
|
+
self.is_running = True
|
|
264
|
+
self._play_next()
|
|
265
|
+
return self
|
|
266
|
+
|
|
267
|
+
def _play_next(self):
|
|
268
|
+
if self._index >= len(self.tweens):
|
|
269
|
+
if self.loop:
|
|
270
|
+
self._index = 0
|
|
271
|
+
else:
|
|
272
|
+
self.is_running = False
|
|
273
|
+
return
|
|
274
|
+
tween = self.tweens[self._index]
|
|
275
|
+
original = tween.on_complete
|
|
276
|
+
|
|
277
|
+
def _on_done():
|
|
278
|
+
if original:
|
|
279
|
+
original()
|
|
280
|
+
self._index += 1
|
|
281
|
+
self._play_next()
|
|
282
|
+
|
|
283
|
+
tween.on_complete = _on_done
|
|
284
|
+
tween.loop = False
|
|
285
|
+
tween.start()
|
|
286
|
+
|
|
287
|
+
def stop(self):
|
|
288
|
+
for t in self.tweens:
|
|
289
|
+
t.stop()
|
|
290
|
+
self.is_running = False
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
# ---------------------------------------------------------------------------
|
|
294
|
+
# 便捷函数
|
|
295
|
+
# ---------------------------------------------------------------------------
|
|
296
|
+
def animate(target, attr: str, end_value, **kwargs) -> Tween:
|
|
297
|
+
"""通用补间:把 target 的 attr 属性补间到 end_value"""
|
|
298
|
+
return Tween(target, attr, end_value, **kwargs).start()
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def animate_position(target, end_value, duration: float = 1.0, **kwargs) -> Tween:
|
|
302
|
+
return Tween(target, "position", end_value, duration=duration, **kwargs).start()
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def animate_scale(target, end_value, duration: float = 1.0, **kwargs) -> Tween:
|
|
306
|
+
return Tween(target, "scale", end_value, duration=duration, **kwargs).start()
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def animate_rotation(target, end_value, duration: float = 1.0, **kwargs) -> Tween:
|
|
310
|
+
return Tween(target, "rotation", end_value, duration=duration, **kwargs).start()
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def animate_color(target, end_value, duration: float = 1.0, **kwargs) -> Tween:
|
|
314
|
+
return Tween(target, "color", end_value, duration=duration, **kwargs).start()
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def stop_all_tweens():
|
|
318
|
+
"""停止所有正在运行的补间动画"""
|
|
319
|
+
for t in list(_active_tweens):
|
|
320
|
+
t.stop()
|
|
321
|
+
_active_tweens.clear()
|