# PyGine3D

A single-file importable 3D game engine for Python built on Pygame and OpenGL 3.3 Core.

> Created by **Somting** — MIT License

---

## What is it?

PyGine3D is a lightweight 3D game engine that lives in a single `.py` file. Drop it into your project, import it, and you have a full Unity-style game engine with GameObjects, Components, Scenes, cameras, lighting, collision, a 2D HUD overlay, and a Minecraft-style inventory — no framework setup, no project structure required.

---

## Installation

### Option 1 — pip
```
pip install pygine3d
```

### Option 2 — manual
Download `PyGine3D.py` and drop it next to your game file. Then at the top of your script force Python to load the local file:
```python
import sys
sys.path.insert(0, r"path/to/your/project")
import PyGine3D
```

### Dependencies
```
pip install pygame PyOpenGL PyOpenGL_accelerate numpy pillow
```

---

## Quick Start

```python
import sys
sys.path.insert(0, r"C:/your/project/folder")
import PyGine3D

engine = PyGine3D.Engine(800, 600, "My Game")
scene  = engine.scene

# Add a cube
cube = scene.spawn("Cube")
cube.add_component(PyGine3D.MeshRenderer(PyGine3D.Mesh.cube()))
cube.transform.position = PyGine3D.Vec3(0, 0, -5)

# Lighting
scene.ambient_light = PyGine3D.Color(0.2, 0.2, 0.2)
scene.directional_light = PyGine3D.DirectionalLight(
    direction=PyGine3D.Vec3(-1, -2, -1),
    color=PyGine3D.Color(1.0, 0.95, 0.85)
)

# Camera
engine.camera = PyGine3D.FlyCamera(speed=5.0)

engine.run()
```

---

## Core Concepts

PyGine3D uses a Unity-style Entity-Component architecture.

- **Engine** — creates the window, runs the game loop
- **Scene** — holds all GameObjects
- **GameObject** — an entity in the world, has a Transform and Components
- **Component** — behaviour attached to a GameObject, subclass to add logic
- **Transform** — position, rotation (Euler degrees), scale as Vec3

---

## API Reference

### Engine

```python
engine = PyGine3D.Engine(width, height, title, fps=60, clear_color=None)
```

| Property / Method | Description |
|---|---|
| `engine.scene` | The active Scene |
| `engine.camera` | The active Camera |
| `engine.input` | The Input object |
| `engine.hud` | The 2D HUD overlay |
| `engine.time` | Total elapsed seconds |
| `engine.frame` | Current frame number |
| `engine.on_start` | Callback — called once before the loop |
| `engine.on_update` | Callback — called every frame with `dt` |
| `engine.on_gui` | Callback — called every frame for HUD drawing |
| `engine.on_quit` | Callback — called when the engine stops |
| `engine.set_icon(path_or_surface)` | Set a custom window icon |
| `engine.quit()` | Stop the engine from anywhere |
| `engine.run()` | Start the game loop |

---

### Scene

```python
scene = engine.scene
```

| Method | Description |
|---|---|
| `scene.spawn(name)` | Create and return a new GameObject |
| `scene.destroy(go)` | Remove a GameObject from the scene |
| `scene.find(name)` | Find a GameObject by name |
| `scene.find_by_tag(tag)` | Find all GameObjects with a tag |
| `scene.check_collision(collider)` | Returns list of overlapping BoxColliders |
| `scene.ambient_light` | Color — ambient light applied to all objects |
| `scene.directional_light` | DirectionalLight — main directional light |

---

### GameObject

```python
go = scene.spawn("MyObject")
```

| Property / Method | Description |
|---|---|
| `go.name` | Name string |
| `go.active` | Bool — if False, object is skipped |
| `go.transform` | The Transform |
| `go.tags` | List of tag strings |
| `go.add_component(comp)` | Attach a component, returns it |
| `go.get_component(cls)` | Get first component of type |

---

### Transform

```python
go.transform.position = PyGine3D.Vec3(0, 1, -5)
go.transform.rotation = PyGine3D.Vec3(0, 45, 0)   # Euler degrees
go.transform.scale    = PyGine3D.Vec3(1, 1, 1)
```

---

### Component

Subclass `Component` to add custom behaviour to any GameObject.

```python
class Spinner(PyGine3D.Component):
    def on_start(self):
        pass  # called once when the scene starts

    def on_update(self, dt):
        self.owner.transform.rotation.y += 60 * dt  # spin 60 deg/sec

    def on_destroy(self):
        pass  # called when the GameObject is destroyed

cube.add_component(Spinner())
```

---

### Vec3

```python
v = PyGine3D.Vec3(x, y, z)
```

| Method / Property | Description |
|---|---|
| `v + v2`, `v - v2`, `v * scalar` | Arithmetic |
| `v.dot(v2)` | Dot product |
| `v.cross(v2)` | Cross product |
| `v.length()` | Magnitude |
| `v.normalized()` | Unit vector |
| `Vec3.zero()` | (0, 0, 0) |
| `Vec3.one()` | (1, 1, 1) |
| `Vec3.up()` | (0, 1, 0) |
| `Vec3.forward()` | (0, 0, -1) |
| `Vec3.right()` | (1, 0, 0) |

---

### Color

```python
c = PyGine3D.Color(r, g, b, a=1.0)   # values 0.0 to 1.0
```

Static helpers: `Color.white()`, `Color.black()`, `Color.red()`, `Color.green()`, `Color.blue()`

---

### Mesh

```python
PyGine3D.Mesh.cube()
PyGine3D.Mesh.sphere(radius=0.5, rings=16, sectors=16)
PyGine3D.Mesh.plane(size=10.0, divisions=4)
PyGine3D.Mesh.from_obj("path/to/model.obj")
```

---

### Texture

```python
tex = PyGine3D.Texture("path/to/image.png")
tex = PyGine3D.Texture.solid_color(r, g, b, a)   # 1x1 colour texture
```

---

### Material

```python
mat = PyGine3D.Material(texture=tex, color=PyGine3D.Color(1, 0.5, 0.2))
```

Both `texture` and `color` are optional. Color acts as a tint multiplied over the texture.

---

### MeshRenderer

```python
go.add_component(PyGine3D.MeshRenderer(mesh, material))
```

Renders a Mesh with a Material. Uses a built-in Phong shader automatically.

---

### Lighting

```python
scene.ambient_light = PyGine3D.Color(0.2, 0.2, 0.25)

scene.directional_light = PyGine3D.DirectionalLight(
    direction  = PyGine3D.Vec3(-1, -2, -1),
    color      = PyGine3D.Color(1.0, 0.95, 0.85),
    intensity  = 1.0
)
```

---

### BoxCollider

```python
go.add_component(PyGine3D.BoxCollider(PyGine3D.Vec3(0.5, 0.5, 0.5)))

# Check for overlaps
col  = go.get_component(PyGine3D.BoxCollider)
hits = scene.check_collision(col)   # returns list of BoxColliders
for h in hits:
    print(h.owner.name)
```

`auto_collision=True` is the default on `quick_cube`, `quick_plane`, and `quick_box` — they add a BoxCollider automatically.

---

### Cameras

#### FlyCamera
First-person free-fly camera. Right-click + drag to look, WASD to move.

```python
engine.camera = PyGine3D.FlyCamera(speed=5.0, sensitivity=0.15, fov=75)
```

Controls: `WASD` / arrow keys — move, `Q/E` — down/up, right-click drag — look

#### OrbitCamera
Orbits around a target point.

```python
engine.camera = PyGine3D.OrbitCamera(target=PyGine3D.Vec3(0,0,0), distance=8.0)
```

Controls: right-click drag — orbit, scroll wheel — zoom

---

### Input

```python
engine.input.get_key(pygame.K_w)          # held down
engine.input.get_key_down(pygame.K_space) # just pressed this frame
engine.input.get_mouse_button(1)          # 1=left, 2=middle, 3=right
engine.input.mouse_pos                    # (x, y) tuple
engine.input.mouse_delta                  # (dx, dy) tuple
```

Key constants are also available directly: `PyGine3D.K_W`, `PyGine3D.K_SPACE`, `PyGine3D.K_ESC`, etc.

---

### Convenience Spawners

```python
# Uniform scale cube with optional texture/color and auto BoxCollider
quick_cube(scene, position, color, texture, scale, name, auto_collision=True)

# Flat plane
quick_plane(scene, position, size, color, texture, name, auto_collision=True)

# Non-uniform box (width/height/depth independently)
quick_box(scene, position, size=Vec3(1,1,1), color, texture, name, auto_collision=True)
```

Set `auto_collision=False` for purely decorative objects that the player should walk through.

---

### HUD (2D Overlay)

Draw 2D elements on top of the 3D scene. Call inside `engine.on_gui`.

```python
def on_gui():
    hud = engine.hud
    hud.rect(x, y, w, h, color=(0,0,0,180), border_radius=6)
    hud.text("Hello!", x, y, size=20, color=(255,255,255), shadow=True)
    hud.text_centered("Centred", x, y, w, h, size=18)
    hud.image("icon.png", x, y, w, h)
    hud.line(x1, y1, x2, y2, color=(255,255,255), width=1)
    hud.circle(cx, cy, radius, color=(255,255,255,255))
    hud.crosshair(size=10, thickness=2, color=(255,255,255,200))
    hud.panel(x, y, w, h)   # dark semi-transparent panel

engine.on_gui = on_gui
```

---

### Inventory

Minecraft-style hotbar + grid inventory.

```python
inv = PyGine3D.Inventory(engine, cols=9, rows=4)

# Add items to hotbar
inv.hotbar[0] = PyGine3D.InventoryItem("Sword",  color=(180, 180, 200))
inv.hotbar[1] = PyGine3D.InventoryItem("Torch",  count=8, color=(255, 180, 40))

# Add items to grid
inv.add_item(PyGine3D.InventoryItem("Stone", count=32, color=(120,120,120)))

# Remove item from grid
inv.remove_item("Stone")

# Get selected hotbar item
item = inv.selected_item()

def on_update(dt):
    inv.update()   # handles E to open/close, 1-9 keys, scroll wheel

def on_gui():
    inv.draw()     # renders hotbar + grid if open

engine.on_update = on_update
engine.on_gui    = on_gui
```

`E` — open/close inventory grid  
`1-9` — select hotbar slot  
Scroll wheel — cycle hotbar selection

#### InventoryItem

```python
item = PyGine3D.InventoryItem(
    name  = "Pickaxe",
    count = 1,
    icon  = "pickaxe.png",   # optional — file path or pygame.Surface
    color = (140, 120, 100)  # fallback color if no icon
)
```

---

## Full Example

```python
import sys
sys.path.insert(0, r"C:/your/project")
import PyGine3D
import pygame

engine = PyGine3D.Engine(1280, 720, "My Game", fps=60)
scene  = engine.scene

# Lighting
scene.ambient_light     = PyGine3D.Color(0.2, 0.2, 0.25)
scene.directional_light = PyGine3D.DirectionalLight(
    direction=PyGine3D.Vec3(-1, -2, -1),
    color=PyGine3D.Color(1.0, 0.95, 0.85),
    intensity=1.0
)

# Ground
floor = PyGine3D.quick_plane(scene, position=PyGine3D.Vec3(0,-0.5,0),
                              size=20, color=PyGine3D.Color(0.3,0.5,0.3))

# Cube with custom component
cube = PyGine3D.quick_cube(scene, position=PyGine3D.Vec3(0,0,-5),
                            color=PyGine3D.Color(0.8,0.3,0.2))

class Spinner(PyGine3D.Component):
    def on_update(self, dt):
        self.owner.transform.rotation.y += 45 * dt

cube.add_component(Spinner())

# Camera
engine.camera = PyGine3D.FlyCamera(speed=6.0)

# HUD
def on_gui():
    engine.hud.crosshair()
    engine.hud.text(f"Time: {engine.time:.1f}s", 10, 10, size=18)

engine.on_gui = on_gui
engine.run()
```

---

## Version History

| Version | Notes |
|---|---|
| 1.1.2 | Fixed OpenGL wildcard import clobbering class names |
| 1.1.1 | Added 2D HUD overlay and Inventory system |
| 1.1.0 | Added auto_collision, quick_box, startup banner, custom icon support |
| 1.0.0 | Initial release |

---

## License

MIT License — Copyright (c) 2026 Somting

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.