aether-engine 1.0.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.
- aether/__init__.py +54 -0
- aether/audio/__init__.py +0 -0
- aether/audio/audio_manager.py +211 -0
- aether/audio/music.py +6 -0
- aether/audio/sound.py +6 -0
- aether/core/__init__.py +211 -0
- aether/core/achievement_system.py +304 -0
- aether/core/application.py +19 -0
- aether/core/config.py +75 -0
- aether/core/console.py +366 -0
- aether/core/engine.py +50 -0
- aether/core/event_bus.py +24 -0
- aether/core/input.py +58 -0
- aether/core/plugin_system.py +93 -0
- aether/core/save_system.py +409 -0
- aether/core/time_manager.py +25 -0
- aether/core/window.py +57 -0
- aether/graphics/__init__.py +0 -0
- aether/graphics/camera.py +69 -0
- aether/graphics/framebuffer.py +119 -0
- aether/graphics/light.py +32 -0
- aether/graphics/lod_system.py +170 -0
- aether/graphics/material.py +25 -0
- aether/graphics/mesh.py +83 -0
- aether/graphics/mesh_factory.py +67 -0
- aether/graphics/particle_system.py +268 -0
- aether/graphics/post_process.py +279 -0
- aether/graphics/renderer.py +135 -0
- aether/graphics/shader.py +61 -0
- aether/graphics/shader_library.py +157 -0
- aether/graphics/sky_atmosphere.py +309 -0
- aether/graphics/terrain.py +408 -0
- aether/graphics/texture.py +34 -0
- aether/graphics/water.py +432 -0
- aether/physics/__init__.py +0 -0
- aether/physics/bullet_backend.py +395 -0
- aether/physics/collider.py +22 -0
- aether/physics/physics_world.py +436 -0
- aether/physics/raycast.py +25 -0
- aether/physics/rigidbody.py +7 -0
- aether/resources/__init__.py +0 -0
- aether/resources/asset_database.py +76 -0
- aether/resources/resource_manager.py +147 -0
- aether/resources/serialization.py +114 -0
- aether/scene/__init__.py +0 -0
- aether/scene/component.py +368 -0
- aether/scene/entity.py +46 -0
- aether/scene/scene.py +33 -0
- aether/scene/scene_graph.py +252 -0
- aether/scene/scene_manager.py +191 -0
- aether/ui/__init__.py +0 -0
- aether/ui/button.py +297 -0
- aether/ui/canvas.py +11 -0
- aether/ui/text.py +286 -0
- aether/utils/__init__.py +0 -0
- aether/utils/logger.py +16 -0
- aether/utils/math_utils.py +31 -0
- aether/utils/object_pool.py +137 -0
- aether/utils/profiler.py +187 -0
- aether_engine-1.0.0.dist-info/METADATA +71 -0
- aether_engine-1.0.0.dist-info/RECORD +64 -0
- aether_engine-1.0.0.dist-info/WHEEL +5 -0
- aether_engine-1.0.0.dist-info/licenses/LICENSE +21 -0
- aether_engine-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Сериализация сцен и префабов"""
|
|
2
|
+
import json
|
|
3
|
+
import yaml
|
|
4
|
+
from typing import Dict, Any, Type
|
|
5
|
+
from aether.scene.entity import Entity
|
|
6
|
+
from aether.scene.component import Component
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Serializer:
|
|
10
|
+
"""Сериализатор игровых объектов"""
|
|
11
|
+
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self._component_serializers: Dict[str, Type['ComponentSerializer']] = {}
|
|
14
|
+
|
|
15
|
+
def register_component(self, component_class, serializer):
|
|
16
|
+
"""Регистрирует сериализатор для компонента"""
|
|
17
|
+
self._component_serializers[component_class.__name__] = serializer
|
|
18
|
+
|
|
19
|
+
def serialize_entity(self, entity: Entity) -> dict:
|
|
20
|
+
"""Сериализует Entity в словарь"""
|
|
21
|
+
data = {
|
|
22
|
+
'id': entity.id,
|
|
23
|
+
'name': entity.name,
|
|
24
|
+
'tag': entity.tag,
|
|
25
|
+
'enabled': entity.enabled,
|
|
26
|
+
'components': {}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
for comp_name, component in entity.components.items():
|
|
30
|
+
if comp_name in self._component_serializers:
|
|
31
|
+
serializer = self._component_serializers[comp_name]
|
|
32
|
+
data['components'][comp_name] = serializer.serialize(component)
|
|
33
|
+
|
|
34
|
+
if entity.children:
|
|
35
|
+
data['children'] = [
|
|
36
|
+
self.serialize_entity(child)
|
|
37
|
+
for child in entity.children
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
return data
|
|
41
|
+
|
|
42
|
+
def deserialize_entity(self, data: dict) -> Entity:
|
|
43
|
+
"""Создаёт Entity из словаря"""
|
|
44
|
+
entity = Entity(name=data['name'], tag=data.get('tag', ''))
|
|
45
|
+
entity.id = data.get('id', entity.id)
|
|
46
|
+
entity.enabled = data.get('enabled', True)
|
|
47
|
+
|
|
48
|
+
for comp_name, comp_data in data.get('components', {}).items():
|
|
49
|
+
if comp_name in self._component_serializers:
|
|
50
|
+
serializer = self._component_serializers[comp_name]
|
|
51
|
+
component = serializer.deserialize(comp_data)
|
|
52
|
+
entity.add_component(component)
|
|
53
|
+
|
|
54
|
+
for child_data in data.get('children', []):
|
|
55
|
+
child = self.deserialize_entity(child_data)
|
|
56
|
+
entity.add_child(child)
|
|
57
|
+
|
|
58
|
+
return entity
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ComponentSerializer(ABC):
|
|
62
|
+
"""Базовый класс сериализатора компонентов"""
|
|
63
|
+
|
|
64
|
+
@abstractmethod
|
|
65
|
+
def serialize(self, component: Component) -> dict:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
@abstractmethod
|
|
69
|
+
def deserialize(self, data: dict) -> Component:
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class SceneSerializer:
|
|
74
|
+
"""Сериализатор целых сцен"""
|
|
75
|
+
|
|
76
|
+
def __init__(self, entity_serializer: Serializer):
|
|
77
|
+
self.entity_serializer = entity_serializer
|
|
78
|
+
|
|
79
|
+
def save_scene(self, scene, path: str):
|
|
80
|
+
"""Сохраняет сцену в файл"""
|
|
81
|
+
data = {
|
|
82
|
+
'name': scene.name,
|
|
83
|
+
'version': '1.0',
|
|
84
|
+
'entities': []
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for entity in scene.entities:
|
|
88
|
+
entity_data = self.entity_serializer.serialize_entity(entity)
|
|
89
|
+
data['entities'].append(entity_data)
|
|
90
|
+
|
|
91
|
+
with open(path, 'w') as f:
|
|
92
|
+
if path.endswith('.json'):
|
|
93
|
+
json.dump(data, f, indent=2)
|
|
94
|
+
elif path.endswith('.yaml') or path.endswith('.yml'):
|
|
95
|
+
yaml.dump(data, f, default_flow_style=False)
|
|
96
|
+
|
|
97
|
+
def load_scene(self, path: str) -> 'Scene':
|
|
98
|
+
"""Загружает сцену из файла"""
|
|
99
|
+
with open(path, 'r') as f:
|
|
100
|
+
if path.endswith('.json'):
|
|
101
|
+
data = json.load(f)
|
|
102
|
+
elif path.endswith('.yaml') or path.endswith('.yml'):
|
|
103
|
+
data = yaml.safe_load(f)
|
|
104
|
+
else:
|
|
105
|
+
raise ValueError(f"Unsupported format: {path}")
|
|
106
|
+
|
|
107
|
+
from aether.scene.scene import Scene
|
|
108
|
+
scene = Scene(name=data['name'])
|
|
109
|
+
|
|
110
|
+
for entity_data in data.get('entities', []):
|
|
111
|
+
entity = self.entity_serializer.deserialize_entity(entity_data)
|
|
112
|
+
scene.spawn(entity)
|
|
113
|
+
|
|
114
|
+
return scene
|
aether/scene/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"""Компоненты для Entity"""
|
|
2
|
+
import numpy as np
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from OpenGL.GL import *
|
|
5
|
+
from aether.graphics.shader import Shader
|
|
6
|
+
from aether.graphics.shader_library import ShaderLibrary
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Component(ABC):
|
|
10
|
+
"""Базовый класс компонента"""
|
|
11
|
+
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self.entity = None
|
|
14
|
+
self.enabled = True
|
|
15
|
+
self._started = False
|
|
16
|
+
|
|
17
|
+
def awake(self):
|
|
18
|
+
"""Вызывается при добавлении компонента"""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
def start(self):
|
|
22
|
+
"""Вызывается один раз перед первым update"""
|
|
23
|
+
self._started = True
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def update(self, dt: float):
|
|
27
|
+
"""Вызывается каждый кадр"""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
def on_enable(self):
|
|
31
|
+
"""Вызывается при включении компонента"""
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
def on_disable(self):
|
|
35
|
+
"""Вызывается при выключении компонента"""
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
def destroy(self):
|
|
39
|
+
"""Вызывается при удалении компонента"""
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Transform(Component):
|
|
44
|
+
"""Компонент трансформации"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, position=(0, 0, 0), rotation=(0, 0, 0), scale=(1, 1, 1)):
|
|
47
|
+
super().__init__()
|
|
48
|
+
self.position = np.array(position, dtype=np.float32)
|
|
49
|
+
self.rotation = np.array(rotation, dtype=np.float32) # Euler angles
|
|
50
|
+
self.scale = np.array(scale, dtype=np.float32)
|
|
51
|
+
|
|
52
|
+
# Кэшированная матрица
|
|
53
|
+
self._matrix_dirty = True
|
|
54
|
+
self._cached_matrix = np.identity(4, dtype=np.float32)
|
|
55
|
+
|
|
56
|
+
def update(self, dt: float):
|
|
57
|
+
if self._matrix_dirty:
|
|
58
|
+
self._update_matrix()
|
|
59
|
+
|
|
60
|
+
def _update_matrix(self):
|
|
61
|
+
"""Обновляет модельную матрицу"""
|
|
62
|
+
# Translate
|
|
63
|
+
m = np.identity(4, dtype=np.float32)
|
|
64
|
+
m[3, :3] = self.position
|
|
65
|
+
|
|
66
|
+
# Rotation
|
|
67
|
+
rx, ry, rz = np.radians(self.rotation)
|
|
68
|
+
|
|
69
|
+
# Rotate X
|
|
70
|
+
cx, sx = np.cos(rx), np.sin(rx)
|
|
71
|
+
rot_x = np.array([
|
|
72
|
+
[1, 0, 0],
|
|
73
|
+
[0, cx, -sx],
|
|
74
|
+
[0, sx, cx]
|
|
75
|
+
], dtype=np.float32)
|
|
76
|
+
|
|
77
|
+
# Rotate Y
|
|
78
|
+
cy, sy = np.cos(ry), np.sin(ry)
|
|
79
|
+
rot_y = np.array([
|
|
80
|
+
[cy, 0, sy],
|
|
81
|
+
[0, 1, 0],
|
|
82
|
+
[-sy, 0, cy]
|
|
83
|
+
], dtype=np.float32)
|
|
84
|
+
|
|
85
|
+
# Rotate Z
|
|
86
|
+
cz, sz = np.cos(rz), np.sin(rz)
|
|
87
|
+
rot_z = np.array([
|
|
88
|
+
[cz, -sz, 0],
|
|
89
|
+
[sz, cz, 0],
|
|
90
|
+
[0, 0, 1]
|
|
91
|
+
], dtype=np.float32)
|
|
92
|
+
|
|
93
|
+
# Комбинированная ротация
|
|
94
|
+
rot = rot_z @ rot_y @ rot_x
|
|
95
|
+
|
|
96
|
+
# Применяем ротацию и скейл
|
|
97
|
+
m[:3, :3] = rot * self.scale
|
|
98
|
+
|
|
99
|
+
self._cached_matrix = m
|
|
100
|
+
self._matrix_dirty = False
|
|
101
|
+
|
|
102
|
+
def get_model_matrix(self) -> np.ndarray:
|
|
103
|
+
"""Возвращает модельную матрицу (с кэшированием)"""
|
|
104
|
+
if self._matrix_dirty:
|
|
105
|
+
self._update_matrix()
|
|
106
|
+
return self._cached_matrix
|
|
107
|
+
|
|
108
|
+
def translate(self, dx, dy, dz):
|
|
109
|
+
"""Перемещает объект"""
|
|
110
|
+
self.position += np.array([dx, dy, dz], dtype=np.float32)
|
|
111
|
+
self._matrix_dirty = True
|
|
112
|
+
|
|
113
|
+
def set_position(self, x, y, z):
|
|
114
|
+
"""Устанавливает позицию"""
|
|
115
|
+
self.position = np.array([x, y, z], dtype=np.float32)
|
|
116
|
+
self._matrix_dirty = True
|
|
117
|
+
|
|
118
|
+
def rotate(self, rx, ry, rz):
|
|
119
|
+
"""Поворачивает объект"""
|
|
120
|
+
self.rotation += np.array([rx, ry, rz], dtype=np.float32)
|
|
121
|
+
self._matrix_dirty = True
|
|
122
|
+
|
|
123
|
+
def look_at(self, target: np.ndarray):
|
|
124
|
+
"""Поворачивает объект в сторону цели"""
|
|
125
|
+
direction = target - self.position
|
|
126
|
+
direction = direction / np.linalg.norm(direction)
|
|
127
|
+
|
|
128
|
+
# Вычисляем углы Эйлера из направления
|
|
129
|
+
self.rotation[0] = np.degrees(np.arcsin(-direction[1]))
|
|
130
|
+
self.rotation[1] = np.degrees(np.arctan2(-direction[0], -direction[2]))
|
|
131
|
+
self._matrix_dirty = True
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class MeshRenderer(Component):
|
|
135
|
+
"""Компонент рендеринга меша"""
|
|
136
|
+
|
|
137
|
+
def __init__(self, mesh=None, material=None, cast_shadows=True, receive_shadows=True):
|
|
138
|
+
super().__init__()
|
|
139
|
+
self.mesh = mesh
|
|
140
|
+
self.material = material
|
|
141
|
+
self.visible = True
|
|
142
|
+
self.cast_shadows = cast_shadows
|
|
143
|
+
self.receive_shadows = receive_shadows
|
|
144
|
+
self._shader_library = ShaderLibrary()
|
|
145
|
+
|
|
146
|
+
def update(self, dt: float):
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
def render(self, view: np.ndarray, projection: np.ndarray):
|
|
150
|
+
"""Рендерит меш"""
|
|
151
|
+
if not self.mesh or not self.material or not self.visible:
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
if not self.entity:
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
transform = self.entity.get_component("Transform")
|
|
158
|
+
if not transform:
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
# Получаем шейдер
|
|
162
|
+
shader = self._shader_library.get_pbr_shader()
|
|
163
|
+
if not shader:
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
shader.use()
|
|
167
|
+
|
|
168
|
+
# Устанавливаем матрицы
|
|
169
|
+
model = transform.get_model_matrix()
|
|
170
|
+
shader.set_mat4("model", model)
|
|
171
|
+
shader.set_mat4("view", view)
|
|
172
|
+
shader.set_mat4("projection", projection)
|
|
173
|
+
|
|
174
|
+
# Устанавливаем свойства материала
|
|
175
|
+
shader.set_vec3("albedo", self.material.albedo)
|
|
176
|
+
shader.set_float("metallic", self.material.metallic)
|
|
177
|
+
shader.set_float("roughness", self.material.roughness)
|
|
178
|
+
shader.set_float("ao", self.material.ao)
|
|
179
|
+
|
|
180
|
+
# Устанавливаем текстуры
|
|
181
|
+
if self.material.albedo_map:
|
|
182
|
+
self.material.albedo_map.bind(0)
|
|
183
|
+
shader.set_int("albedoMap", 0)
|
|
184
|
+
|
|
185
|
+
if self.material.normal_map:
|
|
186
|
+
self.material.normal_map.bind(1)
|
|
187
|
+
shader.set_int("normalMap", 1)
|
|
188
|
+
|
|
189
|
+
if self.material.metallic_map:
|
|
190
|
+
self.material.metallic_map.bind(2)
|
|
191
|
+
shader.set_int("metallicMap", 2)
|
|
192
|
+
|
|
193
|
+
if self.material.roughness_map:
|
|
194
|
+
self.material.roughness_map.bind(3)
|
|
195
|
+
shader.set_int("roughnessMap", 3)
|
|
196
|
+
|
|
197
|
+
# Рендерим меш
|
|
198
|
+
self.mesh.draw()
|
|
199
|
+
|
|
200
|
+
def destroy(self):
|
|
201
|
+
"""Очищает ресурсы"""
|
|
202
|
+
if self.mesh:
|
|
203
|
+
self.mesh.cleanup()
|
|
204
|
+
self.mesh = None
|
|
205
|
+
self.material = None
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class CameraComponent(Component):
|
|
209
|
+
"""Компонент камеры"""
|
|
210
|
+
|
|
211
|
+
def __init__(self, camera=None):
|
|
212
|
+
super().__init__()
|
|
213
|
+
self.camera = camera
|
|
214
|
+
self.is_main = False
|
|
215
|
+
|
|
216
|
+
def update(self, dt: float):
|
|
217
|
+
pass
|
|
218
|
+
|
|
219
|
+
def set_as_main(self):
|
|
220
|
+
"""Делает камеру основной в сцене"""
|
|
221
|
+
if self.entity and hasattr(self.entity, 'scene'):
|
|
222
|
+
self.entity.scene.set_main_camera(self.camera)
|
|
223
|
+
self.is_main = True
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class LightComponent(Component):
|
|
227
|
+
"""Компонент источника света"""
|
|
228
|
+
|
|
229
|
+
def __init__(self, light=None):
|
|
230
|
+
super().__init__()
|
|
231
|
+
self.light = light
|
|
232
|
+
self.intensity_multiplier = 1.0
|
|
233
|
+
|
|
234
|
+
def update(self, dt: float):
|
|
235
|
+
if self.light and self.entity:
|
|
236
|
+
transform = self.entity.get_component("Transform")
|
|
237
|
+
if transform:
|
|
238
|
+
self.light.position = transform.position
|
|
239
|
+
self.light.direction = -transform.position / np.linalg.norm(transform.position)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class RigidbodyComponent(Component):
|
|
243
|
+
"""Компонент физического тела"""
|
|
244
|
+
|
|
245
|
+
def __init__(self, mass=1.0, use_gravity=True, is_kinematic=False):
|
|
246
|
+
super().__init__()
|
|
247
|
+
self.mass = mass
|
|
248
|
+
self.use_gravity = use_gravity
|
|
249
|
+
self.is_kinematic = is_kinematic
|
|
250
|
+
self.velocity = np.array([0.0, 0.0, 0.0], dtype=np.float32)
|
|
251
|
+
self.angular_velocity = np.array([0.0, 0.0, 0.0], dtype=np.float32)
|
|
252
|
+
self.drag = 0.0
|
|
253
|
+
self.angular_drag = 0.05
|
|
254
|
+
|
|
255
|
+
def update(self, dt: float):
|
|
256
|
+
if self.is_kinematic:
|
|
257
|
+
return
|
|
258
|
+
|
|
259
|
+
transform = self.entity.get_component("Transform")
|
|
260
|
+
if not transform:
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
# Применяем drag
|
|
264
|
+
self.velocity *= (1.0 - self.drag * dt)
|
|
265
|
+
self.angular_velocity *= (1.0 - self.angular_drag * dt)
|
|
266
|
+
|
|
267
|
+
# Обновляем позицию
|
|
268
|
+
transform.position += self.velocity * dt
|
|
269
|
+
transform.rotation += self.angular_velocity * dt
|
|
270
|
+
transform._matrix_dirty = True
|
|
271
|
+
|
|
272
|
+
def add_force(self, force: np.ndarray):
|
|
273
|
+
"""Добавляет силу"""
|
|
274
|
+
if not self.is_kinematic and self.mass > 0:
|
|
275
|
+
self.velocity += force / self.mass
|
|
276
|
+
|
|
277
|
+
def add_impulse(self, impulse: np.ndarray):
|
|
278
|
+
"""Мгновенный импульс"""
|
|
279
|
+
if not self.is_kinematic and self.mass > 0:
|
|
280
|
+
self.velocity += impulse / self.mass
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class ScriptComponent(Component):
|
|
284
|
+
"""Компонент для пользовательских скриптов"""
|
|
285
|
+
|
|
286
|
+
def __init__(self):
|
|
287
|
+
super().__init__()
|
|
288
|
+
self._scripts = []
|
|
289
|
+
|
|
290
|
+
def add_script(self, script):
|
|
291
|
+
"""Добавляет скрипт"""
|
|
292
|
+
self._scripts.append(script)
|
|
293
|
+
if hasattr(script, 'entity'):
|
|
294
|
+
script.entity = self.entity
|
|
295
|
+
if hasattr(script, 'awake'):
|
|
296
|
+
script.awake()
|
|
297
|
+
if hasattr(script, 'start'):
|
|
298
|
+
script.start()
|
|
299
|
+
|
|
300
|
+
def update(self, dt: float):
|
|
301
|
+
for script in self._scripts:
|
|
302
|
+
if hasattr(script, 'update'):
|
|
303
|
+
script.update(dt)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class AnimatorComponent(Component):
|
|
307
|
+
"""Компонент анимации"""
|
|
308
|
+
|
|
309
|
+
def __init__(self):
|
|
310
|
+
super().__init__()
|
|
311
|
+
self.animations = {}
|
|
312
|
+
self.current_animation = None
|
|
313
|
+
self.playback_speed = 1.0
|
|
314
|
+
self._time = 0.0
|
|
315
|
+
self._playing = False
|
|
316
|
+
self._loop = True
|
|
317
|
+
|
|
318
|
+
def add_animation(self, name: str, animation):
|
|
319
|
+
"""Добавляет анимацию"""
|
|
320
|
+
self.animations[name] = animation
|
|
321
|
+
|
|
322
|
+
def play(self, name: str, loop: bool = True):
|
|
323
|
+
"""Проигрывает анимацию"""
|
|
324
|
+
if name in self.animations:
|
|
325
|
+
self.current_animation = name
|
|
326
|
+
self._time = 0.0
|
|
327
|
+
self._playing = True
|
|
328
|
+
self._loop = loop
|
|
329
|
+
|
|
330
|
+
def stop(self):
|
|
331
|
+
"""Останавливает анимацию"""
|
|
332
|
+
self._playing = False
|
|
333
|
+
self._time = 0.0
|
|
334
|
+
|
|
335
|
+
def pause(self):
|
|
336
|
+
"""Ставит на паузу"""
|
|
337
|
+
self._playing = False
|
|
338
|
+
|
|
339
|
+
def resume(self):
|
|
340
|
+
"""Продолжает проигрывание"""
|
|
341
|
+
self._playing = True
|
|
342
|
+
|
|
343
|
+
def update(self, dt: float):
|
|
344
|
+
if not self._playing or not self.current_animation:
|
|
345
|
+
return
|
|
346
|
+
|
|
347
|
+
anim = self.animations.get(self.current_animation)
|
|
348
|
+
if not anim:
|
|
349
|
+
return
|
|
350
|
+
|
|
351
|
+
self._time += dt * self.playback_speed
|
|
352
|
+
|
|
353
|
+
if self._time >= anim.duration:
|
|
354
|
+
if self._loop:
|
|
355
|
+
self._time %= anim.duration
|
|
356
|
+
else:
|
|
357
|
+
self._time = anim.duration
|
|
358
|
+
self._playing = False
|
|
359
|
+
|
|
360
|
+
# Применяем анимацию к трансформу
|
|
361
|
+
transform = self.entity.get_component("Transform")
|
|
362
|
+
if transform:
|
|
363
|
+
frame_data = anim.get_frame(self._time)
|
|
364
|
+
if frame_data:
|
|
365
|
+
transform.position = frame_data.get('position', transform.position)
|
|
366
|
+
transform.rotation = frame_data.get('rotation', transform.rotation)
|
|
367
|
+
transform.scale = frame_data.get('scale', transform.scale)
|
|
368
|
+
transform._matrix_dirty = True
|
aether/scene/entity.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Игровой объект"""
|
|
2
|
+
from typing import Dict, Optional, List
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Entity:
|
|
7
|
+
def __init__(self, name="Entity", tag=""):
|
|
8
|
+
self.id = str(uuid.uuid4())
|
|
9
|
+
self.name = name
|
|
10
|
+
self.tag = tag
|
|
11
|
+
self.enabled = True
|
|
12
|
+
self.components: Dict[str, 'Component'] = {}
|
|
13
|
+
self.children: List['Entity'] = []
|
|
14
|
+
self.parent: Optional['Entity'] = None
|
|
15
|
+
|
|
16
|
+
def add_component(self, component: 'Component'):
|
|
17
|
+
component.entity = self
|
|
18
|
+
self.components[component.__class__.__name__] = component
|
|
19
|
+
component.awake()
|
|
20
|
+
component.start()
|
|
21
|
+
return component
|
|
22
|
+
|
|
23
|
+
def get_component(self, name: str) -> Optional['Component']:
|
|
24
|
+
return self.components.get(name)
|
|
25
|
+
|
|
26
|
+
def remove_component(self, name: str):
|
|
27
|
+
if name in self.components:
|
|
28
|
+
self.components[name].destroy()
|
|
29
|
+
del self.components[name]
|
|
30
|
+
|
|
31
|
+
def add_child(self, child: 'Entity'):
|
|
32
|
+
child.parent = self
|
|
33
|
+
self.children.append(child)
|
|
34
|
+
|
|
35
|
+
def update(self, dt: float):
|
|
36
|
+
for comp in self.components.values():
|
|
37
|
+
if comp.enabled:
|
|
38
|
+
comp.update(dt)
|
|
39
|
+
for child in self.children:
|
|
40
|
+
child.update(dt)
|
|
41
|
+
|
|
42
|
+
def destroy(self):
|
|
43
|
+
for comp in list(self.components.values()):
|
|
44
|
+
comp.destroy()
|
|
45
|
+
for child in self.children:
|
|
46
|
+
child.destroy()
|
aether/scene/scene.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Сцена"""
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
from aether.scene.entity import Entity
|
|
4
|
+
from aether.graphics.camera import Camera
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Scene:
|
|
8
|
+
def __init__(self, name="Scene"):
|
|
9
|
+
self.name = name
|
|
10
|
+
self.entities: List[Entity] = []
|
|
11
|
+
self.main_camera: Optional[Camera] = None
|
|
12
|
+
|
|
13
|
+
def spawn(self, entity: Entity):
|
|
14
|
+
self.entities.append(entity)
|
|
15
|
+
return entity
|
|
16
|
+
|
|
17
|
+
def set_main_camera(self, camera: Camera):
|
|
18
|
+
self.main_camera = camera
|
|
19
|
+
|
|
20
|
+
def awake(self):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
def start(self):
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
def update(self, dt: float):
|
|
27
|
+
for entity in self.entities:
|
|
28
|
+
if entity.enabled:
|
|
29
|
+
entity.update(dt)
|
|
30
|
+
|
|
31
|
+
def destroy(self):
|
|
32
|
+
for entity in self.entities:
|
|
33
|
+
entity.destroy()
|