Untitled

 avatar
unknown
plain_text
2 months ago
19 kB
4
Indexable
import sys
import tkinter as tk
from tkinter import ttk
import pygame
import numpy as np
from OpenGL.GL import *
from OpenGL.GLU import *
from pygame.locals import *
import math


class Component:
    def __init__(self, game_object):
        self.game_object = game_object
        game_object.components.append(self)

    def update(self):
        pass


class GameObject:
    def __init__(self, object_name="GameObject"):
        self.name = object_name
        self.position = np.array([0.0, 0.0, 0.0])
        self.rotation = np.array([0.0, 0.0, 0.0])
        self.scale = np.array([1.0, 1.0, 1.0])
        self.children = []
        self.parent = None
        self.components = []
        self.mesh_renderer = None
        self.gizmo = Gizmo(self)
        self.engine = None  # Will be set when added to scene

    def add_child(self, child):
        child.parent = self
        self.children.append(child)

    def update(self):
        for component in self.components:
            component.update()
        for child in self.children:
            child.update()


class Camera:
    def __init__(self):
        self.position = np.array([0.0, 2.0, 5.0], dtype=np.float32)
        self.front = np.array([0.0, 0.0, -1.0], dtype=np.float32)
        self.up = np.array([0.0, 1.0, 0.0], dtype=np.float32)
        self.right = np.array([1.0, 0.0, 0.0], dtype=np.float32)

        self.yaw = -90.0
        self.pitch = 0.0
        self.move_speed = 0.3
        self.mouse_sensitivity = 0.1
        self.last_mouse = None

    def update_camera_vectors(self):
        self.front[0] = math.cos(math.radians(self.yaw)) * math.cos(math.radians(self.pitch))
        self.front[1] = math.sin(math.radians(self.pitch))
        self.front[2] = math.sin(math.radians(self.yaw)) * math.cos(math.radians(self.pitch))
        self.front = self.front / np.linalg.norm(self.front)

        self.right = np.cross(self.front, np.array([0.0, 1.0, 0.0]))
        self.right = self.right / np.linalg.norm(self.right)
        self.up = np.cross(self.right, self.front)

    def handle_mouse(self, mouse_pos, right_button_down):
        if right_button_down:  # Only update camera if right mouse button is held down
            mouse_rel = pygame.mouse.get_rel()
            x_offset = mouse_rel[0]
            y_offset = -mouse_rel[1]

            x_offset *= self.mouse_sensitivity
            y_offset *= self.mouse_sensitivity

            self.yaw += x_offset
            self.pitch += y_offset
            self.pitch = np.clip(self.pitch, -89.0, 89.0)

            self.update_camera_vectors()

    def handle_keyboard(self, keys):
        if keys[K_w]:
            self.position += self.front * self.move_speed
        if keys[K_s]:
            self.position -= self.front * self.move_speed
        if keys[K_a]:
            self.position -= self.right * self.move_speed
        if keys[K_d]:
            self.position += self.right * self.move_speed
        if keys[K_SPACE]:
            self.position[1] += self.move_speed
        if keys[K_LSHIFT]:
            self.position[1] -= self.move_speed

    def apply_view(self):
        glLoadIdentity()
        gluPerspective(45, 800 / 600, 0.1, 1000.0)
        target = self.position + self.front
        gluLookAt(
            self.position[0], self.position[1], self.position[2],
            target[0], target[1], target[2],
            self.up[0], self.up[1], self.up[2]
        )


class PrimitiveFactory:
    @staticmethod
    def create_cube():
        vertices = [
            -0.5, -0.5, 0.5, 0.5, -0.5, 0.5, 0.5, 0.5, 0.5, -0.5, 0.5, 0.5,
            -0.5, -0.5, -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, -0.5, -0.5, 0.5, -0.5,
        ]
        indices = [
            0, 1, 2, 2, 3, 0,  # Front
            1, 5, 6, 6, 2, 1,  # Right
            5, 4, 7, 7, 6, 5,  # Back
            4, 0, 3, 3, 7, 4,  # Left
            3, 2, 6, 6, 7, 3,  # Top
            4, 5, 1, 1, 0, 4  # Bottom
        ]
        return vertices, indices

    @staticmethod
    def create_sphere(radius=1, segments=16):
        vertices = []
        indices = []
        for i in range(segments + 1):
            lat = np.pi * (-0.5 + float(i) / segments)
            for j in range(segments + 1):
                lon = 2 * np.pi * float(j) / segments
                x = np.cos(lat) * np.cos(lon) * radius
                y = np.sin(lat) * radius
                z = np.cos(lat) * np.sin(lon) * radius
                vertices.extend([x, y, z])

        for i in range(segments):
            for j in range(segments):
                first = i * (segments + 1) + j
                second = first + segments + 1
                indices.extend([first, second, first + 1, second, second + 1, first + 1])
        return vertices, indices


class Editor(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Game Engine Editor")
        self.geometry("400x800")  # Made taller to accommodate inspector

        # Hierarchy Panel
        self.hierarchy_frame = ttk.LabelFrame(self, text="Hierarchy")
        self.hierarchy_frame.pack(fill=tk.X, padx=5, pady=5)

        self.hierarchy_header = ttk.Frame(self.hierarchy_frame)
        self.hierarchy_header.pack(fill=tk.X, padx=5, pady=5)

        self.add_button = ttk.Button(self.hierarchy_header, text="Add", command=self.show_add_menu)
        self.add_button.pack(side=tk.LEFT)

        self.hierarchy_tree = ttk.Treeview(self.hierarchy_frame, height=15)
        self.hierarchy_tree.pack(fill=tk.X, padx=5, pady=5)
        self.hierarchy_tree.bind('<<TreeviewSelect>>', self.on_select)

        # Inspector Panel
        self.inspector_frame = ttk.LabelFrame(self, text="Inspector")
        self.inspector_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
        
        # Transform controls
        self.transform_frame = ttk.LabelFrame(self.inspector_frame, text="Transform")
        self.transform_frame.pack(fill=tk.X, padx=5, pady=5)
        
        # Position controls
        self.position_vars = [tk.StringVar(value="0.0") for _ in range(3)]
        self.create_vector3_control("Position", self.position_vars)
        
        # Rotation controls
        self.rotation_vars = [tk.StringVar(value="0.0") for _ in range(3)]
        self.create_vector3_control("Rotation", self.rotation_vars)
        
        # Scale controls
        self.scale_vars = [tk.StringVar(value="1.0") for _ in range(3)]
        self.create_vector3_control("Scale", self.scale_vars)

        self.selected_object = None
        self.scene_root = GameObject("Scene")
        self.scene_objects = {}
        self.hierarchy_tree.insert('', 'end', 'root', text='Scene')
        self.running = True

    def create_vector3_control(self, label, variables):
        frame = ttk.Frame(self.transform_frame)
        frame.pack(fill=tk.X, padx=5, pady=2)
        
        ttk.Label(frame, text=label, width=8).pack(side=tk.LEFT)
        
        for i, var in enumerate(variables):
            subframe = ttk.Frame(frame)
            subframe.pack(side=tk.LEFT, padx=2)
            
            ttk.Label(subframe, text="XYZ"[i], width=2).pack(side=tk.LEFT)
            entry = ttk.Entry(subframe, textvariable=var, width=8)
            entry.pack(side=tk.LEFT)
            entry.bind('<Return>', lambda e, idx=i, vars=variables: self.on_transform_change(label.lower(), idx, vars))

    def on_select(self, event):
        selected_items = self.hierarchy_tree.selection()
        if selected_items and selected_items[0] != 'root':
            self.selected_object = self.scene_objects[selected_items[0]]
            self.update_inspector()
        else:
            self.selected_object = None
            self.clear_inspector()

    def update_inspector(self):
        if self.selected_object:
            for i, var in enumerate(self.position_vars):
                var.set(f"{self.selected_object.position[i]:.2f}")
            for i, var in enumerate(self.rotation_vars):
                var.set(f"{self.selected_object.rotation[i]:.2f}")
            for i, var in enumerate(self.scale_vars):
                var.set(f"{self.selected_object.scale[i]:.2f}")

    def clear_inspector(self):
        for vars in [self.position_vars, self.rotation_vars, self.scale_vars]:
            for var in vars:
                var.set("0.0")

    def on_transform_change(self, transform_type, index, variables):
        if not self.selected_object:
            return
            
        try:
            value = float(variables[index].get())
            if transform_type == "position":
                self.selected_object.position[index] = value
            elif transform_type == "rotation":
                self.selected_object.rotation[index] = value
            elif transform_type == "scale":
                self.selected_object.scale[index] = value
        except ValueError:
            self.update_inspector()  # Reset to current values if invalid input

    def show_add_menu(self):
        menu = tk.Menu(self, tearoff=0)
        menu.add_command(label="Cube", command=lambda: self.add_primitive("Cube"))
        menu.add_command(label="Sphere", command=lambda: self.add_primitive("Sphere"))
        menu.tk_popup(self.add_button.winfo_rootx(),
                      self.add_button.winfo_rooty() + self.add_button.winfo_height())

    def add_primitive(self, primitive_type):
        game_object = GameObject(f"{primitive_type}_{len(self.scene_objects)}")
        game_object.engine = self.engine  # Set reference to engine
        vertices, indices = (PrimitiveFactory.create_cube() if primitive_type == "Cube"
                             else PrimitiveFactory.create_sphere())
        game_object.mesh_renderer = MeshRenderer(game_object, vertices, indices)
        self.scene_root.add_child(game_object)
        item_id = self.hierarchy_tree.insert('root', 'end', text=game_object.name)
        self.scene_objects[item_id] = game_object

    def process_events(self):
        self.update()


class MeshRenderer(Component):
    def __init__(self, game_object, vertices, indices):
        super().__init__(game_object)
        self.vertices = vertices
        self.indices = indices

    def render(self):
        glBegin(GL_TRIANGLES)
        for index in self.indices:
            vertex_idx = index * 3
            glVertex3f(self.vertices[vertex_idx],
                       self.vertices[vertex_idx + 1],
                       self.vertices[vertex_idx + 2])
        glEnd()

    def update(self):
        glPushMatrix()
        glTranslatef(*self.game_object.position)
        glRotatef(self.game_object.rotation[0], 1, 0, 0)
        glRotatef(self.game_object.rotation[1], 0, 1, 0)
        glRotatef(self.game_object.rotation[2], 0, 0, 1)
        glScalef(*self.game_object.scale)
        self.render()
        glPopMatrix()


class Gizmo(Component):
    def __init__(self, game_object):
        super().__init__(game_object)
        self.selected_axis = None
        self.axis_colors = {
            'x': (1.0, 0.0, 0.0),
            'y': (0.0, 1.0, 0.0),
            'z': (0.0, 0.0, 1.0)
        }
        self.axis_length = 1.0  # Base length of the gizmo axes
        self.dragging = False
        self.last_mouse_pos = None

    def render(self, camera_position):
        glDisable(GL_LIGHTING)
        glLineWidth(2.0)

        # Calculate distance from camera to object
        distance = np.linalg.norm(camera_position - self.game_object.position)
        scale_factor = distance * 0.1  # Scale gizmo size based on distance

        # Draw axes
        glBegin(GL_LINES)
        for axis, color in zip(['x', 'y', 'z'], [(1, 0, 0), (0, 1, 0), (0, 0, 1)]):
            glColor3f(*color)
            glVertex3f(0, 0, 0)
            if axis == 'x':
                glVertex3f(scale_factor, 0, 0)
            elif axis == 'y':
                glVertex3f(0, scale_factor, 0)
            else:
                glVertex3f(0, 0, scale_factor)
        glEnd()

        glLineWidth(1.0)
        glEnable(GL_LIGHTING)

    def update(self):
        if self.game_object == self.game_object.engine.editor.selected_object:
            glPushMatrix()
            glTranslatef(*self.game_object.position)
            glRotatef(self.game_object.rotation[0], 1, 0, 0)
            glRotatef(self.game_object.rotation[1], 0, 1, 0)
            glRotatef(self.game_object.rotation[2], 0, 0, 1)
            self.render(self.game_object.engine.camera.position)  # Pass camera position
            glPopMatrix()

    def handle_mouse_click(self, mouse_pos):
        # Check if the mouse is over any of the gizmo axes
        if self.selected_axis is None:
            for axis in ['x', 'y', 'z']:
                if self.is_mouse_over_axis(axis, mouse_pos):
                    self.selected_axis = axis
                    self.dragging = True
                    self.last_mouse_pos = mouse_pos
                    break

    def is_mouse_over_axis(self, axis, mouse_pos):
        # Convert mouse position to normalized device coordinates
        width, height = pygame.display.get_surface().get_size()
        x = (mouse_pos[0] / width) * 2 - 1
        y = 1 - (mouse_pos[1] / height) * 2

        # Check against the axis position in world coordinates
        if axis == 'x':
            axis_start = np.array([0, 0, 0])
            axis_end = np.array([self.axis_length, 0, 0])
        elif axis == 'y':
            axis_start = np.array([0, 0, 0])
            axis_end = np.array([0, self.axis_length, 0])
        else:  # 'z'
            axis_start = np.array([0, 0, 0])
            axis_end = np.array([0, 0, self.axis_length])

        # Check if the mouse is close to the axis line
        # This is a simplified distance check; you may want to implement a more precise method
        distance_to_axis = np.linalg.norm(np.cross(axis_end - axis_start, axis_start - np.array([x, y, 0]))) / np.linalg.norm(axis_end - axis_start)
        return distance_to_axis < 0.1  # Adjust threshold as needed

    def update_position(self, mouse_delta):
        if self.selected_axis and self.dragging:
            if self.selected_axis == 'x':
                self.game_object.position[0] += mouse_delta[0] * 0.1  # Adjust sensitivity as needed
            elif self.selected_axis == 'y':
                self.game_object.position[1] += mouse_delta[1] * 0.1
            elif self.selected_axis == 'z':
                self.game_object.position[2] += mouse_delta[2] * 0.1

    def stop_dragging(self):
        self.selected_axis = None
        self.dragging = False


class Engine:
    def __init__(self):
        pygame.init()
        self.editor = Editor()
        self.editor.engine = self  # Add reference to engine in editor

        self.display = (800, 600)
        self.screen = pygame.display.set_mode(self.display, DOUBLEBUF | OPENGL)
        pygame.display.set_caption("Game Engine Viewport")

        # Add mouse_captured state
        self.mouse_captured = True
        pygame.event.set_grab(True)
        pygame.mouse.set_visible(False)
        pygame.mouse.set_pos(self.display[0]//2, self.display[1]//2)
        self.mouse_rel_needs_reset = False

        glEnable(GL_DEPTH_TEST)
        
        # Update lighting configuration
        glEnable(GL_LIGHTING)
        glEnable(GL_LIGHT0)
        glLightfv(GL_LIGHT0, GL_POSITION, [0, 1, 0, 0])  # Directional light from above
        glLightfv(GL_LIGHT0, GL_AMBIENT, [0.2, 0.2, 0.2, 1.0])  # Add some ambient light
        glLightfv(GL_LIGHT0, GL_DIFFUSE, [0.5, 0.5, 0.5, 1.0])  # Reduce diffuse intensity
        
        # Enable color material to work better with basic colors
        glEnable(GL_COLOR_MATERIAL)
        glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE)

        self.camera = Camera()

    def toggle_mouse_capture(self):
        self.mouse_captured = not self.mouse_captured
        pygame.event.set_grab(self.mouse_captured)
        pygame.mouse.set_visible(not self.mouse_captured)
        if self.mouse_captured:
            pygame.mouse.set_pos(self.display[0]//2, self.display[1]//2)
            self.mouse_rel_needs_reset = True

    def handle_input(self):
        if self.mouse_captured:  # Only handle camera input when mouse is captured
            self.camera.handle_keyboard(pygame.key.get_pressed())
            self.camera.handle_mouse(pygame.mouse.get_pos(), pygame.mouse.get_pressed()[2])

    def render_grid(self):
        glDisable(GL_LIGHTING)  # Disable lighting for grid
        glColor3f(0.2, 0.2, 0.2)  # Set grid color to gray
        glBegin(GL_LINES)
        grid_size = 20
        step = 1.0
        for x in range(-grid_size, grid_size + 1):
            glVertex3f(x * step, 0, -grid_size * step)
            glVertex3f(x * step, 0, grid_size * step)
            glVertex3f(-grid_size * step, 0, x * step)
            glVertex3f(grid_size * step, 0, x * step)
        glEnd()
        glEnable(GL_LIGHTING)  # Re-enable lighting for other objects

    def run(self):
        while self.editor.running:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    self.editor.running = False
                elif event.type == pygame.KEYDOWN:
                    if event.key == K_ESCAPE:
                        self.toggle_mouse_capture()
                elif event.type == pygame.MOUSEBUTTONDOWN:
                    if event.button == 3:  # Right mouse button
                        self.toggle_mouse_capture()
                    elif event.button == 1:  # Left mouse button
                        if self.editor.selected_object and self.editor.selected_object.gizmo:
                            self.editor.selected_object.gizmo.handle_mouse_click(pygame.mouse.get_pos())

                elif event.type == pygame.MOUSEBUTTONUP:
                    if event.button == 3:  # Right mouse button
                        self.toggle_mouse_capture()
                    elif event.button == 1:  # Left mouse button
                        if self.editor.selected_object and self.editor.selected_object.gizmo:
                            self.editor.selected_object.gizmo.stop_dragging()

            right_button_down = pygame.mouse.get_pressed()[2]  # Check if right mouse button is down

            if self.mouse_rel_needs_reset:
                pygame.mouse.get_rel()
                self.mouse_rel_needs_reset = False

            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

            self.handle_input()
            self.camera.handle_mouse(pygame.mouse.get_pos(), right_button_down)  # Pass right button state
            self.camera.apply_view()

            self.render_grid()

            # Update all components for each game object
            for game_object in self.editor.scene_root.children:
                # First render the mesh
                if game_object.mesh_renderer:
                    game_object.mesh_renderer.update()
                
                # Then render the gizmo if object is selected
                if game_object.gizmo:
                    game_object.gizmo.update()

            pygame.display.flip()
            self.editor.process_events()
            pygame.time.wait(10)

        pygame.quit()
        self.editor.destroy()
        sys.exit()


if __name__ == "__main__":
    engine = Engine()
    engine.run()
Editor is loading...
Leave a Comment