aether-engine 1.0.0__tar.gz
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_engine-1.0.0/LICENSE +21 -0
- aether_engine-1.0.0/MANIFEST.in +14 -0
- aether_engine-1.0.0/PKG-INFO +71 -0
- aether_engine-1.0.0/README.md +32 -0
- aether_engine-1.0.0/aether/__init__.py +54 -0
- aether_engine-1.0.0/aether/ai/behavior_tree.py +403 -0
- aether_engine-1.0.0/aether/ai/navmesh.py +265 -0
- aether_engine-1.0.0/aether/animation/skeleton.py +296 -0
- aether_engine-1.0.0/aether/assets/asset_pipeline.py +365 -0
- aether_engine-1.0.0/aether/audio/__init__.py +0 -0
- aether_engine-1.0.0/aether/audio/audio_manager.py +211 -0
- aether_engine-1.0.0/aether/audio/music.py +6 -0
- aether_engine-1.0.0/aether/audio/sound.py +6 -0
- aether_engine-1.0.0/aether/core/__init__.py +211 -0
- aether_engine-1.0.0/aether/core/achievement_system.py +304 -0
- aether_engine-1.0.0/aether/core/application.py +19 -0
- aether_engine-1.0.0/aether/core/config.py +75 -0
- aether_engine-1.0.0/aether/core/console.py +366 -0
- aether_engine-1.0.0/aether/core/engine.py +50 -0
- aether_engine-1.0.0/aether/core/event_bus.py +24 -0
- aether_engine-1.0.0/aether/core/input.py +58 -0
- aether_engine-1.0.0/aether/core/plugin_system.py +93 -0
- aether_engine-1.0.0/aether/core/save_system.py +409 -0
- aether_engine-1.0.0/aether/core/time_manager.py +25 -0
- aether_engine-1.0.0/aether/core/window.py +57 -0
- aether_engine-1.0.0/aether/editor/gizmo.py +346 -0
- aether_engine-1.0.0/aether/editor/imgui_integration.py +468 -0
- aether_engine-1.0.0/aether/environment/environment_system.py +291 -0
- aether_engine-1.0.0/aether/graphics/__init__.py +0 -0
- aether_engine-1.0.0/aether/graphics/camera.py +69 -0
- aether_engine-1.0.0/aether/graphics/framebuffer.py +119 -0
- aether_engine-1.0.0/aether/graphics/light.py +32 -0
- aether_engine-1.0.0/aether/graphics/lod_system.py +170 -0
- aether_engine-1.0.0/aether/graphics/material.py +25 -0
- aether_engine-1.0.0/aether/graphics/mesh.py +83 -0
- aether_engine-1.0.0/aether/graphics/mesh_factory.py +67 -0
- aether_engine-1.0.0/aether/graphics/particle_system.py +268 -0
- aether_engine-1.0.0/aether/graphics/post_process.py +279 -0
- aether_engine-1.0.0/aether/graphics/renderer.py +135 -0
- aether_engine-1.0.0/aether/graphics/shader.py +61 -0
- aether_engine-1.0.0/aether/graphics/shader_library.py +157 -0
- aether_engine-1.0.0/aether/graphics/sky_atmosphere.py +309 -0
- aether_engine-1.0.0/aether/graphics/terrain.py +408 -0
- aether_engine-1.0.0/aether/graphics/texture.py +34 -0
- aether_engine-1.0.0/aether/graphics/water.py +432 -0
- aether_engine-1.0.0/aether/network/network_manager.py +395 -0
- aether_engine-1.0.0/aether/physics/__init__.py +0 -0
- aether_engine-1.0.0/aether/physics/bullet_backend.py +395 -0
- aether_engine-1.0.0/aether/physics/collider.py +22 -0
- aether_engine-1.0.0/aether/physics/physics_world.py +436 -0
- aether_engine-1.0.0/aether/physics/raycast.py +25 -0
- aether_engine-1.0.0/aether/physics/rigidbody.py +7 -0
- aether_engine-1.0.0/aether/platform/steam_api.py +378 -0
- aether_engine-1.0.0/aether/resources/__init__.py +0 -0
- aether_engine-1.0.0/aether/resources/asset_database.py +76 -0
- aether_engine-1.0.0/aether/resources/loaders/obj_loader.py +104 -0
- aether_engine-1.0.0/aether/resources/loaders/texture_loader.py +261 -0
- aether_engine-1.0.0/aether/resources/resource_manager.py +147 -0
- aether_engine-1.0.0/aether/resources/serialization.py +114 -0
- aether_engine-1.0.0/aether/scene/__init__.py +0 -0
- aether_engine-1.0.0/aether/scene/component.py +368 -0
- aether_engine-1.0.0/aether/scene/entity.py +46 -0
- aether_engine-1.0.0/aether/scene/scene.py +33 -0
- aether_engine-1.0.0/aether/scene/scene_graph.py +252 -0
- aether_engine-1.0.0/aether/scene/scene_manager.py +191 -0
- aether_engine-1.0.0/aether/ui/__init__.py +0 -0
- aether_engine-1.0.0/aether/ui/button.py +297 -0
- aether_engine-1.0.0/aether/ui/canvas.py +11 -0
- aether_engine-1.0.0/aether/ui/text.py +286 -0
- aether_engine-1.0.0/aether/utils/__init__.py +0 -0
- aether_engine-1.0.0/aether/utils/logger.py +16 -0
- aether_engine-1.0.0/aether/utils/math_utils.py +31 -0
- aether_engine-1.0.0/aether/utils/object_pool.py +137 -0
- aether_engine-1.0.0/aether/utils/profiler.py +187 -0
- aether_engine-1.0.0/aether_engine.egg-info/PKG-INFO +71 -0
- aether_engine-1.0.0/aether_engine.egg-info/SOURCES.txt +82 -0
- aether_engine-1.0.0/aether_engine.egg-info/dependency_links.txt +1 -0
- aether_engine-1.0.0/aether_engine.egg-info/requires.txt +19 -0
- aether_engine-1.0.0/aether_engine.egg-info/top_level.txt +1 -0
- aether_engine-1.0.0/examples/01_hello_cube.py +28 -0
- aether_engine-1.0.0/examples/02_sphere_scene.py +42 -0
- aether_engine-1.0.0/pyproject.toml +3 -0
- aether_engine-1.0.0/setup.cfg +4 -0
- aether_engine-1.0.0/setup.py +54 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 entiti937
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
include README.md
|
|
2
|
+
include LICENSE
|
|
3
|
+
include pyproject.toml
|
|
4
|
+
|
|
5
|
+
recursive-include aether *.py
|
|
6
|
+
recursive-include aether *.glsl
|
|
7
|
+
recursive-include aether *.vert
|
|
8
|
+
recursive-include aether *.frag
|
|
9
|
+
recursive-include aether *.json
|
|
10
|
+
|
|
11
|
+
recursive-include examples *.py
|
|
12
|
+
|
|
13
|
+
exclude __pycache__
|
|
14
|
+
exclude *.pyc
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aether-engine
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Полноценный 3D игровой движок на Python с OpenGL
|
|
5
|
+
Author: entiti937
|
|
6
|
+
Classifier: Development Status :: 4 - Beta
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: numpy>=1.24
|
|
15
|
+
Requires-Dist: glfw>=2.6
|
|
16
|
+
Requires-Dist: PyOpenGL>=3.1.7
|
|
17
|
+
Requires-Dist: Pillow>=10.0
|
|
18
|
+
Provides-Extra: physics
|
|
19
|
+
Requires-Dist: PyBullet>=3.2.5; extra == "physics"
|
|
20
|
+
Provides-Extra: audio
|
|
21
|
+
Requires-Dist: pygame>=2.5; extra == "audio"
|
|
22
|
+
Provides-Extra: full
|
|
23
|
+
Requires-Dist: PyBullet>=3.2.5; extra == "full"
|
|
24
|
+
Requires-Dist: pygame>=2.5; extra == "full"
|
|
25
|
+
Requires-Dist: freetype-py>=2.4; extra == "full"
|
|
26
|
+
Requires-Dist: imgui>=2.0; extra == "full"
|
|
27
|
+
Requires-Dist: noise>=1.2; extra == "full"
|
|
28
|
+
Requires-Dist: pyyaml>=6.0; extra == "full"
|
|
29
|
+
Requires-Dist: imageio>=2.31; extra == "full"
|
|
30
|
+
Dynamic: author
|
|
31
|
+
Dynamic: classifier
|
|
32
|
+
Dynamic: description
|
|
33
|
+
Dynamic: description-content-type
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
Dynamic: provides-extra
|
|
36
|
+
Dynamic: requires-dist
|
|
37
|
+
Dynamic: requires-python
|
|
38
|
+
Dynamic: summary
|
|
39
|
+
|
|
40
|
+
# Aether Engine
|
|
41
|
+
|
|
42
|
+
Полноценный 3D игровой движок на Python с OpenGL 4.6.
|
|
43
|
+
|
|
44
|
+
## Возможности
|
|
45
|
+
- PBR-рендеринг
|
|
46
|
+
- Система Entity-Component
|
|
47
|
+
- Физика (базовая)
|
|
48
|
+
- Звуковая система
|
|
49
|
+
- UI-фреймворк
|
|
50
|
+
- Загрузка моделей (OBJ)
|
|
51
|
+
- Пост-обработка
|
|
52
|
+
|
|
53
|
+
## Установка
|
|
54
|
+
```bash
|
|
55
|
+
pip install -e .
|
|
56
|
+
# Быстрый старт
|
|
57
|
+
from aether import Engine, Scene, Entity, MeshFactory, Material
|
|
58
|
+
|
|
59
|
+
engine = Engine(1280, 720, "Моя игра")
|
|
60
|
+
scene = Scene("Level 1")
|
|
61
|
+
|
|
62
|
+
cube = Entity("Куб")
|
|
63
|
+
cube.add_component(Transform())
|
|
64
|
+
cube.add_component(MeshRenderer(
|
|
65
|
+
mesh=MeshFactory.create_cube(),
|
|
66
|
+
material=Material()
|
|
67
|
+
))
|
|
68
|
+
|
|
69
|
+
scene.spawn(cube)
|
|
70
|
+
engine.load_scene(scene)
|
|
71
|
+
engine.run()
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Aether Engine
|
|
2
|
+
|
|
3
|
+
Полноценный 3D игровой движок на Python с OpenGL 4.6.
|
|
4
|
+
|
|
5
|
+
## Возможности
|
|
6
|
+
- PBR-рендеринг
|
|
7
|
+
- Система Entity-Component
|
|
8
|
+
- Физика (базовая)
|
|
9
|
+
- Звуковая система
|
|
10
|
+
- UI-фреймворк
|
|
11
|
+
- Загрузка моделей (OBJ)
|
|
12
|
+
- Пост-обработка
|
|
13
|
+
|
|
14
|
+
## Установка
|
|
15
|
+
```bash
|
|
16
|
+
pip install -e .
|
|
17
|
+
# Быстрый старт
|
|
18
|
+
from aether import Engine, Scene, Entity, MeshFactory, Material
|
|
19
|
+
|
|
20
|
+
engine = Engine(1280, 720, "Моя игра")
|
|
21
|
+
scene = Scene("Level 1")
|
|
22
|
+
|
|
23
|
+
cube = Entity("Куб")
|
|
24
|
+
cube.add_component(Transform())
|
|
25
|
+
cube.add_component(MeshRenderer(
|
|
26
|
+
mesh=MeshFactory.create_cube(),
|
|
27
|
+
material=Material()
|
|
28
|
+
))
|
|
29
|
+
|
|
30
|
+
scene.spawn(cube)
|
|
31
|
+
engine.load_scene(scene)
|
|
32
|
+
engine.run()
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Aether Engine — полноценный 3D игровой движок на Python с OpenGL
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
__version__ = "1.0.0"
|
|
6
|
+
__author__ = "entiti937"
|
|
7
|
+
|
|
8
|
+
# Ядро
|
|
9
|
+
from aether.core.engine import Engine
|
|
10
|
+
from aether.core.application import Application
|
|
11
|
+
from aether.core.config import EngineConfig, GraphicsConfig, PhysicsConfig, AudioConfig
|
|
12
|
+
|
|
13
|
+
# Сцена
|
|
14
|
+
from aether.scene.entity import Entity
|
|
15
|
+
from aether.scene.component import (
|
|
16
|
+
Transform, MeshRenderer, CameraComponent,
|
|
17
|
+
LightComponent, RigidbodyComponent, ScriptComponent, AnimatorComponent
|
|
18
|
+
)
|
|
19
|
+
from aether.scene.scene import Scene
|
|
20
|
+
from aether.scene.scene_manager import SceneManager
|
|
21
|
+
|
|
22
|
+
# Графика
|
|
23
|
+
from aether.graphics.shader import Shader
|
|
24
|
+
from aether.graphics.shader_library import ShaderLibrary
|
|
25
|
+
from aether.graphics.mesh import Mesh
|
|
26
|
+
from aether.graphics.mesh_factory import MeshFactory
|
|
27
|
+
from aether.graphics.material import Material
|
|
28
|
+
from aether.graphics.texture import Texture
|
|
29
|
+
from aether.graphics.camera import Camera
|
|
30
|
+
from aether.graphics.light import DirectionalLight, PointLight, SpotLight
|
|
31
|
+
from aether.graphics.renderer import Renderer
|
|
32
|
+
from aether.graphics.particle_system import ParticleEmitter, ParticleRenderer
|
|
33
|
+
|
|
34
|
+
# Физика
|
|
35
|
+
from aether.physics.physics_world import PhysicsWorld
|
|
36
|
+
from aether.physics.collider import BoxCollider, SphereCollider
|
|
37
|
+
from aether.physics.rigidbody import Rigidbody
|
|
38
|
+
|
|
39
|
+
# Ресурсы
|
|
40
|
+
from aether.resources.resource_manager import ResourceManager
|
|
41
|
+
from aether.resources.asset_database import AssetDatabase
|
|
42
|
+
from aether.resources.loaders.obj_loader import OBJLoader
|
|
43
|
+
from aether.resources.loaders.texture_loader import TextureLoader
|
|
44
|
+
|
|
45
|
+
# UI
|
|
46
|
+
from aether.ui.canvas import Canvas
|
|
47
|
+
from aether.ui.button import Button, Panel, Label
|
|
48
|
+
from aether.ui.text import Font, TextRenderer
|
|
49
|
+
|
|
50
|
+
# Аудио
|
|
51
|
+
from aether.audio.audio_manager import AudioManager, Sound, Music
|
|
52
|
+
|
|
53
|
+
# Утилиты
|
|
54
|
+
from aether.utils.profiler import Profiler, ScopedProfiler
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
"""Система деревьев поведения для AI"""
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import List, Dict, Optional, Callable, Any
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BTStatus(Enum):
|
|
8
|
+
"""Статус узла дерева поведения"""
|
|
9
|
+
SUCCESS = 1
|
|
10
|
+
FAILURE = 2
|
|
11
|
+
RUNNING = 3
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BTNode:
|
|
15
|
+
"""Базовый узел дерева поведения"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, name: str = ""):
|
|
18
|
+
self.name = name or self.__class__.__name__
|
|
19
|
+
self.parent: Optional['BTNode'] = None
|
|
20
|
+
self._status = BTStatus.FAILURE
|
|
21
|
+
|
|
22
|
+
def tick(self, context: Dict[str, Any]) -> BTStatus:
|
|
23
|
+
"""Выполняет узел"""
|
|
24
|
+
self._status = self._execute(context)
|
|
25
|
+
return self._status
|
|
26
|
+
|
|
27
|
+
def _execute(self, context: Dict[str, Any]) -> BTStatus:
|
|
28
|
+
"""Внутренняя логика (переопределяется)"""
|
|
29
|
+
return BTStatus.SUCCESS
|
|
30
|
+
|
|
31
|
+
def reset(self):
|
|
32
|
+
"""Сбрасывает состояние"""
|
|
33
|
+
self._status = BTStatus.FAILURE
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def status(self) -> BTStatus:
|
|
37
|
+
return self._status
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Sequence(BTNode):
|
|
41
|
+
"""
|
|
42
|
+
Последовательность: выполняет детей по порядку.
|
|
43
|
+
Останавливается при первой FAILURE или RUNNING.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, name: str = "Sequence"):
|
|
47
|
+
super().__init__(name)
|
|
48
|
+
self.children: List[BTNode] = []
|
|
49
|
+
self._current_index = 0
|
|
50
|
+
|
|
51
|
+
def add_child(self, node: BTNode) -> 'Sequence':
|
|
52
|
+
node.parent = self
|
|
53
|
+
self.children.append(node)
|
|
54
|
+
return self
|
|
55
|
+
|
|
56
|
+
def _execute(self, context: Dict[str, Any]) -> BTStatus:
|
|
57
|
+
while self._current_index < len(self.children):
|
|
58
|
+
child = self.children[self._current_index]
|
|
59
|
+
status = child.tick(context)
|
|
60
|
+
|
|
61
|
+
if status == BTStatus.FAILURE:
|
|
62
|
+
self._current_index = 0
|
|
63
|
+
return BTStatus.FAILURE
|
|
64
|
+
|
|
65
|
+
if status == BTStatus.RUNNING:
|
|
66
|
+
return BTStatus.RUNNING
|
|
67
|
+
|
|
68
|
+
self._current_index += 1
|
|
69
|
+
|
|
70
|
+
self._current_index = 0
|
|
71
|
+
return BTStatus.SUCCESS
|
|
72
|
+
|
|
73
|
+
def reset(self):
|
|
74
|
+
super().reset()
|
|
75
|
+
self._current_index = 0
|
|
76
|
+
for child in self.children:
|
|
77
|
+
child.reset()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class Selector(BTNode):
|
|
81
|
+
"""
|
|
82
|
+
Селектор: выполняет детей по порядку.
|
|
83
|
+
Останавливается при первой SUCCESS или RUNNING.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def __init__(self, name: str = "Selector"):
|
|
87
|
+
super().__init__(name)
|
|
88
|
+
self.children: List[BTNode] = []
|
|
89
|
+
self._current_index = 0
|
|
90
|
+
|
|
91
|
+
def add_child(self, node: BTNode) -> 'Selector':
|
|
92
|
+
node.parent = self
|
|
93
|
+
self.children.append(node)
|
|
94
|
+
return self
|
|
95
|
+
|
|
96
|
+
def _execute(self, context: Dict[str, Any]) -> BTStatus:
|
|
97
|
+
while self._current_index < len(self.children):
|
|
98
|
+
child = self.children[self._current_index]
|
|
99
|
+
status = child.tick(context)
|
|
100
|
+
|
|
101
|
+
if status == BTStatus.SUCCESS:
|
|
102
|
+
self._current_index = 0
|
|
103
|
+
return BTStatus.SUCCESS
|
|
104
|
+
|
|
105
|
+
if status == BTStatus.RUNNING:
|
|
106
|
+
return BTStatus.RUNNING
|
|
107
|
+
|
|
108
|
+
self._current_index += 1
|
|
109
|
+
|
|
110
|
+
self._current_index = 0
|
|
111
|
+
return BTStatus.FAILURE
|
|
112
|
+
|
|
113
|
+
def reset(self):
|
|
114
|
+
super().reset()
|
|
115
|
+
self._current_index = 0
|
|
116
|
+
for child in self.children:
|
|
117
|
+
child.reset()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class Parallel(BTNode):
|
|
121
|
+
"""
|
|
122
|
+
Параллельное выполнение всех детей.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
def __init__(self, name: str = "Parallel",
|
|
126
|
+
success_threshold: int = -1):
|
|
127
|
+
super().__init__(name)
|
|
128
|
+
self.children: List[BTNode] = []
|
|
129
|
+
self.success_threshold = success_threshold # -1 = все должны выполниться
|
|
130
|
+
|
|
131
|
+
def add_child(self, node: BTNode) -> 'Parallel':
|
|
132
|
+
node.parent = self
|
|
133
|
+
self.children.append(node)
|
|
134
|
+
return self
|
|
135
|
+
|
|
136
|
+
def _execute(self, context: Dict[str, Any]) -> BTStatus:
|
|
137
|
+
success_count = 0
|
|
138
|
+
failure_count = 0
|
|
139
|
+
|
|
140
|
+
for child in self.children:
|
|
141
|
+
status = child.tick(context)
|
|
142
|
+
|
|
143
|
+
if status == BTStatus.SUCCESS:
|
|
144
|
+
success_count += 1
|
|
145
|
+
elif status == BTStatus.FAILURE:
|
|
146
|
+
failure_count += 1
|
|
147
|
+
|
|
148
|
+
threshold = self.success_threshold
|
|
149
|
+
if threshold < 0:
|
|
150
|
+
threshold = len(self.children)
|
|
151
|
+
|
|
152
|
+
if success_count >= threshold:
|
|
153
|
+
return BTStatus.SUCCESS
|
|
154
|
+
elif failure_count > len(self.children) - threshold:
|
|
155
|
+
return BTStatus.FAILURE
|
|
156
|
+
|
|
157
|
+
return BTStatus.RUNNING
|
|
158
|
+
|
|
159
|
+
def reset(self):
|
|
160
|
+
super().reset()
|
|
161
|
+
for child in self.children:
|
|
162
|
+
child.reset()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class Condition(BTNode):
|
|
166
|
+
"""Условие (проверка)"""
|
|
167
|
+
|
|
168
|
+
def __init__(self, name: str, condition_fn: Callable):
|
|
169
|
+
super().__init__(name)
|
|
170
|
+
self.condition_fn = condition_fn
|
|
171
|
+
|
|
172
|
+
def _execute(self, context: Dict[str, Any]) -> BTStatus:
|
|
173
|
+
if self.condition_fn(context):
|
|
174
|
+
return BTStatus.SUCCESS
|
|
175
|
+
return BTStatus.FAILURE
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class Action(BTNode):
|
|
179
|
+
"""Действие"""
|
|
180
|
+
|
|
181
|
+
def __init__(self, name: str, action_fn: Callable):
|
|
182
|
+
super().__init__(name)
|
|
183
|
+
self.action_fn = action_fn
|
|
184
|
+
|
|
185
|
+
def _execute(self, context: Dict[str, Any]) -> BTStatus:
|
|
186
|
+
return self.action_fn(context)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class Inverter(BTNode):
|
|
190
|
+
"""Инвертирует результат дочернего узла"""
|
|
191
|
+
|
|
192
|
+
def __init__(self, name: str = "Inverter"):
|
|
193
|
+
super().__init__(name)
|
|
194
|
+
self.child: Optional[BTNode] = None
|
|
195
|
+
|
|
196
|
+
def set_child(self, node: BTNode) -> 'Inverter':
|
|
197
|
+
node.parent = self
|
|
198
|
+
self.child = node
|
|
199
|
+
return self
|
|
200
|
+
|
|
201
|
+
def _execute(self, context: Dict[str, Any]) -> BTStatus:
|
|
202
|
+
if not self.child:
|
|
203
|
+
return BTStatus.FAILURE
|
|
204
|
+
|
|
205
|
+
status = self.child.tick(context)
|
|
206
|
+
|
|
207
|
+
if status == BTStatus.SUCCESS:
|
|
208
|
+
return BTStatus.FAILURE
|
|
209
|
+
elif status == BTStatus.FAILURE:
|
|
210
|
+
return BTStatus.SUCCESS
|
|
211
|
+
|
|
212
|
+
return BTStatus.RUNNING
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class Repeater(BTNode):
|
|
216
|
+
"""Повторяет дочерний узел N раз или пока не FAILURE"""
|
|
217
|
+
|
|
218
|
+
def __init__(self, name: str = "Repeater",
|
|
219
|
+
count: int = -1,
|
|
220
|
+
stop_on_failure: bool = True):
|
|
221
|
+
super().__init__(name)
|
|
222
|
+
self.child: Optional[BTNode] = None
|
|
223
|
+
self.count = count # -1 = бесконечно
|
|
224
|
+
self.stop_on_failure = stop_on_failure
|
|
225
|
+
self._iteration = 0
|
|
226
|
+
|
|
227
|
+
def set_child(self, node: BTNode) -> 'Repeater':
|
|
228
|
+
node.parent = self
|
|
229
|
+
self.child = node
|
|
230
|
+
return self
|
|
231
|
+
|
|
232
|
+
def _execute(self, context: Dict[str, Any]) -> BTStatus:
|
|
233
|
+
if not self.child:
|
|
234
|
+
return BTStatus.FAILURE
|
|
235
|
+
|
|
236
|
+
status = self.child.tick(context)
|
|
237
|
+
|
|
238
|
+
if self.stop_on_failure and status == BTStatus.FAILURE:
|
|
239
|
+
return BTStatus.FAILURE
|
|
240
|
+
|
|
241
|
+
if status != BTStatus.RUNNING:
|
|
242
|
+
self._iteration += 1
|
|
243
|
+
|
|
244
|
+
if self.count > 0 and self._iteration >= self.count:
|
|
245
|
+
return BTStatus.SUCCESS
|
|
246
|
+
|
|
247
|
+
self.child.reset()
|
|
248
|
+
|
|
249
|
+
return BTStatus.RUNNING
|
|
250
|
+
|
|
251
|
+
def reset(self):
|
|
252
|
+
super().reset()
|
|
253
|
+
self._iteration = 0
|
|
254
|
+
if self.child:
|
|
255
|
+
self.child.reset()
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class Wait(BTNode):
|
|
259
|
+
"""Ожидание заданное время"""
|
|
260
|
+
|
|
261
|
+
def __init__(self, name: str = "Wait", duration: float = 1.0):
|
|
262
|
+
super().__init__(name)
|
|
263
|
+
self.duration = duration
|
|
264
|
+
self._start_time = 0.0
|
|
265
|
+
self._started = False
|
|
266
|
+
|
|
267
|
+
def _execute(self, context: Dict[str, Any]) -> BTStatus:
|
|
268
|
+
if not self._started:
|
|
269
|
+
self._start_time = time.time()
|
|
270
|
+
self._started = True
|
|
271
|
+
|
|
272
|
+
elapsed = time.time() - self._start_time
|
|
273
|
+
|
|
274
|
+
if elapsed >= self.duration:
|
|
275
|
+
self._started = False
|
|
276
|
+
return BTStatus.SUCCESS
|
|
277
|
+
|
|
278
|
+
return BTStatus.RUNNING
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class BehaviorTree:
|
|
282
|
+
"""Дерево поведения"""
|
|
283
|
+
|
|
284
|
+
def __init__(self, root: BTNode = None):
|
|
285
|
+
self.root = root
|
|
286
|
+
self._context: Dict[str, Any] = {}
|
|
287
|
+
self._running = True
|
|
288
|
+
|
|
289
|
+
def set_root(self, node: BTNode):
|
|
290
|
+
self.root = node
|
|
291
|
+
|
|
292
|
+
def set_context(self, key: str, value: Any):
|
|
293
|
+
self._context[key] = value
|
|
294
|
+
|
|
295
|
+
def get_context(self, key: str, default: Any = None) -> Any:
|
|
296
|
+
return self._context.get(key, default)
|
|
297
|
+
|
|
298
|
+
def tick(self) -> BTStatus:
|
|
299
|
+
"""Один такт дерева поведения"""
|
|
300
|
+
if not self.root or not self._running:
|
|
301
|
+
return BTStatus.FAILURE
|
|
302
|
+
|
|
303
|
+
return self.root.tick(self._context)
|
|
304
|
+
|
|
305
|
+
def reset(self):
|
|
306
|
+
"""Сбрасывает дерево"""
|
|
307
|
+
if self.root:
|
|
308
|
+
self.root.reset()
|
|
309
|
+
|
|
310
|
+
def pause(self):
|
|
311
|
+
self._running = False
|
|
312
|
+
|
|
313
|
+
def resume(self):
|
|
314
|
+
self._running = True
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
# ---- ПРИМЕРЫ ПОВЕДЕНИЯ ----
|
|
318
|
+
|
|
319
|
+
class IsEnemyNearby(Condition):
|
|
320
|
+
"""Проверка: есть ли враг рядом"""
|
|
321
|
+
def __init__(self, radius: float = 5.0):
|
|
322
|
+
super().__init__("IsEnemyNearby", self._check)
|
|
323
|
+
self.radius = radius
|
|
324
|
+
|
|
325
|
+
def _check(self, context: Dict[str, Any]) -> bool:
|
|
326
|
+
entity = context.get('entity')
|
|
327
|
+
if not entity:
|
|
328
|
+
return False
|
|
329
|
+
|
|
330
|
+
# Здесь должна быть реальная проверка
|
|
331
|
+
enemies = context.get('enemies', [])
|
|
332
|
+
position = context.get('position')
|
|
333
|
+
|
|
334
|
+
if position is None:
|
|
335
|
+
transform = entity.get_component("Transform")
|
|
336
|
+
if transform:
|
|
337
|
+
position = transform.position
|
|
338
|
+
|
|
339
|
+
for enemy in enemies:
|
|
340
|
+
enemy_pos = enemy.get_component("Transform").position
|
|
341
|
+
if np.linalg.norm(position - enemy_pos) < self.radius:
|
|
342
|
+
return True
|
|
343
|
+
|
|
344
|
+
return False
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class MoveToTarget(Action):
|
|
348
|
+
"""Движение к цели"""
|
|
349
|
+
def __init__(self, speed: float = 3.0):
|
|
350
|
+
super().__init__("MoveToTarget", self._move)
|
|
351
|
+
self.speed = speed
|
|
352
|
+
|
|
353
|
+
def _move(self, context: Dict[str, Any]) -> BTStatus:
|
|
354
|
+
entity = context.get('entity')
|
|
355
|
+
target = context.get('target')
|
|
356
|
+
|
|
357
|
+
if not entity or not target:
|
|
358
|
+
return BTStatus.FAILURE
|
|
359
|
+
|
|
360
|
+
transform = entity.get_component("Transform")
|
|
361
|
+
target_pos = target.get_component("Transform").position
|
|
362
|
+
|
|
363
|
+
if transform is None:
|
|
364
|
+
return BTStatus.FAILURE
|
|
365
|
+
|
|
366
|
+
direction = target_pos - transform.position
|
|
367
|
+
distance = np.linalg.norm(direction)
|
|
368
|
+
|
|
369
|
+
if distance < 0.5:
|
|
370
|
+
return BTStatus.SUCCESS
|
|
371
|
+
|
|
372
|
+
direction = direction / distance
|
|
373
|
+
transform.position += direction * self.speed * 0.016
|
|
374
|
+
transform._matrix_dirty = True
|
|
375
|
+
|
|
376
|
+
return BTStatus.RUNNING
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
class AttackTarget(Action):
|
|
380
|
+
"""Атака цели"""
|
|
381
|
+
def __init__(self, damage: float = 10.0):
|
|
382
|
+
super().__init__("AttackTarget", self._attack)
|
|
383
|
+
self.damage = damage
|
|
384
|
+
self._cooldown = 1.0
|
|
385
|
+
self._last_attack = 0.0
|
|
386
|
+
|
|
387
|
+
def _attack(self, context: Dict[str, Any]) -> BTStatus:
|
|
388
|
+
target = context.get('target')
|
|
389
|
+
if not target:
|
|
390
|
+
return BTStatus.FAILURE
|
|
391
|
+
|
|
392
|
+
current_time = time.time()
|
|
393
|
+
if current_time - self._last_attack < self._cooldown:
|
|
394
|
+
return BTStatus.RUNNING
|
|
395
|
+
|
|
396
|
+
# Наносим урон
|
|
397
|
+
health = target.get_component("Health")
|
|
398
|
+
if health:
|
|
399
|
+
health.take_damage(self.damage)
|
|
400
|
+
|
|
401
|
+
self._last_attack = current_time
|
|
402
|
+
|
|
403
|
+
return BTStatus.SUCCESS
|