diff --git a/README.md b/README.md
index 8e392052..692aad28 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,4 @@
+
# MCPY
Source code for each episode of my Minecraft clone in Python YouTube tutorial series.
@@ -20,12 +21,22 @@ And for Debian-based Linux distros:
[](https://youtu.be/TtkTkfwwefA?list=PL6_bLxRDFzoKjaa3qCGkwR5L_ouSreaVP)
-The `pyglet` module is a necessary dependency for all episodes, the `nbtlib` & `base36` modules are necessary dependencies for all episodes starting with 11, and the `pyglm` module is necessary for the `community` directory. You can install them with PIP by issuing:
+The `pyglet` module is a necessary dependency for all episodes, the `nbtlib` & `base36` modules are necessary dependencies for all episodes starting with 11.
+
+The `pyglm` and PyImGUI modules are necessary for the `community` directory. You can install them with PIP by issuing:
+
+- Without community additions:
```console
pip install --user pyglet nbtlib base36 pyglm
```
+- With community additions:
+
+```console
+pip install --user -r community/requirements.txt
+```
+
## Running
Run the following command in the directory of any episode to run the result from that episode:
@@ -44,6 +55,7 @@ Characteristic contributions are contributions which *add* something to the code
Contributions which *fix* something are still merged on the source of all episodes.
The community has several features and options that can be toggled in `options.py`:
+
- Render Distance: At what distance (in chunks) should chunks stop being rendered
- FOV: Camera field of view
@@ -59,12 +71,13 @@ The community has several features and options that can be toggled in `options.p
- Mipmap (minification filtering): Texture filtering used on higher distances. Default is `GL_NEAREST` (no filtering) (more info in `options.py`)
- Colored lighting: Uses an alternative shader program to achieve a more colored lighting; it aims to look similar to Beta 1.8+ (no performance loss should be incurred)
- Antialiasing: Experimental feature
+- A main menu / UI system: Uses PyImGUI to create a menu system which appears when the game loads instead of loading the world straight away.
## List of projects based on this
-- **Nim implementation:** https://github.com/phargobikcin/nim-minecraft-clone
-- **Java implementation:** https://github.com/StartForKillerMC/JavaMinecraft
-- **C++ implementation:** https://github.com/Jukitsu/CppMinecraft-clone
-- **Odin implementation:** https://github.com/anthony-63/lvo
-- **Lua implementation:** https://github.com/brennop/lunarcraft
-- **Javascript implementation:** https://github.com/drakeerv/js-minecraft-clone ([Demo](https://drakeerv.github.io/js-minecraft-clone/))
+- **Nim implementation:**
+- **Java implementation:**
+- **C++ implementation:**
+- **Odin implementation:**
+- **Lua implementation:**
+- **Javascript implementation:** ([Demo](https://drakeerv.github.io/js-minecraft-clone/))
diff --git a/community/controller.py b/community/controller.py
index a3a433c2..e0989721 100644
--- a/community/controller.py
+++ b/community/controller.py
@@ -57,12 +57,12 @@ def misc(self, mode):
elif mode == self.MiscMode.SAVE:
self.game.world.save.save()
elif mode == self.MiscMode.ESCAPE:
- self.game.mouse_captured = False
- self.game.set_exclusive_mouse(False)
+ self.game.window.mouse_captured = False
+ self.game.window.set_exclusive_mouse(False)
elif mode == self.MiscMode.SPEED_TIME:
self.game.world.speed_daytime()
elif mode == self.MiscMode.FULLSCREEN:
- self.game.toggle_fullscreen()
+ self.game.window.toggle_fullscreen()
elif mode == self.MiscMode.FLY:
self.game.player.flying = not self.game.player.flying
elif mode == self.MiscMode.TELEPORT:
diff --git a/community/joystick.py b/community/joystick.py
index 455b136a..76057c45 100644
--- a/community/joystick.py
+++ b/community/joystick.py
@@ -41,7 +41,7 @@ def init_joysticks(self, joysticks):
joystick.open(exclusive=True)
def update_controller(self):
- if not self.game.mouse_captured or not self.joysticks:
+ if not self.game.window.mouse_captured or not self.joysticks:
return
self.game.player.rotation[0] += self.joystick_look[0] * self.camera_sensitivity
diff --git a/community/keyboard_mouse.py b/community/keyboard_mouse.py
index c1146707..2bf9026a 100644
--- a/community/keyboard_mouse.py
+++ b/community/keyboard_mouse.py
@@ -2,73 +2,79 @@
import controller
import math
+import imgui
class Keyboard_Mouse(controller.Controller):
def __init__(self, game):
super().__init__(game)
- self.game.on_mouse_press = self.on_mouse_press
- self.game.on_mouse_motion = self.on_mouse_motion
- self.game.on_mouse_drag = self.on_mouse_drag
+ self.game.window.on_mouse_press = self.on_mouse_press
+ self.game.window.on_mouse_motion = self.on_mouse_motion
+ self.game.window.on_mouse_drag = self.on_mouse_drag
- self.game.on_key_press = self.on_key_press
- self.game.on_key_release = self.on_key_release
+ self.game.window.on_key_press = self.on_key_press
+ self.game.window.on_key_release = self.on_key_release
def on_mouse_press(self, x, y, button, modifiers):
- if not self.game.mouse_captured:
- self.game.mouse_captured = True
- self.game.set_exclusive_mouse(True)
+ if not imgui.get_io().want_capture_mouse:
+ if not self.game.window.mouse_captured:
+ self.game.window.mouse_captured = True
+ self.game.window.set_exclusive_mouse(True)
- return
+ return
- if button == pyglet.window.mouse.RIGHT: self.interact(self.InteractMode.PLACE)
- elif button == pyglet.window.mouse.LEFT: self.interact(self.InteractMode.BREAK)
- elif button == pyglet.window.mouse.MIDDLE: self.interact(self.InteractMode.PICK)
+ if button == pyglet.window.mouse.RIGHT: self.interact(self.InteractMode.PLACE)
+ elif button == pyglet.window.mouse.LEFT: self.interact(self.InteractMode.BREAK)
+ elif button == pyglet.window.mouse.MIDDLE: self.interact(self.InteractMode.PICK)
def on_mouse_motion(self, x, y, delta_x, delta_y):
- if self.game.mouse_captured:
- sensitivity = 0.004
+ if not imgui.get_io().want_capture_mouse:
+ if self.game.window.mouse_captured:
+ sensitivity = 0.004
- self.game.player.rotation[0] += delta_x * sensitivity
- self.game.player.rotation[1] += delta_y * sensitivity
+ self.game.player.rotation[0] += delta_x * sensitivity
+ self.game.player.rotation[1] += delta_y * sensitivity
- self.game.player.rotation[1] = max(-math.tau / 4, min(math.tau / 4, self.game.player.rotation[1]))
+ self.game.player.rotation[1] = max(-math.tau / 4, min(math.tau / 4, self.game.player.rotation[1]))
def on_mouse_drag(self, x, y, delta_x, delta_y, buttons, modifiers):
- self.on_mouse_motion(x, y, delta_x, delta_y)
+ if not imgui.get_io().want_capture_mouse:
+ self.on_mouse_motion(x, y, delta_x, delta_y)
def on_key_press(self, key, modifiers):
- if not self.game.mouse_captured:
- return
-
- if key == pyglet.window.key.D: self.start_move(self.MoveMode.RIGHT)
- elif key == pyglet.window.key.A: self.start_move(self.MoveMode.LEFT)
- elif key == pyglet.window.key.W: self.start_move(self.MoveMode.FORWARD)
- elif key == pyglet.window.key.S: self.start_move(self.MoveMode.BACKWARD)
- elif key == pyglet.window.key.SPACE : self.start_move(self.MoveMode.UP)
- elif key == pyglet.window.key.LSHIFT: self.start_move(self.MoveMode.DOWN)
-
- elif key == pyglet.window.key.LCTRL : self.start_modifier(self.ModifierMode.SPRINT)
-
- elif key == pyglet.window.key.F: self.misc(self.MiscMode.FLY)
- elif key == pyglet.window.key.G: self.misc(self.MiscMode.RANDOM)
- elif key == pyglet.window.key.O: self.misc(self.MiscMode.SAVE)
- elif key == pyglet.window.key.R: self.misc(self.MiscMode.TELEPORT)
- elif key == pyglet.window.key.ESCAPE: self.misc(self.MiscMode.ESCAPE)
- elif key == pyglet.window.key.F6: self.misc(self.MiscMode.SPEED_TIME)
- elif key == pyglet.window.key.F11: self.misc(self.MiscMode.FULLSCREEN)
- elif key == pyglet.window.key.F3: self.misc(self.MiscMode.TOGGLE_F3)
- elif key == pyglet.window.key.F10: self.misc(self.MiscMode.TOGGLE_AO)
+ if not imgui.get_io().want_capture_keyboard:
+ if not self.game.window.mouse_captured:
+ return
+
+ if key == pyglet.window.key.D: self.start_move(self.MoveMode.RIGHT)
+ elif key == pyglet.window.key.A: self.start_move(self.MoveMode.LEFT)
+ elif key == pyglet.window.key.W: self.start_move(self.MoveMode.FORWARD)
+ elif key == pyglet.window.key.S: self.start_move(self.MoveMode.BACKWARD)
+ elif key == pyglet.window.key.SPACE : self.start_move(self.MoveMode.UP)
+ elif key == pyglet.window.key.LSHIFT: self.start_move(self.MoveMode.DOWN)
+
+ elif key == pyglet.window.key.LCTRL : self.start_modifier(self.ModifierMode.SPRINT)
+
+ elif key == pyglet.window.key.F: self.misc(self.MiscMode.FLY)
+ elif key == pyglet.window.key.G: self.misc(self.MiscMode.RANDOM)
+ elif key == pyglet.window.key.O: self.misc(self.MiscMode.SAVE)
+ elif key == pyglet.window.key.R: self.misc(self.MiscMode.TELEPORT)
+ elif key == pyglet.window.key.ESCAPE: self.misc(self.MiscMode.ESCAPE)
+ elif key == pyglet.window.key.F6: self.misc(self.MiscMode.SPEED_TIME)
+ elif key == pyglet.window.key.F11: self.misc(self.MiscMode.FULLSCREEN)
+ elif key == pyglet.window.key.F3: self.misc(self.MiscMode.TOGGLE_F3)
+ elif key == pyglet.window.key.F10: self.misc(self.MiscMode.TOGGLE_AO)
def on_key_release(self, key, modifiers):
- if not self.game.mouse_captured:
- return
-
- if key == pyglet.window.key.D: self.end_move(self.MoveMode.RIGHT)
- elif key == pyglet.window.key.A: self.end_move(self.MoveMode.LEFT)
- elif key == pyglet.window.key.W: self.end_move(self.MoveMode.FORWARD)
- elif key == pyglet.window.key.S: self.end_move(self.MoveMode.BACKWARD)
- elif key == pyglet.window.key.SPACE : self.end_move(self.MoveMode.UP)
- elif key == pyglet.window.key.LSHIFT: self.end_move(self.MoveMode.DOWN)
-
- elif key == pyglet.window.key.LCTRL : self.end_modifier(self.ModifierMode.SPRINT)
\ No newline at end of file
+ if not imgui.get_io().want_capture_keyboard:
+ if not self.game.window.mouse_captured:
+ return
+
+ if key == pyglet.window.key.D: self.end_move(self.MoveMode.RIGHT)
+ elif key == pyglet.window.key.A: self.end_move(self.MoveMode.LEFT)
+ elif key == pyglet.window.key.W: self.end_move(self.MoveMode.FORWARD)
+ elif key == pyglet.window.key.S: self.end_move(self.MoveMode.BACKWARD)
+ elif key == pyglet.window.key.SPACE : self.end_move(self.MoveMode.UP)
+ elif key == pyglet.window.key.LSHIFT: self.end_move(self.MoveMode.DOWN)
+
+ elif key == pyglet.window.key.LCTRL : self.end_modifier(self.ModifierMode.SPRINT)
\ No newline at end of file
diff --git a/community/main.py b/community/main.py
index aaf34b2c..810bb840 100644
--- a/community/main.py
+++ b/community/main.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import platform
import ctypes
import logging
@@ -26,6 +28,10 @@
import keyboard_mouse
from collections import deque
+import imgui
+from imgui.integrations.pyglet import create_renderer
+
+
class InternalConfig:
def __init__(self, options):
self.RENDER_DISTANCE = options.RENDER_DISTANCE
@@ -43,62 +49,61 @@ def __init__(self, options):
self.ANTIALIASING = options.ANTIALIASING
-class Window(pyglet.window.Window):
- def __init__(self, **args):
- super().__init__(**args)
+class Scene():
+ def __init__(self, window: Window) -> None:
+ self.window = window
- # Options
- self.options = InternalConfig(options)
+ def on_close(self):
+ pass
+
+ def on_draw(self):
+ pass
+
+ def on_resize(self, width, height):
+ pass
+
+ def update(self, delta_time):
+ pass
+
+ def update_ui(self):
+ pass
+
+
+class GameScene(Scene):
+ def __init__(self, window: Window, path) -> None:
+ super().__init__(window)
+ logging.info("Loading game scene")
- if self.options.INDIRECT_RENDERING and not gl.gl_info.have_version(4, 2):
- raise RuntimeError("""Indirect Rendering is not supported on your hardware
- This feature is only supported on OpenGL 4.2+, but your driver doesnt seem to support it,
- Please disable "INDIRECT_RENDERING" in options.py""")
-
# F3 Debug Screen
self.show_f3 = False
- self.f3 = pyglet.text.Label("", x = 10, y = self.height - 10,
- font_size = 16,
- color = (255, 255, 255, 255),
- width = self.width // 3,
- multiline = True
- )
- self.system_info = f"""Python: {platform.python_implementation()} {platform.python_version()}
-System: {platform.machine()} {platform.system()} {platform.release()} {platform.version()}
-CPU: {platform.processor()}
-Display: {gl.gl_info.get_renderer()}
-{gl.gl_info.get_version()}"""
- logging.info(f"System Info: {self.system_info}")
+ # create textures
+ logging.info("Creating Texture Array")
+ self.texture_manager = texture_manager.TextureManager(16, 16, 256)
+
# create shader
logging.info("Compiling Shaders")
- if not self.options.COLORED_LIGHTING:
+ if not self.window.options.COLORED_LIGHTING:
self.shader = shader.Shader("shaders/alpha_lighting/vert.glsl", "shaders/alpha_lighting/frag.glsl")
else:
self.shader = shader.Shader("shaders/colored_lighting/vert.glsl", "shaders/colored_lighting/frag.glsl")
self.shader_sampler_location = self.shader.find_uniform(b"u_TextureArraySampler")
self.shader.use()
- # create textures
- logging.info("Creating Texture Array")
- self.texture_manager = texture_manager.TextureManager(16, 16, 256)
-
# create world
- self.world = world.World(self.shader, None, self.texture_manager, self.options)
+ self.world = world.World(path, self.shader, None, self.texture_manager, self.window.options)
# player stuff
logging.info("Setting up player & camera")
- self.player = player.Player(self.world, self.shader, self.width, self.height)
+ self.player = player.Player(self.world, self.shader, self.window.width, self.window.height)
self.world.player = self.player
# pyglet stuff
pyglet.clock.schedule(self.player.update_interpolation)
- pyglet.clock.schedule_interval(self.update, 1 / 60)
- self.mouse_captured = False
# misc stuff
@@ -110,17 +115,6 @@ def __init__(self, **args):
gl.glBindTexture(gl.GL_TEXTURE_2D_ARRAY, self.world.texture_manager.texture_array)
gl.glUniform1i(self.shader_sampler_location, 0)
- # enable cool stuff
-
- gl.glEnable(gl.GL_DEPTH_TEST)
- gl.glEnable(gl.GL_CULL_FACE)
- gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
-
- if self.options.ANTIALIASING:
- gl.glEnable(gl.GL_MULTISAMPLE)
- gl.glEnable(gl.GL_SAMPLE_ALPHA_TO_COVERAGE)
- gl.glSampleCoverage(0.5, gl.GL_TRUE)
-
# controls stuff
self.controls = [0, 0, 0]
@@ -149,22 +143,18 @@ def __init__(self, **args):
self.media_player.next_time = 0
- # GPU command syncs
- self.fences = deque()
-
- def toggle_fullscreen(self):
- self.set_fullscreen(not self.fullscreen)
-
def on_close(self):
+ super().on_close()
logging.info("Deleting media player")
self.media_player.delete()
- for fence in self.fences:
+ for fence in self.window.fences:
gl.glDeleteSync(fence)
- super().on_close()
-
- def update_f3(self, delta_time):
+ def update_f3(self):
"""Update the F3 debug screen content"""
+ imgui.set_next_window_position(5, 5)
+ imgui.set_next_window_bg_alpha(0.175)
+ imgui.get_io().ini_file_name = None
player_chunk_pos = world.get_chunk_position(self.player.position)
player_local_pos = world.get_local_position(self.player.position)
@@ -172,31 +162,39 @@ def update_f3(self, delta_time):
visible_chunk_count = len(self.world.visible_chunks)
quad_count = sum(chunk.mesh_quad_count for chunk in self.world.chunks.values())
visible_quad_count = sum(chunk.mesh_quad_count for chunk in self.world.visible_chunks)
- self.f3.text = \
-f"""
-{round(1 / delta_time)} FPS ({self.world.chunk_update_counter} Chunk Updates) {"inf" if not self.options.VSYNC else "vsync"}{"ao" if self.options.SMOOTH_LIGHTING else ""}
+
+ if imgui.begin(
+ "F3 Debug Screen",
+ self.show_f3,
+ flags=
+ imgui.WINDOW_NO_DECORATION |
+ imgui.WINDOW_ALWAYS_AUTO_RESIZE |
+ imgui.WINDOW_NO_SAVED_SETTINGS |
+ imgui.WINDOW_NO_FOCUS_ON_APPEARING |
+ imgui.WINDOW_NO_NAV
+ ):
+ imgui.text(f"""
+{round(1 / self.window.delta_time)} FPS ({self.world.chunk_update_counter} Chunk Updates) {"inf" if not self.window.options.VSYNC else "vsync"}{"ao" if self.window.options.SMOOTH_LIGHTING else ""}
C: {visible_chunk_count} / {chunk_count} pC: {self.world.pending_chunk_update_count} pU: {len(self.world.chunk_building_queue)} aB: {chunk_count}
-Client Singleplayer @{round(delta_time * 1000)} ms tick {round(1 / delta_time)} TPS
+Client Singleplayer @{round(self.window.delta_time * 1000)} ms tick {round(1 / self.window.delta_time)} TPS
XYZ: ( X: {round(self.player.position[0], 3)} / Y: {round(self.player.position[1], 3)} / Z: {round(self.player.position[2], 3)} )
Block: {self.player.rounded_position[0]} {self.player.rounded_position[1]} {self.player.rounded_position[2]}
Chunk: {player_local_pos[0]} {player_local_pos[1]} {player_local_pos[2]} in {player_chunk_pos[0]} {player_chunk_pos[1]} {player_chunk_pos[2]}
Light: {max(self.world.get_light(self.player.rounded_position), self.world.get_skylight(self.player.rounded_position))} ({self.world.get_skylight(self.player.rounded_position)} sky, {self.world.get_light(self.player.rounded_position)} block)
-{self.system_info}
+{self.window.system_info}
-Renderer: {"OpenGL 3.3 VAOs" if not self.options.INDIRECT_RENDERING else "OpenGL 4.0 VAOs Indirect"} {"Conditional" if self.options.ADVANCED_OPENGL else ""}
+Renderer: {"OpenGL 3.3 VAOs" if not self.window.options.INDIRECT_RENDERING else "OpenGL 4.0 VAOs Indirect"} {"Conditional" if self.window.options.ADVANCED_OPENGL else ""}
Buffers: {chunk_count}
Vertex Data: {round(quad_count * 28 * ctypes.sizeof(gl.GLfloat) / 1048576, 3)} MiB ({quad_count} Quads)
Visible Quads: {visible_quad_count}
Buffer Uploading: Direct (glBufferSubData)
-"""
+ """)
+ imgui.end()
def update(self, delta_time):
- """Every tick"""
- if self.show_f3:
- self.update_f3(delta_time)
-
+ super().update(delta_time)
if not self.media_player.source and len(self.music) > 0:
if not self.media_player.standby:
self.media_player.standby = True
@@ -206,7 +204,7 @@ def update(self, delta_time):
self.media_player.queue(random.choice(self.music))
self.media_player.play()
- if not self.mouse_captured:
+ if not self.window.mouse_captured:
self.player.input = [0, 0, 0]
self.joystick_controller.update_controller()
@@ -214,42 +212,240 @@ def update(self, delta_time):
self.world.tick(delta_time)
+ def update_ui(self):
+ super().update_ui()
+ if self.show_f3:
+ self.update_f3()
+
def on_draw(self):
+ super().on_draw()
gl.glEnable(gl.GL_DEPTH_TEST)
self.shader.use()
self.player.update_matrices()
- while len(self.fences) > self.options.MAX_CPU_AHEAD_FRAMES:
- fence = self.fences.popleft()
+ while len(self.window.fences) > self.window.options.MAX_CPU_AHEAD_FRAMES:
+ fence = self.window.fences.popleft()
gl.glClientWaitSync(fence, gl.GL_SYNC_FLUSH_COMMANDS_BIT, 2147483647)
gl.glDeleteSync(fence)
- self.clear()
+ self.window.clear()
self.world.prepare_rendering()
self.world.draw()
- # Draw the F3 Debug screen
- if self.show_f3:
- self.f3.draw()
-
# CPU - GPU Sync
- if not self.options.SMOOTH_FPS:
+ if not self.window.options.SMOOTH_FPS:
# self.fences.append(gl.glFenceSync(gl.GL_SYNC_GPU_COMMANDS_COMPLETE, 0))
# Broken in pyglet 2; glFenceSync is missing
pass
else:
gl.glFinish()
- # input functions
-
def on_resize(self, width, height):
+ super().on_resize(width, height)
logging.info(f"Resize {width} * {height}")
gl.glViewport(0, 0, width, height)
self.player.view_width = width
self.player.view_height = height
- self.f3.y = self.height - 10
- self.f3.width = self.width // 3
+
+
+class MenuScene(Scene):
+ def __init__(self, window: Window) -> None:
+ super().__init__(window)
+ self.texture_manager = texture_manager.TextureManager(0, 0, 0)
+
+ self.icon = self.texture_manager.load_texture("dirt", use_pyglet_gl=False)
+ self.logo = self.texture_manager.load_texture("logo", use_pyglet_gl=False)
+
+ def on_draw(self):
+ super().on_draw()
+ gl.glEnable(gl.GL_TEXTURE_2D)
+ self.window.clear()
+
+ def update_ui(self):
+ super().update_ui()
+ io = imgui.get_io()
+ imgui.set_next_window_size(io.display_size.x, io.display_size.y)
+ imgui.set_next_window_position(0, 0)
+ imgui.push_style_var(imgui.STYLE_WINDOW_BORDERSIZE, 0.0)
+ imgui.push_style_var(imgui.STYLE_WINDOW_PADDING, (0.0, 0.0))
+
+ if imgui.begin(
+ "MCPY",
+ True,
+ flags=
+ imgui.WINDOW_NO_DECORATION |
+ imgui.WINDOW_ALWAYS_AUTO_RESIZE |
+ imgui.WINDOW_NO_SAVED_SETTINGS |
+ imgui.WINDOW_NO_NAV
+ ):
+ # Get the draw list
+ draw_list = imgui.get_window_draw_list()
+
+ # Tile the image across the window
+ for x in range(0, int(io.display_size.x), self.icon.width):
+ for y in range(0, int(io.display_size.y), self.icon.height):
+ draw_list.add_image(self.icon.id, (x, y), (x + self.icon.width, y + self.icon.height), self.logo.uv0, self.logo.uv1)
+
+ # Draw a semi-transparent overlay to darken the background
+ overlay_color = imgui.get_color_u32_rgba(0, 0, 0, 0.75)
+ draw_list.add_rect_filled(0, 0, io.display_size.x, io.display_size.y, overlay_color)
+
+
+ # Calculate the position to horizontally center the image
+ image_width = self.logo.width / 2.5
+ image_height = self.logo.height / 2.5
+
+ # Set the cursor position to the calculated center position
+ imgui.set_cursor_pos(((io.display_size.x - image_width) / 2, 50))
+
+ # Draw the image at the calculated position
+ imgui.image(self.logo.id, image_width, image_height, self.logo.uv0, self.logo.uv1)
+
+ # Calculate the position to horizontally center the buttons
+ button_width = 300 # Adjust button width as needed
+ button_height = 30 # Adjust button height as needed
+ button_x = (io.display_size.x - button_width) / 2
+ button_y = imgui.get_cursor_pos().y + image_height - 50
+
+ # Set the cursor position to the calculated center position for buttons
+ imgui.set_cursor_pos((button_x, button_y))
+
+ # Add buttons under the logo
+ if imgui.button("Singleplayer", width=button_width, height=button_height):
+ # Handle button 1 click
+ pass
+
+ imgui.set_cursor_pos((button_x, imgui.get_cursor_pos().y + 10))
+
+ if imgui.button("Multiplayer", width=button_width, height=button_height):
+ # Handle button 2 click
+ pass
+
+ imgui.set_cursor_pos((button_x, imgui.get_cursor_pos().y + 10))
+
+ if imgui.button("Play tutorial level", width=button_width, height=button_height):
+ scene = GameScene(self.window, "save")
+ self.window.push_scene(scene)
+
+ imgui.set_cursor_pos((button_x, imgui.get_cursor_pos().y + 20))
+
+ if imgui.button("Options", width=button_width, height=button_height):
+ # Handle button 2 click
+ pass
+
+ text = "Copyright MCPY contributors. MCPY is licensed under the MIT."
+ # Calculate the position to render the text in the bottom right
+ text_width, text_height = imgui.calc_text_size(text)
+ text_x = io.display_size.x - text_width
+ text_y = io.display_size.y - text_height
+
+ # Set the cursor position to the calculated position for the text
+ imgui.set_cursor_pos((text_x, text_y))
+
+ # Render the text in the bottom right
+ imgui.text(text)
+ imgui.end()
+ imgui.pop_style_var()
+ imgui.pop_style_var()
+
+
+class Window(pyglet.window.Window):
+ def __init__(self, **args):
+ super().__init__(**args)
+
+ # Options
+ self.options = InternalConfig(options)
+
+ if self.options.INDIRECT_RENDERING and not gl.gl_info.have_version(4, 2):
+ raise RuntimeError("""Indirect Rendering is not supported on your hardware
+ This feature is only supported on OpenGL 4.2+, but your driver doesnt seem to support it,
+ Please disable "INDIRECT_RENDERING" in options.py""")
+
+ self.system_info = f"""Python: {platform.python_implementation()} {platform.python_version()}
+System: {platform.machine()} {platform.system()} {platform.release()} {platform.version()}
+CPU: {platform.processor()}
+Display: {gl.gl_info.get_renderer()}
+{gl.gl_info.get_version()}"""
+
+ logging.info(f"System Info: {self.system_info}")
+
+ # set scene
+ self.scenes = [MenuScene(self)]
+ self.current_scene = self.scenes[0]
+ self.go_next_scene = False
+ self.mouse_captured = False
+
+ # enable cool stuff
+
+ gl.glEnable(gl.GL_DEPTH_TEST)
+ gl.glEnable(gl.GL_CULL_FACE)
+ gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
+
+ if self.options.ANTIALIASING:
+ gl.glEnable(gl.GL_MULTISAMPLE)
+ gl.glEnable(gl.GL_SAMPLE_ALPHA_TO_COVERAGE)
+ gl.glSampleCoverage(0.5, gl.GL_TRUE)
+
+ # GPU command syncs
+ self.fences = deque()
+
+ # ui stuff
+ imgui.create_context()
+ self.impl = create_renderer(self)
+ self.delta_time = 1
+ pyglet.clock.schedule_interval(self.update, 1 / 60)
+
+ def toggle_fullscreen(self):
+ self.set_fullscreen(not self.fullscreen)
+
+ def on_close(self):
+ self.current_scene.on_close()
+ super().on_close()
+
+ def update_ui(self):
+ self.current_scene.update_ui()
+
+ def update(self, delta_time):
+ """Every tick"""
+ self.impl.process_inputs()
+ self.delta_time = delta_time
+ self.current_scene.update(delta_time)
+ self.try_next_scene()
+
+ def on_draw(self):
+ self.current_scene.on_draw()
+
+ # Handle UI
+ imgui.new_frame()
+ self.update_ui()
+ imgui.render()
+ self.impl.render(imgui.get_draw_data())
+
+ # input functions
+
+ def on_resize(self, width, height):
+ self.current_scene.on_resize(width, height)
+
+ # scene functions
+
+ def push_scene(self, scene):
+ self.scenes.append(scene)
+ self.go_next_scene = True
+
+ def pop_scene(self):
+ return self.scenes.pop(len(self.scenes) - 1)
+
+ def try_next_scene(self):
+ if self.go_next_scene:
+ try:
+ scene = self.scenes[len(self.scenes) - 1]
+ if scene != self.current_scene:
+ self.current_scene = self.pop_scene()
+ self.go_next_scene = False
+ except IndexError:
+ pass # do nothing because there are no other scenes to switch to.
+
class Game:
def __init__(self):
@@ -262,7 +458,6 @@ def run(self):
pyglet.app.run(interval = 0)
-
def init_logger():
log_folder = "logs/"
log_filename = f"{time.time()}.log"
@@ -278,8 +473,6 @@ def init_logger():
format="[%(asctime)s] [%(processName)s/%(threadName)s/%(levelname)s] (%(module)s.py/%(funcName)s) %(message)s")
-
-
def main():
init_logger()
game = Game()
diff --git a/community/requirements.txt b/community/requirements.txt
new file mode 100644
index 00000000..2b49805e
--- /dev/null
+++ b/community/requirements.txt
@@ -0,0 +1,8 @@
+pyglet >= 2.0.10
+nbtlib >= 2.0.4
+base36 >= 0.1.1
+pyglm >= 2.7.1
+numpy >= 1.26.2
+imgui[pyglet] >= 2.0.0
+pillow >= 9.3.0
+PyOpenGL >= 3.1.6
\ No newline at end of file
diff --git a/community/save.py b/community/save.py
index 3483accb..cb4c301f 100644
--- a/community/save.py
+++ b/community/save.py
@@ -6,7 +6,7 @@
import glm
class Save:
- def __init__(self, world, path = "save"):
+ def __init__(self, world, path):
self.world = world
self.path = path
diff --git a/community/start.bat b/community/start.bat
deleted file mode 100644
index 863f00b5..00000000
--- a/community/start.bat
+++ /dev/null
@@ -1,6 +0,0 @@
-py -m pip install --upgrade pyglet
-py -m pip install --upgrade nbtlib
-py -m pip install --upgrade base36
-py -m pip install --upgrade pyglm
-py -m pip install --upgrade numpy
-py main.py
\ No newline at end of file
diff --git a/community/texture_manager.py b/community/texture_manager.py
index 5088997b..f3e6d826 100644
--- a/community/texture_manager.py
+++ b/community/texture_manager.py
@@ -3,6 +3,18 @@
import logging
import pyglet.gl as gl
+from PIL import Image
+import OpenGL.GL as pgl
+
+
+class GLImage:
+ def __init__(self, gl_id, width, height, uv0, uv1) -> None:
+ self.id = gl_id
+ self.width = width
+ self.height = height
+ self.uv0 = uv0
+ self.uv1 = uv1
+
class TextureManager:
def __init__(self, texture_width, texture_height, max_textures):
@@ -43,4 +55,51 @@ def add_texture(self, texture):
0, 0, self.textures.index(texture),
self.texture_width, self.texture_height, 1,
gl.GL_RGBA, gl.GL_UNSIGNED_BYTE,
- texture_image.get_data("RGBA", texture_image.width * 4))
\ No newline at end of file
+ texture_image.get_data("RGBA", texture_image.width * 4))
+
+ def load_texture(self, texture, use_pyglet_gl = True):
+ logging.debug(f"Loading texture textures/{texture}.png")
+
+ if use_pyglet_gl:
+ image = pyglet.image.load(f"textures/{texture}.png")
+ width = image.width
+ height = image.height
+
+ return GLImage(image.get_texture().id, width, height, (0, 1), (1, 0))
+ else:
+ # Load the image using PIL
+ image = Image.open(f"textures/{texture}.png")
+ img_data = list(image.getdata())
+
+ # Create a texture ID
+ texture_id = pgl.glGenTextures(1)
+
+ # Bind the texture
+ pgl.glBindTexture(pgl.GL_TEXTURE_2D, texture_id)
+
+ # Set texture parameters
+ pgl.glTexParameteri(pgl.GL_TEXTURE_2D, pgl.GL_TEXTURE_MIN_FILTER, pgl.GL_LINEAR)
+ pgl.glTexParameteri(pgl.GL_TEXTURE_2D, pgl.GL_TEXTURE_MAG_FILTER, pgl.GL_LINEAR)
+
+ # Convert image data to bytes
+ img_bytes = []
+ for rgba in img_data:
+ img_bytes.extend(rgba)
+
+ # Upload the texture data
+ pgl.glTexImage2D(
+ pgl.GL_TEXTURE_2D,
+ 0,
+ pgl.GL_RGBA,
+ image.width,
+ image.height,
+ 0,
+ pgl.GL_RGBA,
+ pgl.GL_UNSIGNED_BYTE,
+ (pgl.GLubyte * len(img_bytes))(*img_bytes)
+ )
+
+ # Unbind the texture
+ pgl.glBindTexture(pgl.GL_TEXTURE_2D, 0)
+
+ return GLImage(texture_id, image.width, image.height, (0, -1), (1, 0))
diff --git a/community/textures/logo.png b/community/textures/logo.png
new file mode 100644
index 00000000..176f3b58
Binary files /dev/null and b/community/textures/logo.png differ
diff --git a/community/world.py b/community/world.py
index f9b2b8f1..e8659b9a 100644
--- a/community/world.py
+++ b/community/world.py
@@ -35,7 +35,7 @@ def get_local_position(position):
class World:
- def __init__(self, shader, player, texture_manager, options):
+ def __init__(self, path, shader, player, texture_manager, options):
self.options = options
self.shader = shader
self.player = player
@@ -134,7 +134,7 @@ def __init__(self, shader, player, texture_manager, options):
# load the world
- self.save = save.Save(self)
+ self.save = save.Save(self, path)
self.chunks = {}
self.sorted_chunks = []
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 00000000..e17955f6
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,5 @@
+pyglet >= 2.0.10
+nbtlib >= 2.0.4
+base36 >= 0.1.1
+pyglm >= 2.7.1
+numpy >= 1.26.2