diff --git a/.github/workflows/build-test-publish.yml b/.github/workflows/build-test-publish.yml
index 3637e750d..07d089794 100644
--- a/.github/workflows/build-test-publish.yml
+++ b/.github/workflows/build-test-publish.yml
@@ -55,9 +55,9 @@ jobs:
           - python-version: "3.10"
             py-bin: cp310-cp310
             platform: manylinux_2_28_x86_64
-          # - python-version: "3.11"
-          #   py-bin: cp311-cp311
-          #   platform: manylinux_2_28_x86_64
+          - python-version: "3.11"
+            py-bin: cp311-cp311
+            platform: manylinux_2_28_x86_64
           - python-version: "3.8"
             py-bin: cp38-cp38
             platform: manylinux2014_x86_64
@@ -67,6 +67,9 @@ jobs:
           - python-version: "3.10"
             py-bin: cp310-cp310
             platform: manylinux2014_x86_64
+          - python-version: "3.11"
+            py-bin: cp311-cp311
+            platform: manylinux2014_x86_64
     container:
       image: quay.io/pypa/${{ matrix.manylinux_config.platform }}
       env:
@@ -136,6 +139,7 @@ jobs:
           - python-version: "3.8"
           - python-version: "3.9"
           - python-version: "3.10"
+          - python-version: "3.11"
 
     steps:
       # Checkout the repository
@@ -201,6 +205,7 @@ jobs:
           - python-version: "3.8"
           - python-version: "3.9"
           - python-version: "3.10"
+          - python-version: "3.11"
 
     steps:
       # Checkout the repository
diff --git a/.gitignore b/.gitignore
index 267702a27..43ae9e4fb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -61,7 +61,6 @@ tests/vendor/*
 !tests/vendor/CMakeLists.txt*
 
 # Vulkan
-*.spv
 *.ppm
 
 # Idea
diff --git a/compile_shaders.bat b/compile_shaders.bat
index 07ac423a2..2f094ce0e 100644
--- a/compile_shaders.bat
+++ b/compile_shaders.bat
@@ -7,9 +7,15 @@ CALL :compile_shaders_in_dir .\tests\resources\observer\block\shaders\global_lig
 CALL :compile_shaders_in_dir .\tests\resources\observer\isometric\shaders\lighting
 CALL :compile_shaders_in_dir .\tests\resources\observer\sprite\shaders\health_bars
 
+CALL :compile_shaders_in_dir ".\python\examples\Custom Shaders\Global Lighting\shaders"
+CALL :compile_shaders_in_dir ".\python\examples\Custom Shaders\Health Bars\shaders"
+CALL :compile_shaders_in_dir ".\python\examples\Custom Shaders\Object Lighting\shaders"
+
+
+
 EXIT /B 0
 
 :compile_shaders_in_dir
 echo "Compiling shaders in %~1"
-%GLSLC_BIN% %~1\triangle-textured.frag -o %~1\triangle-textured.frag.spv
-%GLSLC_BIN% %~1\triangle-textured.vert -o %~1\triangle-textured.vert.spv
+%GLSLC_BIN% "%~1\triangle-textured.frag" -o "%~1\triangle-textured.frag.spv"
+%GLSLC_BIN% "%~1\triangle-textured.vert" -o "%~1\triangle-textured.vert.spv"
diff --git a/compile_shaders.sh b/compile_shaders.sh
index 67225c0cb..0f0f9f46e 100755
--- a/compile_shaders.sh
+++ b/compile_shaders.sh
@@ -4,8 +4,8 @@ cd "$(dirname "$0")"
 
 compile_shaders_in_dir () {
   echo "Compiling shaders in $1"
-  $GLSLC_BIN $1/triangle-textured.frag -o $1/triangle-textured.frag.spv
-  $GLSLC_BIN $1/triangle-textured.vert -o $1/triangle-textured.vert.spv
+  $GLSLC_BIN "$1/triangle-textured.frag" -o "$1/triangle-textured.frag.spv"
+  $GLSLC_BIN "$1/triangle-textured.vert" -o "$1/triangle-textured.vert.spv"
 }
 
 compile_shaders_in_dir ./resources/shaders/default/block
@@ -14,3 +14,8 @@ compile_shaders_in_dir ./resources/shaders/default/isometric
 compile_shaders_in_dir ./tests/resources/observer/block/shaders/global_lighting
 compile_shaders_in_dir ./tests/resources/observer/isometric/shaders/lighting
 compile_shaders_in_dir ./tests/resources/observer/sprite/shaders/health_bars
+
+
+compile_shaders_in_dir "./python/examples/Custom Shaders/Global Lighting/shaders"
+compile_shaders_in_dir "./python/examples/Custom Shaders/Health Bars/shaders"
+compile_shaders_in_dir "./python/examples/Custom Shaders/Object Lighting/shaders"
diff --git a/docs/getting-started/action spaces/index.rst b/docs/getting-started/action spaces/index.rst
index 2c325d411..faa747883 100644
--- a/docs/getting-started/action spaces/index.rst	
+++ b/docs/getting-started/action spaces/index.rst	
@@ -230,19 +230,6 @@ Lets say our RTS game has units that have an action ``move`` and an action ``gat
 
 .. code-block:: python
 
-  # env.step([
-  #   [ # List of actions for player 1
-  #     [x1, y1, action_type1, action_id1],
-  #     [x2, y2, action_type2, action_id2],
-  #     ...
-  #   ], 
-  #   [ # List of actions for player 2
-  #     [x1, y1, action_type1, action_id1],
-  #     [x2, y2, action_type2, action_id2],
-  #     ..
-  #   ],
-  # ])
-
   env.step([
     # Player 1
     [ 
diff --git a/docs/getting-started/observation spaces/index.rst b/docs/getting-started/observation spaces/index.rst
index 03c0acd50..36fe0713a 100644
--- a/docs/getting-started/observation spaces/index.rst	
+++ b/docs/getting-started/observation spaces/index.rst	
@@ -40,7 +40,7 @@ The observations for environments where a single avatar is being controlled are
 
   # obs = np.array([ ... ]) # Player observation
 
-  obs, reward, done, info = env.step( ... )
+  obs, reward, done, truncated, info = env.step( ... )
 
   # obs = np.array([ ... ]) # Player observation
 
diff --git a/python/examples/AStar Search/main.py b/python/examples/AStar Search/main.py
index ed9ae5d50..c43a64f34 100644
--- a/python/examples/AStar Search/main.py	
+++ b/python/examples/AStar Search/main.py	
@@ -1,23 +1,18 @@
-from griddly import gd, gym
+from griddly import gd
+from griddly.gym import GymWrapper
 from griddly.util.render_tools import RenderToVideo
 from griddly.wrappers.render_wrapper import RenderWrapper
 
 if __name__ == "__main__":
-    # Uncommment to see normal actions (not rotated) being used
-
+    # Astar action space: 0: up, 1: down, 2: left, 3: right
     # env = GymWrapper('astar_opponent_environment.yaml',
     #                  player_observer_type=gd.ObserverType.VECTOR,
     #                  global_observer_type=gd.ObserverType.SPRITE_2D,
     #                  level=0)
 
-    # env = GymWrapper('astar_opponent_rotation_actions_environment.yaml',
-    #                  player_observer_type=gd.ObserverType.VECTOR,
-    #                  global_observer_type=gd.ObserverType.SPRITE_2D,
-    #                  level=0)
-
-    # Uncommment to see multiple spiders chasing!
-    env = gym(
-        "astar_opponent_rotation_actions_environment.yaml",
+    # Astar action space: 1: Rotate left, 2: Move forward, 3: Rotate right
+    env = GymWrapper(
+        "./astar_opponent_rotation_actions_environment.yaml",
         player_observer_type=gd.ObserverType.VECTOR,
         global_observer_type=gd.ObserverType.SPRITE_2D,
         level=1,
diff --git a/python/examples/Custom Shaders/Global Lighting/main.py b/python/examples/Custom Shaders/Global Lighting/main.py
index a2300bc98..98d9ceb34 100644
--- a/python/examples/Custom Shaders/Global Lighting/main.py	
+++ b/python/examples/Custom Shaders/Global Lighting/main.py	
@@ -1,9 +1,10 @@
-from griddly import gd, gym
+from griddly import gd
+from griddly.gym import GymWrapper
 from griddly.util.render_tools import RenderToFile, RenderToVideo
 from griddly.wrappers.render_wrapper import RenderWrapper
 
 if __name__ == "__main__":
-    env = gym(
+    env = GymWrapper(
         "global_lighting.yaml",
         player_observer_type=gd.ObserverType.SPRITE_2D,
         global_observer_type=gd.ObserverType.SPRITE_2D,
diff --git a/python/examples/Custom Shaders/Global Lighting/shaders/compile_shaders.sh b/python/examples/Custom Shaders/Global Lighting/shaders/compile_shaders.sh
deleted file mode 100755
index 924df3f31..000000000
--- a/python/examples/Custom Shaders/Global Lighting/shaders/compile_shaders.sh	
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/bin/bash
-
-SHADER_OUTPUT_DIR=.
-
-if [ ! -d $SHADER_OUTPUT_DIR ]; then
-  mkdir -p $SHADER_OUTPUT_DIR;
-fi
-
-glslc triangle-textured.frag -o $SHADER_OUTPUT_DIR/triangle-textured.frag.spv
-glslc triangle-textured.vert -o $SHADER_OUTPUT_DIR/triangle-textured.vert.spv
\ No newline at end of file
diff --git a/python/examples/Custom Shaders/Global Lighting/shaders/triangle-textured.frag.spv b/python/examples/Custom Shaders/Global Lighting/shaders/triangle-textured.frag.spv
new file mode 100644
index 000000000..c4c3ff469
Binary files /dev/null and b/python/examples/Custom Shaders/Global Lighting/shaders/triangle-textured.frag.spv differ
diff --git a/python/examples/Custom Shaders/Global Lighting/shaders/triangle-textured.vert.spv b/python/examples/Custom Shaders/Global Lighting/shaders/triangle-textured.vert.spv
new file mode 100644
index 000000000..9a3af720a
Binary files /dev/null and b/python/examples/Custom Shaders/Global Lighting/shaders/triangle-textured.vert.spv differ
diff --git a/python/examples/Custom Shaders/Health Bars/main.py b/python/examples/Custom Shaders/Health Bars/main.py
index 885ab0458..cab31a3ee 100644
--- a/python/examples/Custom Shaders/Health Bars/main.py	
+++ b/python/examples/Custom Shaders/Health Bars/main.py	
@@ -1,9 +1,10 @@
-from griddly import gd, gym
+from griddly import gd
+from griddly.gym import GymWrapper
 from griddly.util.render_tools import RenderToFile, RenderToVideo
 from griddly.wrappers.render_wrapper import RenderWrapper
 
 if __name__ == "__main__":
-    env = gym(
+    env = GymWrapper(
         "health_bars.yaml",
         player_observer_type=gd.ObserverType.SPRITE_2D,
         global_observer_type=gd.ObserverType.SPRITE_2D,
diff --git a/python/examples/Custom Shaders/Health Bars/reset_global.png b/python/examples/Custom Shaders/Health Bars/reset_global.png
index 80bb7121b..7971c126b 100644
Binary files a/python/examples/Custom Shaders/Health Bars/reset_global.png and b/python/examples/Custom Shaders/Health Bars/reset_global.png differ
diff --git a/python/examples/Custom Shaders/Health Bars/shaders/compile_shaders.sh b/python/examples/Custom Shaders/Health Bars/shaders/compile_shaders.sh
deleted file mode 100755
index 924df3f31..000000000
--- a/python/examples/Custom Shaders/Health Bars/shaders/compile_shaders.sh	
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/bin/bash
-
-SHADER_OUTPUT_DIR=.
-
-if [ ! -d $SHADER_OUTPUT_DIR ]; then
-  mkdir -p $SHADER_OUTPUT_DIR;
-fi
-
-glslc triangle-textured.frag -o $SHADER_OUTPUT_DIR/triangle-textured.frag.spv
-glslc triangle-textured.vert -o $SHADER_OUTPUT_DIR/triangle-textured.vert.spv
\ No newline at end of file
diff --git a/python/examples/Custom Shaders/Health Bars/shaders/triangle-textured.frag.spv b/python/examples/Custom Shaders/Health Bars/shaders/triangle-textured.frag.spv
new file mode 100644
index 000000000..73077fc64
Binary files /dev/null and b/python/examples/Custom Shaders/Health Bars/shaders/triangle-textured.frag.spv differ
diff --git a/python/examples/Custom Shaders/Health Bars/shaders/triangle-textured.vert.spv b/python/examples/Custom Shaders/Health Bars/shaders/triangle-textured.vert.spv
new file mode 100644
index 000000000..38587416a
Binary files /dev/null and b/python/examples/Custom Shaders/Health Bars/shaders/triangle-textured.vert.spv differ
diff --git a/python/examples/Custom Shaders/Object Lighting/main.py b/python/examples/Custom Shaders/Object Lighting/main.py
index ec43a4ddf..f0d3c8b2e 100644
--- a/python/examples/Custom Shaders/Object Lighting/main.py	
+++ b/python/examples/Custom Shaders/Object Lighting/main.py	
@@ -1,9 +1,10 @@
-from griddly import gd, gym
+from griddly import gd
+from griddly.gym import GymWrapper
 from griddly.util.render_tools import RenderToFile, RenderToVideo
 from griddly.wrappers.render_wrapper import RenderWrapper
 
 if __name__ == "__main__":
-    env = gym(
+    env = GymWrapper(
         "object_lighting.yaml",
         player_observer_type=gd.ObserverType.SPRITE_2D,
         global_observer_type=gd.ObserverType.SPRITE_2D,
diff --git a/python/examples/Custom Shaders/Object Lighting/reset_global.png b/python/examples/Custom Shaders/Object Lighting/reset_global.png
index 545eb253b..16c299316 100644
Binary files a/python/examples/Custom Shaders/Object Lighting/reset_global.png and b/python/examples/Custom Shaders/Object Lighting/reset_global.png differ
diff --git a/python/examples/Custom Shaders/Object Lighting/reset_partial.png b/python/examples/Custom Shaders/Object Lighting/reset_partial.png
index 30b316f71..452e88d7a 100644
Binary files a/python/examples/Custom Shaders/Object Lighting/reset_partial.png and b/python/examples/Custom Shaders/Object Lighting/reset_partial.png differ
diff --git a/python/examples/Custom Shaders/Object Lighting/shaders/compile_shaders.sh b/python/examples/Custom Shaders/Object Lighting/shaders/compile_shaders.sh
deleted file mode 100755
index 924df3f31..000000000
--- a/python/examples/Custom Shaders/Object Lighting/shaders/compile_shaders.sh	
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/bin/bash
-
-SHADER_OUTPUT_DIR=.
-
-if [ ! -d $SHADER_OUTPUT_DIR ]; then
-  mkdir -p $SHADER_OUTPUT_DIR;
-fi
-
-glslc triangle-textured.frag -o $SHADER_OUTPUT_DIR/triangle-textured.frag.spv
-glslc triangle-textured.vert -o $SHADER_OUTPUT_DIR/triangle-textured.vert.spv
\ No newline at end of file
diff --git a/python/examples/Custom Shaders/Object Lighting/shaders/triangle-textured.frag.spv b/python/examples/Custom Shaders/Object Lighting/shaders/triangle-textured.frag.spv
new file mode 100644
index 000000000..4ec4314c7
Binary files /dev/null and b/python/examples/Custom Shaders/Object Lighting/shaders/triangle-textured.frag.spv differ
diff --git a/python/examples/Custom Shaders/Object Lighting/shaders/triangle-textured.vert.spv b/python/examples/Custom Shaders/Object Lighting/shaders/triangle-textured.vert.spv
new file mode 100644
index 000000000..da7adf164
Binary files /dev/null and b/python/examples/Custom Shaders/Object Lighting/shaders/triangle-textured.vert.spv differ
diff --git a/python/examples/Custom Shaders/README.md b/python/examples/Custom Shaders/README.md
index 3492dca02..207209d22 100644
--- a/python/examples/Custom Shaders/README.md	
+++ b/python/examples/Custom Shaders/README.md	
@@ -2,21 +2,20 @@
 
 In this directory are a few examples of how to make use of custom shaders to modify the standard pixel output for more complex environnments.
 
-
 ## Global Lighting
 
-We use the episode step timer `_steps` to create a day/night cycle! 
+We use the episode step timer `_steps` to create a day/night cycle!
 
 ![Global Lighting](Global%20Lighting/global_lighting.gif)
 
 ## Health Bars
 
-In this example we show health bars on individual units in a combat scenario. 
+In this example we show health bars on individual units in a combat scenario.
 
 ![Health Bars](Health%20Bars/health_bars.gif)
 
-## Object Lighting 
+## Object Lighting
 
 Add partial observability to agents by only lighting up the immediate area around the agent and the goal
 
-![Object Lighting](Object%20Lighting/object_lighting.gif)
\ No newline at end of file
+![Object Lighting](Object%20Lighting/object_lighting.gif)
diff --git a/python/examples/Level Design/custom_level_global.png b/python/examples/Level Design/custom_level_global.png
index db3a9b540..15cc1fdc7 100644
Binary files a/python/examples/Level Design/custom_level_global.png and b/python/examples/Level Design/custom_level_global.png differ
diff --git a/python/examples/Level Design/custom_level_string_player_1.png b/python/examples/Level Design/custom_level_string_player_1.png
index bf3ad70d4..24b7843ef 100644
Binary files a/python/examples/Level Design/custom_level_string_player_1.png and b/python/examples/Level Design/custom_level_string_player_1.png differ
diff --git a/python/examples/Level Design/custom_level_string_player_2.png b/python/examples/Level Design/custom_level_string_player_2.png
index 31b879e9f..b5ee7f024 100644
Binary files a/python/examples/Level Design/custom_level_string_player_2.png and b/python/examples/Level Design/custom_level_string_player_2.png differ
diff --git a/python/examples/Level Design/level_0_global.png b/python/examples/Level Design/level_0_global.png
index 181b2d4c3..5c9e9bc1b 100644
Binary files a/python/examples/Level Design/level_0_global.png and b/python/examples/Level Design/level_0_global.png differ
diff --git a/python/examples/Level Design/level_1_global.png b/python/examples/Level Design/level_1_global.png
index a80da7baf..e44d937d7 100644
Binary files a/python/examples/Level Design/level_1_global.png and b/python/examples/Level Design/level_1_global.png differ
diff --git a/python/examples/Level Design/level_1_player_1.png b/python/examples/Level Design/level_1_player_1.png
index 8e3c94ef1..b19b56ec0 100644
Binary files a/python/examples/Level Design/level_1_player_1.png and b/python/examples/Level Design/level_1_player_1.png differ
diff --git a/python/examples/Level Design/level_1_player_2.png b/python/examples/Level Design/level_1_player_2.png
index 20cbe0687..2cb0b0232 100644
Binary files a/python/examples/Level Design/level_1_player_2.png and b/python/examples/Level Design/level_1_player_2.png differ
diff --git a/python/examples/Level Design/level_2_global.png b/python/examples/Level Design/level_2_global.png
index db3a9b540..3acb4dd84 100644
Binary files a/python/examples/Level Design/level_2_global.png and b/python/examples/Level Design/level_2_global.png differ
diff --git a/python/examples/Level Design/level_2_player_1.png b/python/examples/Level Design/level_2_player_1.png
index bf3ad70d4..f81c2ec40 100644
Binary files a/python/examples/Level Design/level_2_player_1.png and b/python/examples/Level Design/level_2_player_1.png differ
diff --git a/python/examples/Level Design/level_2_player_2.png b/python/examples/Level Design/level_2_player_2.png
index 31b879e9f..f7e7fdf5a 100644
Binary files a/python/examples/Level Design/level_2_player_2.png and b/python/examples/Level Design/level_2_player_2.png differ
diff --git a/python/examples/Level Design/main.py b/python/examples/Level Design/main.py
index c90ab9d9c..0592f3146 100644
--- a/python/examples/Level Design/main.py	
+++ b/python/examples/Level Design/main.py	
@@ -1,9 +1,10 @@
-from griddly import gd, gym
+from griddly import gd
+from griddly.gym import GymWrapper
 from griddly.util.render_tools import RenderToFile
 from griddly.wrappers.render_wrapper import RenderWrapper
 
 if __name__ == "__main__":
-    env = gym(
+    env = GymWrapper(
         "levels.yaml",
         player_observer_type=gd.ObserverType.BLOCK_2D,
         global_observer_type=gd.ObserverType.BLOCK_2D,
@@ -26,13 +27,13 @@
             image_renderer.render(obs, f"level_{i}_player_2.png")
 
     level_string = \
-""". c c c c c c . . c c c c c c . . c c . c c c c c c . . c c c c c c . . c c . . . . . . c c . . . . c c .
-c c . . . . . . . c c . . . c c . c c . c c . . . c c . c c . . . c c . c c . . . . . . . c c . . c c . .
-c c . . . c c c . c c c c c c . . c c . c c . . . c c . c c . . . c c . c c . . . . . . . . c c c c . . .
-c c . . . . c c . c c . . . c c . c c . c c . . . c c . c c . . . c c . c c . . . . . . . . . c c . . . .
-. c c c c c c . . c c . . . c c . c c . c c c c c c . . c c c c c c . . c c c c c c c . . . . c c . . . .
+""". c c c c c c . . c c c c c c . . c c . c1 c1 c1 c1 c1 c1 . . c2 c2 c2 c2 c2 c2 . . c c . . . . . . c c . . . . c c .
+c c . . . . . . . c c . . . c c . c c . c1 c1 .  .  . c1 c1 . c2 c2 .  .  . c2 c2 . c c . . . . . . . c c . . c c . .
+c c . . . c c c . c c c c c c . . c c . c1 c1 .  .  . c1 c1 . c2 c2 .  .  . c2 c2 . c c . . . . . . . . c c c c . . .
+c c . . . . c c . c c . . . c c . c c . c1 c1 .  .  . c1 c1 . c2 c2 .  .  . c2 c2 . c c . . . . . . . . . c c . . . .
+. c c c c c c . . c c . . . c c . c c . c1 c1 c1 c1 c1 c1 . . c2 c2 c2 c2 c2 c2 . . c c c c c c c . . . . c c . . . .
  """
-    env.reset(options={level_string: level_string})
+    env.reset(options={"level_string": level_string})
     obs = global_obs_render_wrapper.render()
     image_renderer.render(obs, "custom_level_global.png")
     obs = player_1_obs_render_wrapper.render()
diff --git a/python/examples/Procedural Generation/example_maze_13x13_1.png b/python/examples/Procedural Generation/example_maze_13x13_1.png
index 751f05071..c650ddedd 100644
Binary files a/python/examples/Procedural Generation/example_maze_13x13_1.png and b/python/examples/Procedural Generation/example_maze_13x13_1.png differ
diff --git a/python/examples/Procedural Generation/example_maze_13x13_2.png b/python/examples/Procedural Generation/example_maze_13x13_2.png
index 5f8d7438f..1fd08eb30 100644
Binary files a/python/examples/Procedural Generation/example_maze_13x13_2.png and b/python/examples/Procedural Generation/example_maze_13x13_2.png differ
diff --git a/python/examples/Procedural Generation/example_maze_13x13_3.png b/python/examples/Procedural Generation/example_maze_13x13_3.png
index ed7bccfdc..234a78f9b 100644
Binary files a/python/examples/Procedural Generation/example_maze_13x13_3.png and b/python/examples/Procedural Generation/example_maze_13x13_3.png differ
diff --git a/python/examples/Procedural Generation/example_maze_21x21_1.png b/python/examples/Procedural Generation/example_maze_21x21_1.png
index 98cd741d8..6c5f4e1dc 100644
Binary files a/python/examples/Procedural Generation/example_maze_21x21_1.png and b/python/examples/Procedural Generation/example_maze_21x21_1.png differ
diff --git a/python/examples/Procedural Generation/example_maze_21x21_2.png b/python/examples/Procedural Generation/example_maze_21x21_2.png
index d9e4af1a7..d0ad08327 100644
Binary files a/python/examples/Procedural Generation/example_maze_21x21_2.png and b/python/examples/Procedural Generation/example_maze_21x21_2.png differ
diff --git a/python/examples/Procedural Generation/example_maze_21x21_3.png b/python/examples/Procedural Generation/example_maze_21x21_3.png
index 5186186ec..d2370d95a 100644
Binary files a/python/examples/Procedural Generation/example_maze_21x21_3.png and b/python/examples/Procedural Generation/example_maze_21x21_3.png differ
diff --git a/python/examples/Procedural Generation/example_maze_45x45_1.png b/python/examples/Procedural Generation/example_maze_45x45_1.png
index c09529433..2120d69ab 100644
Binary files a/python/examples/Procedural Generation/example_maze_45x45_1.png and b/python/examples/Procedural Generation/example_maze_45x45_1.png differ
diff --git a/python/examples/Procedural Generation/example_maze_45x45_2.png b/python/examples/Procedural Generation/example_maze_45x45_2.png
index 9e56291ad..ffed4eaf8 100644
Binary files a/python/examples/Procedural Generation/example_maze_45x45_2.png and b/python/examples/Procedural Generation/example_maze_45x45_2.png differ
diff --git a/python/examples/Procedural Generation/example_maze_45x45_3.png b/python/examples/Procedural Generation/example_maze_45x45_3.png
index f308bd4d0..4e735f2a9 100644
Binary files a/python/examples/Procedural Generation/example_maze_45x45_3.png and b/python/examples/Procedural Generation/example_maze_45x45_3.png differ
diff --git a/python/examples/Procedural Generation/main.py b/python/examples/Procedural Generation/main.py
index 2e1ffbaa3..c05180d36 100644
--- a/python/examples/Procedural Generation/main.py	
+++ b/python/examples/Procedural Generation/main.py	
@@ -1,16 +1,21 @@
-import gym
+from typing import Any, Dict
+
+import gymnasium as gym
 import numpy as np
+import numpy.typing as npt
 
-from griddly.util.rllib.environment.level_generator import LevelGenerator
+from griddly.gym import GymWrapper
+from griddly.util.render_tools import RenderToFile
+from griddly.wrappers.render_wrapper import RenderWrapper
 
 
-class LabyrinthLevelGenerator(LevelGenerator):
+class LabyrinthLevelGenerator:
     WALL = "w"
     AGENT = "A"
     GOAL = "x"
     EMPTY = "."
 
-    def __init__(self, config):
+    def __init__(self, config: Dict[str, Any]) -> None:
         """
         Initialize the LabyrinthLevelGenerator.
 
@@ -35,9 +40,8 @@ def __init__(self, config):
         env.reset(level_string=level_string)
         """
 
-        super().__init__(config)
-        self._width = config.get("width", 9)
-        self._height = config.get("height", 9)
+        self._width: int = config.get("width", 9)
+        self._height: int = config.get("height", 9)
         self._wall_density = config.get(
             "wall_density", 1
         )  # Adjust this value to control wall density
@@ -46,7 +50,7 @@ def __init__(self, config):
 
         self._num_goals = config.get("num_goals", 1)
 
-    def _generate_maze(self):
+    def _generate_maze(self) -> npt.NDArray:
         """
         Generate the maze grid.
 
@@ -64,7 +68,7 @@ def _generate_maze(self):
         )
 
         # Recursive Backtracking algorithm for generating maze paths
-        def recursive_backtracking(x, y):
+        def recursive_backtracking(x: int, y: int) -> None:
             maze[x, y] = LabyrinthLevelGenerator.EMPTY
 
             # Randomize the order of directions to explore
@@ -99,7 +103,7 @@ def recursive_backtracking(x, y):
 
         return maze
 
-    def _is_reachable(self, maze, x, y):
+    def _is_reachable(self, maze: npt.NDArray, x: int, y: int) -> bool:
         """
         Check if a tile is reachable from the agent's starting position using a flood-fill algorithm.
 
@@ -133,11 +137,16 @@ def _is_reachable(self, maze, x, y):
                 ):
                     stack.append((nx, ny))
 
-        return len(visited) == self._width * self._height - np.sum(
+        len_visited = len(visited)
+        not_walls: int = self._width * self._height - np.sum(
             maze == LabyrinthLevelGenerator.WALL
         )
 
-    def _place_goals(self, maze, agent_x, agent_y):
+        return len_visited == not_walls
+
+    def _place_goals(
+        self, maze: npt.NDArray, agent_x: int, agent_y: int
+    ) -> npt.NDArray:
         """
         Place the goals in the maze while ensuring they don't block the agent's access to all tiles.
 
@@ -167,7 +176,7 @@ def _place_goals(self, maze, agent_x, agent_y):
 
         return maze
 
-    def generate(self):
+    def generate(self) -> str:
         """
         Generate a new maze level.
 
@@ -204,11 +213,15 @@ def generate(self):
 
 
 if __name__ == "__main__":
-    import matplotlib.pyplot as plt
-
+    # Use the mechanics from the built in GDY-Labyrinth-v0 environment
     env = gym.make("GDY-Labyrinth-v0")
     sizes = ["45x45", "21x21", "13x13"]
 
+    assert isinstance(env, GymWrapper)
+
+    image_renderer = RenderToFile()
+    global_obs_render_wrapper = RenderWrapper(env, "global", "rgb_array")
+
     for size in sizes:
         for i in range(3):
             config = {
@@ -217,9 +230,7 @@ def generate(self):
             }
 
             level_generator = LabyrinthLevelGenerator(config)
-            env.reset(level_string=level_generator.generate())
+            env.reset(options={"level_string": level_generator.generate()})
 
-            obs = env.render(mode="rgb_array")
-            plt.figure()
-            plt.imshow(obs)
-            plt.savefig(f"example_maze_{size}_{i+1}.png")
+            obs = global_obs_render_wrapper.render()
+            image_renderer.render(obs, f"example_maze_{size}_{i+1}.png")
diff --git a/python/examples/Projectiles/main.py b/python/examples/Projectiles/main.py
index 048c1cea4..b51b902f8 100644
--- a/python/examples/Projectiles/main.py
+++ b/python/examples/Projectiles/main.py
@@ -1,9 +1,10 @@
-from griddly import gd, gym
+from griddly import gd
+from griddly.gym import GymWrapper
 from griddly.util.render_tools import RenderToVideo
 from griddly.wrappers.render_wrapper import RenderWrapper
 
 if __name__ == "__main__":
-    env = gym(
+    env = GymWrapper(
         "projectiles.yaml",
         player_observer_type=gd.ObserverType.ISOMETRIC,
         global_observer_type=gd.ObserverType.ISOMETRIC,
diff --git a/python/examples/Proximity/main.py b/python/examples/Proximity/main.py
index cc8ee2caf..4c6079ab8 100644
--- a/python/examples/Proximity/main.py
+++ b/python/examples/Proximity/main.py
@@ -1,9 +1,10 @@
-from griddly import gd, gym
+from griddly import gd
+from griddly.gym import GymWrapper
 from griddly.util.render_tools import RenderToVideo
 from griddly.wrappers.render_wrapper import RenderWrapper
 
 if __name__ == "__main__":
-    env = gym(
+    env = GymWrapper(
         "proximity.yaml",
         player_observer_type=gd.ObserverType.ISOMETRIC,
         global_observer_type=gd.ObserverType.ISOMETRIC,
diff --git a/python/examples/Stochasticity/main.py b/python/examples/Stochasticity/main.py
index ed7d80964..a393f992c 100644
--- a/python/examples/Stochasticity/main.py
+++ b/python/examples/Stochasticity/main.py
@@ -1,9 +1,10 @@
-from griddly import gd, gym
+from griddly import gd
+from griddly.gym import GymWrapper
 from griddly.util.render_tools import RenderToVideo
 from griddly.wrappers.render_wrapper import RenderWrapper
 
 if __name__ == "__main__":
-    env = gym(
+    env = GymWrapper(
         "stochasticity.yaml",
         player_observer_type=gd.ObserverType.SPRITE_2D,
         global_observer_type=gd.ObserverType.SPRITE_2D,
diff --git a/python/griddly/gym.py b/python/griddly/gym.py
index 2ba3b12cc..007fe687e 100644
--- a/python/griddly/gym.py
+++ b/python/griddly/gym.py
@@ -585,14 +585,20 @@ def render_observer(
                 return ascii_string
 
         if render_mode == "human":
-            assert isinstance(observer, int)
-            if self._render_window.get(observer) is None:
+            render_window_key: int
+            if observer == "global":
+                render_window_key = -1
+            else:
+                assert isinstance(observer, int)
+                render_window_key = observer
+
+            if self._render_window.get(render_window_key) is None:
                 from griddly.util.render_tools import RenderToWindow
 
-                self._render_window[observer] = RenderToWindow(
+                self._render_window[render_window_key] = RenderToWindow(
                     observation.shape[1], observation.shape[2]
                 )
-            self._render_window[observer].render(observation)
+            self._render_window[render_window_key].render(observation)
 
         return observation.swapaxes(0, 2)
 
diff --git a/python/griddly/util/render_tools.py b/python/griddly/util/render_tools.py
index 5af44bc67..92031b9d3 100644
--- a/python/griddly/util/render_tools.py
+++ b/python/griddly/util/render_tools.py
@@ -7,7 +7,7 @@
 from griddly.wrappers.render_wrapper import RenderWrapper
 
 
-class RenderToVideo():
+class RenderToVideo:
     def __init__(
         self,
         env: RenderWrapper,
@@ -36,7 +36,6 @@ def close(self) -> None:
 
 class RenderToWindow:
     def __init__(self, width: int, height: int, caption: str = "Griddly") -> None:
-        super().__init__()
         self._width = width
         self._height = height
         self._caption = caption
@@ -92,8 +91,7 @@ def __del__(self) -> None:
 
 
 class RenderToFile:
-    def __init__(self) -> None:
-        super().__init__()
-
-    def render(self, observation: npt.NDArray, string_filename: str) -> None:
+    def render(
+        self, observation: Union[str, npt.NDArray], string_filename: str
+    ) -> None:
         imageio.imwrite(string_filename, observation)
diff --git a/python/tools/flamegraph/shader_benchmark/benchmark.py b/python/tools/flamegraph/shader_benchmark/benchmark.py
index da76a86e4..4deeb4dc8 100644
--- a/python/tools/flamegraph/shader_benchmark/benchmark.py
+++ b/python/tools/flamegraph/shader_benchmark/benchmark.py
@@ -1,11 +1,11 @@
 import timeit
 
-from griddly import GymWrapperFactory, gd, gym
+from griddly import GymWrapperFactory, gd
 
 if __name__ == "__main__":
     wrapper = GymWrapperFactory()
 
-    env = gym(
+    env = GymWrapper(
         "Single-Player/GVGAI/sokoban.yaml",
         player_observer_type=gd.ObserverType.BLOCK_2D,
         global_observer_type=gd.ObserverType.BLOCK_2D,
diff --git a/resources/shaders/default/block/triangle-textured.frag.spv b/resources/shaders/default/block/triangle-textured.frag.spv
new file mode 100644
index 000000000..0a7237b3f
Binary files /dev/null and b/resources/shaders/default/block/triangle-textured.frag.spv differ
diff --git a/resources/shaders/default/block/triangle-textured.vert.spv b/resources/shaders/default/block/triangle-textured.vert.spv
new file mode 100644
index 000000000..2a9a9ee4c
Binary files /dev/null and b/resources/shaders/default/block/triangle-textured.vert.spv differ
diff --git a/resources/shaders/default/isometric/triangle-textured.frag.spv b/resources/shaders/default/isometric/triangle-textured.frag.spv
new file mode 100644
index 000000000..f9c993088
Binary files /dev/null and b/resources/shaders/default/isometric/triangle-textured.frag.spv differ
diff --git a/resources/shaders/default/isometric/triangle-textured.vert.spv b/resources/shaders/default/isometric/triangle-textured.vert.spv
new file mode 100644
index 000000000..f28f7f39d
Binary files /dev/null and b/resources/shaders/default/isometric/triangle-textured.vert.spv differ
diff --git a/resources/shaders/default/sprite/triangle-textured.frag.spv b/resources/shaders/default/sprite/triangle-textured.frag.spv
new file mode 100644
index 000000000..0a7237b3f
Binary files /dev/null and b/resources/shaders/default/sprite/triangle-textured.frag.spv differ
diff --git a/resources/shaders/default/sprite/triangle-textured.vert.spv b/resources/shaders/default/sprite/triangle-textured.vert.spv
new file mode 100644
index 000000000..2a9a9ee4c
Binary files /dev/null and b/resources/shaders/default/sprite/triangle-textured.vert.spv differ
diff --git a/src/Griddly/Core/Observers/Vulkan/VulkanDevice.cpp b/src/Griddly/Core/Observers/Vulkan/VulkanDevice.cpp
index a2e72665b..c11a7f630 100644
--- a/src/Griddly/Core/Observers/Vulkan/VulkanDevice.cpp
+++ b/src/Griddly/Core/Observers/Vulkan/VulkanDevice.cpp
@@ -124,8 +124,14 @@ void VulkanDevice::initDevice(bool useGPU) {
     auto graphicsQueueFamilyIndex = physicalDeviceInfo->queueFamilyIndices.graphicsIndices;
     auto computeQueueFamilyIndex = physicalDeviceInfo->queueFamilyIndices.computeIndices;
 
+    const char* ppEnabledExtensionNames[] = {
+      "VK_KHR_portability_subset"
+    };
+
     auto deviceQueueCreateInfo = vk::initializers::deviceQueueCreateInfo(graphicsQueueFamilyIndex, 1.0f);
     auto deviceCreateInfo = vk::initializers::deviceCreateInfo(deviceQueueCreateInfo);
+    deviceCreateInfo.enabledExtensionCount = 1;
+    deviceCreateInfo.ppEnabledExtensionNames = ppEnabledExtensionNames;
 
     physicalDevice_ = physicalDeviceInfo->physicalDevice;
     spdlog::debug("Creating physical device.");
diff --git a/tests/resources/observer/block/shaders/global_lighting/triangle-textured.frag.spv b/tests/resources/observer/block/shaders/global_lighting/triangle-textured.frag.spv
new file mode 100644
index 000000000..51a668100
Binary files /dev/null and b/tests/resources/observer/block/shaders/global_lighting/triangle-textured.frag.spv differ
diff --git a/tests/resources/observer/block/shaders/global_lighting/triangle-textured.vert.spv b/tests/resources/observer/block/shaders/global_lighting/triangle-textured.vert.spv
new file mode 100644
index 000000000..2d0df2dae
Binary files /dev/null and b/tests/resources/observer/block/shaders/global_lighting/triangle-textured.vert.spv differ
diff --git a/tests/resources/observer/isometric/shaders/lighting/triangle-textured.frag.spv b/tests/resources/observer/isometric/shaders/lighting/triangle-textured.frag.spv
new file mode 100644
index 000000000..06d2ba365
Binary files /dev/null and b/tests/resources/observer/isometric/shaders/lighting/triangle-textured.frag.spv differ
diff --git a/tests/resources/observer/isometric/shaders/lighting/triangle-textured.vert.spv b/tests/resources/observer/isometric/shaders/lighting/triangle-textured.vert.spv
new file mode 100644
index 000000000..b53e60662
Binary files /dev/null and b/tests/resources/observer/isometric/shaders/lighting/triangle-textured.vert.spv differ
diff --git a/tests/resources/observer/sprite/shaders/health_bars/triangle-textured.frag.spv b/tests/resources/observer/sprite/shaders/health_bars/triangle-textured.frag.spv
new file mode 100644
index 000000000..7f1f920dc
Binary files /dev/null and b/tests/resources/observer/sprite/shaders/health_bars/triangle-textured.frag.spv differ
diff --git a/tests/resources/observer/sprite/shaders/health_bars/triangle-textured.vert.spv b/tests/resources/observer/sprite/shaders/health_bars/triangle-textured.vert.spv
new file mode 100644
index 000000000..94780bce8
Binary files /dev/null and b/tests/resources/observer/sprite/shaders/health_bars/triangle-textured.vert.spv differ