aether-engine 1.1.0__tar.gz → 1.2.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.2.0/PKG-INFO +122 -0
- aether_engine-1.2.0/README.md +74 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/__init__.py +11 -2
- aether_engine-1.2.0/aether/audio/sfx.py +85 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/core/engine.py +15 -2
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/graphics2d/__init__.py +4 -3
- aether_engine-1.2.0/aether/physics/raycast.py +102 -0
- aether_engine-1.2.0/aether/resources/save_load.py +20 -0
- aether_engine-1.2.0/aether/ui/__init__.py +5 -0
- aether_engine-1.2.0/aether/ui/hud.py +65 -0
- aether_engine-1.2.0/aether_engine.egg-info/PKG-INFO +122 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether_engine.egg-info/SOURCES.txt +3 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/setup.py +2 -2
- aether_engine-1.1.0/PKG-INFO +0 -155
- aether_engine-1.1.0/README.md +0 -107
- aether_engine-1.1.0/aether/physics/raycast.py +0 -25
- aether_engine-1.1.0/aether/utils/__init__.py +0 -0
- aether_engine-1.1.0/aether_engine.egg-info/PKG-INFO +0 -155
- {aether_engine-1.1.0 → aether_engine-1.2.0}/LICENSE +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/MANIFEST.in +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/ai/behavior_tree.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/ai/navmesh.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/animation/skeleton.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/assets/asset_pipeline.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/audio/__init__.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/audio/audio_manager.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/audio/music.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/audio/sound.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/core/__init__.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/core/achievement_system.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/core/application.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/core/config.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/core/console.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/core/event_bus.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/core/input.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/core/plugin_system.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/core/save_system.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/core/time_manager.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/core/window.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/editor/__init__.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/editor/__main__.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/editor/gizmo.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/editor/imgui_integration.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/editor/model_editor.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/environment/environment_system.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/graphics/__init__.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/graphics/camera.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/graphics/first_person.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/graphics/framebuffer.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/graphics/light.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/graphics/lod_system.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/graphics/material.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/graphics/mesh.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/graphics/mesh_factory.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/graphics/particle_system.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/graphics/post_process.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/graphics/renderer.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/graphics/shader.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/graphics/shader_library.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/graphics/sky_atmosphere.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/graphics/terrain.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/graphics/texture.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/graphics/water.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/network/network_manager.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/physics/__init__.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/physics/bullet_backend.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/physics/collider.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/physics/physics2d.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/physics/physics3d.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/physics/physics_world.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/physics/rigidbody.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/platform/steam_api.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/resources/__init__.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/resources/asset_database.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/resources/loaders/model_loader.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/resources/loaders/obj_loader.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/resources/loaders/texture_loader.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/resources/resource_manager.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/resources/serialization.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/scene/__init__.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/scene/component.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/scene/entity.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/scene/scene.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/scene/scene_graph.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/scene/scene_manager.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/ui/button.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/ui/canvas.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/ui/text.py +0 -0
- {aether_engine-1.1.0/aether/ui → aether_engine-1.2.0/aether/utils}/__init__.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/utils/logger.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/utils/math_utils.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/utils/object_pool.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether/utils/profiler.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether_engine.egg-info/dependency_links.txt +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether_engine.egg-info/entry_points.txt +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether_engine.egg-info/requires.txt +3 -3
- {aether_engine-1.1.0 → aether_engine-1.2.0}/aether_engine.egg-info/top_level.txt +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/examples/01_hello_cube.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/examples/02_sphere_scene.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/examples/03_first_person.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/examples/04_physics2d.py +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/pyproject.toml +0 -0
- {aether_engine-1.1.0 → aether_engine-1.2.0}/setup.cfg +0 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aether-engine
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: 3D/2D game engine by entiti937 — OpenGL, physics, FPS, model editor, HUD
|
|
5
|
+
Home-page: https://pypi.org/project/aether-engine/
|
|
6
|
+
Author: entiti937
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Topic :: Games/Entertainment
|
|
17
|
+
Classifier: Topic :: Multimedia :: Graphics :: 3D Rendering
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: numpy>=1.24
|
|
22
|
+
Requires-Dist: glfw>=2.6
|
|
23
|
+
Requires-Dist: PyOpenGL>=3.1.7
|
|
24
|
+
Requires-Dist: Pillow>=10.0
|
|
25
|
+
Provides-Extra: audio
|
|
26
|
+
Requires-Dist: pygame-ce>=2.5; extra == "audio"
|
|
27
|
+
Provides-Extra: physics
|
|
28
|
+
Requires-Dist: pymunk>=6.0; extra == "physics"
|
|
29
|
+
Provides-Extra: models
|
|
30
|
+
Requires-Dist: trimesh>=3.21; extra == "models"
|
|
31
|
+
Provides-Extra: full
|
|
32
|
+
Requires-Dist: pyyaml>=6.0; extra == "full"
|
|
33
|
+
Requires-Dist: pygame-ce>=2.5; extra == "full"
|
|
34
|
+
Requires-Dist: trimesh>=3.21; extra == "full"
|
|
35
|
+
Requires-Dist: pymunk>=6.0; extra == "full"
|
|
36
|
+
Requires-Dist: freetype-py>=2.4; extra == "full"
|
|
37
|
+
Requires-Dist: imageio>=2.31; extra == "full"
|
|
38
|
+
Dynamic: author
|
|
39
|
+
Dynamic: classifier
|
|
40
|
+
Dynamic: description
|
|
41
|
+
Dynamic: description-content-type
|
|
42
|
+
Dynamic: home-page
|
|
43
|
+
Dynamic: license-file
|
|
44
|
+
Dynamic: provides-extra
|
|
45
|
+
Dynamic: requires-dist
|
|
46
|
+
Dynamic: requires-python
|
|
47
|
+
Dynamic: summary
|
|
48
|
+
|
|
49
|
+
# Aether Engine
|
|
50
|
+
|
|
51
|
+
**3D / 2D игровой движок на Python + OpenGL**
|
|
52
|
+
**Разработчик:** entiti937 · **Версия:** 1.2.0 · **Лицензия:** MIT
|
|
53
|
+
**PyPI:** https://pypi.org/project/aether-engine/
|
|
54
|
+
|
|
55
|
+
> Полное руководство: [`docs/GUIDE.md`](docs/GUIDE.md)
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Установка
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install -U "aether-engine[full]"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Showcase-игра (доказательство)
|
|
66
|
+
|
|
67
|
+
Minecraft-like sandbox **на этом же движке**:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
python games/aether_craft.py
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Там есть: воксельный мир, вид от 1 лица, ломать/ставить блоки, HUD, звуки, день/ночь, сейвы (F5/F9), урон и респавн.
|
|
74
|
+
|
|
75
|
+
## Другие запуски
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
python -m aether.editor
|
|
79
|
+
python games/orb_rush.py
|
|
80
|
+
python examples/03_first_person.py
|
|
81
|
+
python examples/04_physics2d.py
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Минимальный код
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from aether import Engine, Scene, Entity, MeshFactory, Material, Camera
|
|
88
|
+
from aether.scene.component import Transform, MeshRenderer
|
|
89
|
+
|
|
90
|
+
engine = Engine(1280, 720, "Game by you — engine by entiti937")
|
|
91
|
+
scene = Scene("Main")
|
|
92
|
+
camera = Camera(position=(0, 2, 6), yaw=-90)
|
|
93
|
+
scene.set_main_camera(camera)
|
|
94
|
+
|
|
95
|
+
cube = Entity("Cube")
|
|
96
|
+
cube.add_component(Transform(position=(0, 1, 0)))
|
|
97
|
+
mat = Material(); mat.albedo[:] = (0.2, 0.85, 1.0)
|
|
98
|
+
cube.add_component(MeshRenderer(mesh=MeshFactory.create_cube(), material=mat))
|
|
99
|
+
scene.spawn(cube)
|
|
100
|
+
|
|
101
|
+
engine.load_scene(scene)
|
|
102
|
+
engine.enable_first_person(camera)
|
|
103
|
+
engine.run()
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Возможности 1.2
|
|
107
|
+
|
|
108
|
+
- PBR 3D + sprites 2D + HUD overlay
|
|
109
|
+
- FPS (`enable_first_person`) и free-look камера
|
|
110
|
+
- Физика 3D/2D + raycast
|
|
111
|
+
- `ModelLoader` (OBJ/GLB/glTF/STL/FBX/…)
|
|
112
|
+
- Встроенный voxel model editor
|
|
113
|
+
- Procedural SFX, JSON save/load
|
|
114
|
+
- Примеры и две игры в репозитории
|
|
115
|
+
|
|
116
|
+
## Документация
|
|
117
|
+
|
|
118
|
+
Все команды, API и туториалы: **[docs/GUIDE.md](docs/GUIDE.md)**
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
Aether Engine © **entiti937**
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Aether Engine
|
|
2
|
+
|
|
3
|
+
**3D / 2D игровой движок на Python + OpenGL**
|
|
4
|
+
**Разработчик:** entiti937 · **Версия:** 1.2.0 · **Лицензия:** MIT
|
|
5
|
+
**PyPI:** https://pypi.org/project/aether-engine/
|
|
6
|
+
|
|
7
|
+
> Полное руководство: [`docs/GUIDE.md`](docs/GUIDE.md)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Установка
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install -U "aether-engine[full]"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Showcase-игра (доказательство)
|
|
18
|
+
|
|
19
|
+
Minecraft-like sandbox **на этом же движке**:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
python games/aether_craft.py
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Там есть: воксельный мир, вид от 1 лица, ломать/ставить блоки, HUD, звуки, день/ночь, сейвы (F5/F9), урон и респавн.
|
|
26
|
+
|
|
27
|
+
## Другие запуски
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
python -m aether.editor
|
|
31
|
+
python games/orb_rush.py
|
|
32
|
+
python examples/03_first_person.py
|
|
33
|
+
python examples/04_physics2d.py
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Минимальный код
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from aether import Engine, Scene, Entity, MeshFactory, Material, Camera
|
|
40
|
+
from aether.scene.component import Transform, MeshRenderer
|
|
41
|
+
|
|
42
|
+
engine = Engine(1280, 720, "Game by you — engine by entiti937")
|
|
43
|
+
scene = Scene("Main")
|
|
44
|
+
camera = Camera(position=(0, 2, 6), yaw=-90)
|
|
45
|
+
scene.set_main_camera(camera)
|
|
46
|
+
|
|
47
|
+
cube = Entity("Cube")
|
|
48
|
+
cube.add_component(Transform(position=(0, 1, 0)))
|
|
49
|
+
mat = Material(); mat.albedo[:] = (0.2, 0.85, 1.0)
|
|
50
|
+
cube.add_component(MeshRenderer(mesh=MeshFactory.create_cube(), material=mat))
|
|
51
|
+
scene.spawn(cube)
|
|
52
|
+
|
|
53
|
+
engine.load_scene(scene)
|
|
54
|
+
engine.enable_first_person(camera)
|
|
55
|
+
engine.run()
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Возможности 1.2
|
|
59
|
+
|
|
60
|
+
- PBR 3D + sprites 2D + HUD overlay
|
|
61
|
+
- FPS (`enable_first_person`) и free-look камера
|
|
62
|
+
- Физика 3D/2D + raycast
|
|
63
|
+
- `ModelLoader` (OBJ/GLB/glTF/STL/FBX/…)
|
|
64
|
+
- Встроенный voxel model editor
|
|
65
|
+
- Procedural SFX, JSON save/load
|
|
66
|
+
- Примеры и две игры в репозитории
|
|
67
|
+
|
|
68
|
+
## Документация
|
|
69
|
+
|
|
70
|
+
Все команды, API и туториалы: **[docs/GUIDE.md](docs/GUIDE.md)**
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
Aether Engine © **entiti937**
|
|
@@ -18,9 +18,12 @@ from aether.graphics.first_person import FirstPersonController
|
|
|
18
18
|
from aether.resources.loaders.model_loader import ModelLoader
|
|
19
19
|
from aether.physics import PhysicsWorld2D, PhysicsWorld3D, BoxCollider, SphereCollider
|
|
20
20
|
from aether.graphics2d import Renderer2D, Sprite, Camera2D
|
|
21
|
-
from aether.
|
|
21
|
+
from aether.ui.hud import Hud, HudBar
|
|
22
|
+
from aether.audio.sfx import SfxBank
|
|
23
|
+
from aether.resources.save_load import save_json, load_json
|
|
24
|
+
from aether.physics.raycast import Raycaster
|
|
22
25
|
|
|
23
|
-
__version__ = "1.
|
|
26
|
+
__version__ = "1.2.0"
|
|
24
27
|
|
|
25
28
|
__all__ = [
|
|
26
29
|
"Engine",
|
|
@@ -45,5 +48,11 @@ __all__ = [
|
|
|
45
48
|
"Sprite",
|
|
46
49
|
"Camera2D",
|
|
47
50
|
"ModelEditor",
|
|
51
|
+
"Hud",
|
|
52
|
+
"HudBar",
|
|
53
|
+
"SfxBank",
|
|
54
|
+
"save_json",
|
|
55
|
+
"load_json",
|
|
56
|
+
"Raycaster",
|
|
48
57
|
"__version__",
|
|
49
58
|
]
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Procedural sound helpers (no asset files required)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import math
|
|
5
|
+
import struct
|
|
6
|
+
import tempfile
|
|
7
|
+
import wave
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _write_wav(path: Path, samples, rate: int = 22050):
|
|
13
|
+
with wave.open(str(path), "w") as wf:
|
|
14
|
+
wf.setnchannels(1)
|
|
15
|
+
wf.setsampwidth(2)
|
|
16
|
+
wf.setframerate(rate)
|
|
17
|
+
frames = b"".join(struct.pack("<h", int(max(-1, min(1, s)) * 32767)) for s in samples)
|
|
18
|
+
wf.writeframes(frames)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def make_tone(
|
|
22
|
+
frequency: float = 440.0,
|
|
23
|
+
duration: float = 0.08,
|
|
24
|
+
volume: float = 0.35,
|
|
25
|
+
kind: str = "square",
|
|
26
|
+
rate: int = 22050,
|
|
27
|
+
) -> Path:
|
|
28
|
+
"""Generate a short WAV tone and return its path."""
|
|
29
|
+
n = int(rate * duration)
|
|
30
|
+
samples = []
|
|
31
|
+
for i in range(n):
|
|
32
|
+
t = i / rate
|
|
33
|
+
env = 1.0
|
|
34
|
+
if i < rate * 0.01:
|
|
35
|
+
env = i / (rate * 0.01)
|
|
36
|
+
elif i > n - rate * 0.02:
|
|
37
|
+
env = max(0.0, (n - i) / (rate * 0.02))
|
|
38
|
+
phase = 2 * math.pi * frequency * t
|
|
39
|
+
if kind == "sine":
|
|
40
|
+
val = math.sin(phase)
|
|
41
|
+
elif kind == "noise":
|
|
42
|
+
# deterministic pseudo-noise
|
|
43
|
+
val = math.sin(phase * 12.9898) * 2.0 % 2.0 - 1.0
|
|
44
|
+
else:
|
|
45
|
+
val = 1.0 if math.sin(phase) >= 0 else -1.0
|
|
46
|
+
samples.append(val * volume * env)
|
|
47
|
+
|
|
48
|
+
path = Path(tempfile.gettempdir()) / f"aether_tone_{int(frequency)}_{kind}.wav"
|
|
49
|
+
_write_wav(path, samples, rate)
|
|
50
|
+
return path
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class SfxBank:
|
|
54
|
+
"""Lazy bank of procedural SFX played through Engine.audio."""
|
|
55
|
+
|
|
56
|
+
def __init__(self, audio_manager):
|
|
57
|
+
self.audio = audio_manager
|
|
58
|
+
self._cache = {}
|
|
59
|
+
|
|
60
|
+
def _get(self, key: str, factory):
|
|
61
|
+
if key not in self._cache:
|
|
62
|
+
path = factory()
|
|
63
|
+
# AudioManager.load_sound(name, path)
|
|
64
|
+
self._cache[key] = self.audio.load_sound(key, str(path))
|
|
65
|
+
return self._cache[key]
|
|
66
|
+
|
|
67
|
+
def play_break(self):
|
|
68
|
+
snd = self._get("break", lambda: make_tone(180, 0.07, 0.4, "noise"))
|
|
69
|
+
if snd:
|
|
70
|
+
self.audio.play_sound("break", volume=0.45)
|
|
71
|
+
|
|
72
|
+
def play_place(self):
|
|
73
|
+
snd = self._get("place", lambda: make_tone(520, 0.06, 0.3, "square"))
|
|
74
|
+
if snd:
|
|
75
|
+
self.audio.play_sound("place", volume=0.4)
|
|
76
|
+
|
|
77
|
+
def play_select(self):
|
|
78
|
+
snd = self._get("select", lambda: make_tone(760, 0.04, 0.25, "sine"))
|
|
79
|
+
if snd:
|
|
80
|
+
self.audio.play_sound("select", volume=0.35)
|
|
81
|
+
|
|
82
|
+
def play_hurt(self):
|
|
83
|
+
snd = self._get("hurt", lambda: make_tone(120, 0.12, 0.45, "square"))
|
|
84
|
+
if snd:
|
|
85
|
+
self.audio.play_sound("hurt", volume=0.5)
|
|
@@ -54,7 +54,7 @@ class Engine:
|
|
|
54
54
|
self.config.graphics.width = width
|
|
55
55
|
self.config.graphics.height = height
|
|
56
56
|
|
|
57
|
-
print("[Aether] Initializing Engine v1.
|
|
57
|
+
print("[Aether] Initializing Engine v1.2.0 by entiti937...")
|
|
58
58
|
|
|
59
59
|
self.window = Window(
|
|
60
60
|
self.config.graphics.width,
|
|
@@ -101,6 +101,7 @@ class Engine:
|
|
|
101
101
|
self.free_camera = True
|
|
102
102
|
self.fps_controller: Optional[FirstPersonController] = None
|
|
103
103
|
self._update_callbacks = []
|
|
104
|
+
self.draw_hud = False # overlay Renderer2D sprites on top of 3D
|
|
104
105
|
|
|
105
106
|
self.event_bus.subscribe("window_resize", self._on_window_resize)
|
|
106
107
|
self.event_bus.subscribe("engine_pause", self._on_pause)
|
|
@@ -327,7 +328,7 @@ class Engine:
|
|
|
327
328
|
|
|
328
329
|
def _render(self):
|
|
329
330
|
if self.mode == "2d":
|
|
330
|
-
self.renderer2d.begin_frame()
|
|
331
|
+
self.renderer2d.begin_frame(clear=True)
|
|
331
332
|
self.renderer2d.render()
|
|
332
333
|
self.renderer2d.end_frame()
|
|
333
334
|
return
|
|
@@ -337,6 +338,18 @@ class Engine:
|
|
|
337
338
|
self.renderer.render_scene(self.scene)
|
|
338
339
|
self.renderer.end_frame()
|
|
339
340
|
|
|
341
|
+
if self.draw_hud:
|
|
342
|
+
# Overlay HUD without clearing the 3D framebuffer color
|
|
343
|
+
self.renderer2d.begin_frame(clear=False)
|
|
344
|
+
# Map HUD from pixel-ish coords: camera at 0, zoom so 1 unit ~= 1 px-ish
|
|
345
|
+
self.renderer2d.camera.x = 0
|
|
346
|
+
self.renderer2d.camera.y = 0
|
|
347
|
+
self.renderer2d.camera.zoom = 1.0
|
|
348
|
+
self.renderer2d.camera.width = float(self.config.graphics.width)
|
|
349
|
+
self.renderer2d.camera.height = float(self.config.graphics.height)
|
|
350
|
+
self.renderer2d.render()
|
|
351
|
+
self.renderer2d.end_frame()
|
|
352
|
+
|
|
340
353
|
def on_update(self, callback):
|
|
341
354
|
"""Регистрирует callback(dt), вызываемый каждый кадр."""
|
|
342
355
|
self._update_callbacks.append(callback)
|
|
@@ -100,12 +100,13 @@ class Renderer2D:
|
|
|
100
100
|
self.sprites.append(sprite)
|
|
101
101
|
return sprite
|
|
102
102
|
|
|
103
|
-
def begin_frame(self):
|
|
103
|
+
def begin_frame(self, clear: bool = True):
|
|
104
104
|
glDisable(GL_DEPTH_TEST)
|
|
105
105
|
glEnable(GL_BLEND)
|
|
106
106
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
if clear:
|
|
108
|
+
glClearColor(*self.clear_color)
|
|
109
|
+
glClear(GL_COLOR_BUFFER_BIT)
|
|
109
110
|
|
|
110
111
|
def render(self):
|
|
111
112
|
self._shader.use()
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Raycasting against AABB / sphere colliders and voxel helpers."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RaycastHit:
|
|
10
|
+
def __init__(self, point, normal, distance, entity=None, body=None):
|
|
11
|
+
self.point = np.asarray(point, dtype=np.float32)
|
|
12
|
+
self.normal = np.asarray(normal, dtype=np.float32)
|
|
13
|
+
self.distance = float(distance)
|
|
14
|
+
self.entity = entity
|
|
15
|
+
self.body = body
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def ray_aabb(origin, direction, bmin, bmax, max_distance=1000.0) -> Optional[float]:
|
|
19
|
+
origin = np.asarray(origin, dtype=np.float32)
|
|
20
|
+
direction = np.asarray(direction, dtype=np.float32)
|
|
21
|
+
length = np.linalg.norm(direction)
|
|
22
|
+
if length < 1e-8:
|
|
23
|
+
return None
|
|
24
|
+
direction = direction / length
|
|
25
|
+
tmin, tmax = 0.0, float(max_distance)
|
|
26
|
+
for i in range(3):
|
|
27
|
+
if abs(direction[i]) < 1e-8:
|
|
28
|
+
if origin[i] < bmin[i] or origin[i] > bmax[i]:
|
|
29
|
+
return None
|
|
30
|
+
continue
|
|
31
|
+
inv = 1.0 / direction[i]
|
|
32
|
+
t1 = (bmin[i] - origin[i]) * inv
|
|
33
|
+
t2 = (bmax[i] - origin[i]) * inv
|
|
34
|
+
if t1 > t2:
|
|
35
|
+
t1, t2 = t2, t1
|
|
36
|
+
tmin = max(tmin, t1)
|
|
37
|
+
tmax = min(tmax, t2)
|
|
38
|
+
if tmin > tmax:
|
|
39
|
+
return None
|
|
40
|
+
return tmin
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def ray_sphere(origin, direction, center, radius, max_distance=1000.0) -> Optional[float]:
|
|
44
|
+
origin = np.asarray(origin, dtype=np.float32)
|
|
45
|
+
direction = np.asarray(direction, dtype=np.float32)
|
|
46
|
+
center = np.asarray(center, dtype=np.float32)
|
|
47
|
+
length = np.linalg.norm(direction)
|
|
48
|
+
if length < 1e-8:
|
|
49
|
+
return None
|
|
50
|
+
direction = direction / length
|
|
51
|
+
oc = origin - center
|
|
52
|
+
b = float(np.dot(oc, direction))
|
|
53
|
+
c = float(np.dot(oc, oc) - radius * radius)
|
|
54
|
+
disc = b * b - c
|
|
55
|
+
if disc < 0:
|
|
56
|
+
return None
|
|
57
|
+
s = np.sqrt(disc)
|
|
58
|
+
t0 = -b - s
|
|
59
|
+
t1 = -b + s
|
|
60
|
+
for t in (t0, t1):
|
|
61
|
+
if 0.0 <= t <= max_distance:
|
|
62
|
+
return float(t)
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Raycaster:
|
|
67
|
+
"""High-level raycast helper for PhysicsWorld3D bodies / custom AABBs."""
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def cast(world, origin, direction, max_distance=100.0) -> Optional[RaycastHit]:
|
|
71
|
+
if hasattr(world, "raycast"):
|
|
72
|
+
hit = world.raycast(origin, direction, max_distance)
|
|
73
|
+
if not hit:
|
|
74
|
+
return None
|
|
75
|
+
return RaycastHit(
|
|
76
|
+
point=hit["point"],
|
|
77
|
+
normal=(0, 1, 0),
|
|
78
|
+
distance=hit["distance"],
|
|
79
|
+
entity=hit.get("entity"),
|
|
80
|
+
body=hit.get("body"),
|
|
81
|
+
)
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def cast_aabb_list(origin, direction, aabbs, max_distance=100.0) -> Optional[RaycastHit]:
|
|
86
|
+
"""aabbs: iterable of (bmin, bmax, payload)."""
|
|
87
|
+
best_t = None
|
|
88
|
+
best_payload = None
|
|
89
|
+
for bmin, bmax, payload in aabbs:
|
|
90
|
+
t = ray_aabb(origin, direction, bmin, bmax, max_distance)
|
|
91
|
+
if t is None:
|
|
92
|
+
continue
|
|
93
|
+
if best_t is None or t < best_t:
|
|
94
|
+
best_t = t
|
|
95
|
+
best_payload = payload
|
|
96
|
+
if best_t is None:
|
|
97
|
+
return None
|
|
98
|
+
origin = np.asarray(origin, dtype=np.float32)
|
|
99
|
+
direction = np.asarray(direction, dtype=np.float32)
|
|
100
|
+
direction = direction / max(np.linalg.norm(direction), 1e-6)
|
|
101
|
+
point = origin + direction * best_t
|
|
102
|
+
return RaycastHit(point=point, normal=(0, 1, 0), distance=best_t, entity=best_payload)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Simple JSON world / scene helpers."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def save_json(path, data: Dict[str, Any]):
|
|
10
|
+
path = Path(path)
|
|
11
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
12
|
+
path.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
13
|
+
print(f"[Save] Wrote {path}")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def load_json(path) -> Dict[str, Any]:
|
|
17
|
+
path = Path(path)
|
|
18
|
+
if not path.exists():
|
|
19
|
+
raise FileNotFoundError(path)
|
|
20
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Minimal immediate-mode HUD overlay (screen-space quads via Renderer2D)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import List, Tuple
|
|
6
|
+
|
|
7
|
+
from aether.graphics2d import Sprite
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class HudBar:
|
|
12
|
+
label: str
|
|
13
|
+
value: float # 0..1
|
|
14
|
+
color: Tuple[float, float, float, float] = (0.2, 0.85, 0.35, 0.95)
|
|
15
|
+
back: Tuple[float, float, float, float] = (0.1, 0.1, 0.12, 0.7)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Hud:
|
|
19
|
+
"""Build sprites for a simple game HUD (hotbar, bars, crosshair)."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, renderer2d):
|
|
22
|
+
self.r = renderer2d
|
|
23
|
+
self.sprites: List[Sprite] = []
|
|
24
|
+
self._owned: List[Sprite] = []
|
|
25
|
+
|
|
26
|
+
def clear(self):
|
|
27
|
+
for s in self._owned:
|
|
28
|
+
if s in self.r.sprites:
|
|
29
|
+
self.r.sprites.remove(s)
|
|
30
|
+
self._owned.clear()
|
|
31
|
+
|
|
32
|
+
def _add(self, sprite: Sprite) -> Sprite:
|
|
33
|
+
self.r.add_sprite(sprite)
|
|
34
|
+
self._owned.append(sprite)
|
|
35
|
+
return sprite
|
|
36
|
+
|
|
37
|
+
def draw_crosshair(self, size: float = 10.0, thickness: float = 2.0):
|
|
38
|
+
c = (1, 1, 1, 0.9)
|
|
39
|
+
self._add(Sprite(0, 0, size, thickness, color=c, layer=100))
|
|
40
|
+
self._add(Sprite(0, 0, thickness, size, color=c, layer=100))
|
|
41
|
+
|
|
42
|
+
def draw_hotbar(self, slots: int, selected: int, colors, y: float = -220):
|
|
43
|
+
slot_w, slot_h, gap = 44, 44, 6
|
|
44
|
+
total = slots * slot_w + (slots - 1) * gap
|
|
45
|
+
start_x = -total * 0.5 + slot_w * 0.5
|
|
46
|
+
for i in range(slots):
|
|
47
|
+
x = start_x + i * (slot_w + gap)
|
|
48
|
+
border = (1, 1, 0.3, 1) if i == selected else (0.15, 0.15, 0.18, 0.85)
|
|
49
|
+
self._add(Sprite(x, y, slot_w + 4, slot_h + 4, color=border, layer=90))
|
|
50
|
+
col = colors[i] if i < len(colors) else (0.4, 0.4, 0.4, 1)
|
|
51
|
+
self._add(Sprite(x, y, slot_w - 6, slot_h - 6, color=(*col[:3], 1.0), layer=91))
|
|
52
|
+
|
|
53
|
+
def draw_bar(self, x: float, y: float, width: float, height: float, bar: HudBar):
|
|
54
|
+
self._add(Sprite(x, y, width, height, color=bar.back, layer=80))
|
|
55
|
+
fill = max(0.0, min(1.0, bar.value)) * (width - 4)
|
|
56
|
+
self._add(
|
|
57
|
+
Sprite(
|
|
58
|
+
x - (width - fill) * 0.5 + 2,
|
|
59
|
+
y,
|
|
60
|
+
max(1.0, fill),
|
|
61
|
+
height - 4,
|
|
62
|
+
color=bar.color,
|
|
63
|
+
layer=81,
|
|
64
|
+
)
|
|
65
|
+
)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aether-engine
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: 3D/2D game engine by entiti937 — OpenGL, physics, FPS, model editor, HUD
|
|
5
|
+
Home-page: https://pypi.org/project/aether-engine/
|
|
6
|
+
Author: entiti937
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Topic :: Games/Entertainment
|
|
17
|
+
Classifier: Topic :: Multimedia :: Graphics :: 3D Rendering
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: numpy>=1.24
|
|
22
|
+
Requires-Dist: glfw>=2.6
|
|
23
|
+
Requires-Dist: PyOpenGL>=3.1.7
|
|
24
|
+
Requires-Dist: Pillow>=10.0
|
|
25
|
+
Provides-Extra: audio
|
|
26
|
+
Requires-Dist: pygame-ce>=2.5; extra == "audio"
|
|
27
|
+
Provides-Extra: physics
|
|
28
|
+
Requires-Dist: pymunk>=6.0; extra == "physics"
|
|
29
|
+
Provides-Extra: models
|
|
30
|
+
Requires-Dist: trimesh>=3.21; extra == "models"
|
|
31
|
+
Provides-Extra: full
|
|
32
|
+
Requires-Dist: pyyaml>=6.0; extra == "full"
|
|
33
|
+
Requires-Dist: pygame-ce>=2.5; extra == "full"
|
|
34
|
+
Requires-Dist: trimesh>=3.21; extra == "full"
|
|
35
|
+
Requires-Dist: pymunk>=6.0; extra == "full"
|
|
36
|
+
Requires-Dist: freetype-py>=2.4; extra == "full"
|
|
37
|
+
Requires-Dist: imageio>=2.31; extra == "full"
|
|
38
|
+
Dynamic: author
|
|
39
|
+
Dynamic: classifier
|
|
40
|
+
Dynamic: description
|
|
41
|
+
Dynamic: description-content-type
|
|
42
|
+
Dynamic: home-page
|
|
43
|
+
Dynamic: license-file
|
|
44
|
+
Dynamic: provides-extra
|
|
45
|
+
Dynamic: requires-dist
|
|
46
|
+
Dynamic: requires-python
|
|
47
|
+
Dynamic: summary
|
|
48
|
+
|
|
49
|
+
# Aether Engine
|
|
50
|
+
|
|
51
|
+
**3D / 2D игровой движок на Python + OpenGL**
|
|
52
|
+
**Разработчик:** entiti937 · **Версия:** 1.2.0 · **Лицензия:** MIT
|
|
53
|
+
**PyPI:** https://pypi.org/project/aether-engine/
|
|
54
|
+
|
|
55
|
+
> Полное руководство: [`docs/GUIDE.md`](docs/GUIDE.md)
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Установка
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install -U "aether-engine[full]"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Showcase-игра (доказательство)
|
|
66
|
+
|
|
67
|
+
Minecraft-like sandbox **на этом же движке**:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
python games/aether_craft.py
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Там есть: воксельный мир, вид от 1 лица, ломать/ставить блоки, HUD, звуки, день/ночь, сейвы (F5/F9), урон и респавн.
|
|
74
|
+
|
|
75
|
+
## Другие запуски
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
python -m aether.editor
|
|
79
|
+
python games/orb_rush.py
|
|
80
|
+
python examples/03_first_person.py
|
|
81
|
+
python examples/04_physics2d.py
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Минимальный код
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from aether import Engine, Scene, Entity, MeshFactory, Material, Camera
|
|
88
|
+
from aether.scene.component import Transform, MeshRenderer
|
|
89
|
+
|
|
90
|
+
engine = Engine(1280, 720, "Game by you — engine by entiti937")
|
|
91
|
+
scene = Scene("Main")
|
|
92
|
+
camera = Camera(position=(0, 2, 6), yaw=-90)
|
|
93
|
+
scene.set_main_camera(camera)
|
|
94
|
+
|
|
95
|
+
cube = Entity("Cube")
|
|
96
|
+
cube.add_component(Transform(position=(0, 1, 0)))
|
|
97
|
+
mat = Material(); mat.albedo[:] = (0.2, 0.85, 1.0)
|
|
98
|
+
cube.add_component(MeshRenderer(mesh=MeshFactory.create_cube(), material=mat))
|
|
99
|
+
scene.spawn(cube)
|
|
100
|
+
|
|
101
|
+
engine.load_scene(scene)
|
|
102
|
+
engine.enable_first_person(camera)
|
|
103
|
+
engine.run()
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Возможности 1.2
|
|
107
|
+
|
|
108
|
+
- PBR 3D + sprites 2D + HUD overlay
|
|
109
|
+
- FPS (`enable_first_person`) и free-look камера
|
|
110
|
+
- Физика 3D/2D + raycast
|
|
111
|
+
- `ModelLoader` (OBJ/GLB/glTF/STL/FBX/…)
|
|
112
|
+
- Встроенный voxel model editor
|
|
113
|
+
- Procedural SFX, JSON save/load
|
|
114
|
+
- Примеры и две игры в репозитории
|
|
115
|
+
|
|
116
|
+
## Документация
|
|
117
|
+
|
|
118
|
+
Все команды, API и туториалы: **[docs/GUIDE.md](docs/GUIDE.md)**
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
Aether Engine © **entiti937**
|