A 3D Game Engine written in Python

Ray Chen
4 min readOct 25, 2021

--

So lately I’ve been experimenting with my Game Engine called PyUnity, and I’ve been porting some things from the Unity GameEngine tutorials. As PyUnity is still WIP, there are a lot of unusable features (such as the half-finished physics engine). However, I have made the basics of the Roll A Ball tutorial. Here is the complete code:

This is an extremely long piece of code, isn’t it? Let’s break it down.

GameObjects and Transforms

A GameObject is an individual object in your Scene. It has a name, a tag and some components. All components will affect the GameObject and any other GameObjects. For example, a Transform component affects the GameObject’s size, position and rotation, while maintaining a hierarchical structure. To instantiate a GameObject, we call its constructor:

gameObject = GameObject("Object 1")
Logger.Log(gameObject.transform.position)

This creates a GameObject with the name “Object 1”. We then print its position, from gameObject.transform. Note that we use Logger.Log instead of print, because it will show up in the PyUnity logs. gameObject.transform is the Transform component and all GameObjects have one. It defines 8 properties, 4 local properties and 4 non- local properties. Local properties are relative to their parent, and non-local properties are relative to the world space. Transforms also define the hierarchical structure, with the parent and children properties.

Components

Components are what make GameObjects interactive. The Transform component is just one of many others. For example, MeshRenderer renders a mesh every frame at the location of the Transform. A Light component casts light on other objects. Here is an example of the MeshRenderer:

cube = GameObject("Cube")ess
renderer = cube.AddComponent(MeshRenderer)
renderer.mesh = Mesh.cube(2)
renderer.mat = Material(RGB(255, 0, 0))

To add a component, call AddComponent from the GameObject or any of its components. If I were to create a new renderer, I could use the old renderer to call AddComponent:

renderer2 = renderer.AddComponent(MeshRenderer)

All components define the gameObject property, which is the GameObject the component belongs to. Each Component will define some writable attributes, like MeshRenderer.mesh. Above we created a cube mesh from the Mesh class, and also set its material to a bright red colour. However, sometimes we don’t always want to waste so much processing power on creating a mesh, so we can use one of 6 presets defined in Loader.Primitives.

Behaviours

To create custom Components, subclass from Behaviour . It has 2 main overridable methods, named Start and Update. Start is called when the scene is started, so it is guaranteed to be able to find all GameObjects and access their properties. Update takes one parameter, dt, which is the time since the last frame. Behaviours also have another function which takes the same parameter, LateUpdate. As a Behaviour is just a Component, you can access the same attributes as a Component:

class Debugger(Behaviour):
def Update(self, dt):
Logger.Log(self.transform.position, self.gameObject.name)

To add a Behaviour, add it just like a Component:

cube.AddComponent(Debugger)

Input

To take user input, we can use the Input class. There are 2 preset axes, which are values from -1 to 1 depending on what keys we press. For example, the “Horizontal” axis will increase when either the right or D key is pressed and will decrease when either the left or A key is pressed. However, sometimes we don’t want a smooth interpolation between -1 and 1, and we would like to just get the raw input of an axis. This is when we can use GetRawAxis:

class KeyLogger(Behaviour):
def Update(self, dt):
Logger.Log(
Input.GetAxis("Horizontal"),
Input.GetRawAxis("Horizontal")
)

To get the state of a single key, we can use GetKey, GetKeyDown and GetKeyUp. GetKey is triggered whenever the key is held down, GetKeyDown is only triggered when the key was pressed down this frame and GetKeyUp is triggered only when the key was released this frame.

Scenes

By default, a scene has two GameObjects: the Main Camera and a Light. The Main Camera is accessible from the mainCamera property of the scene:

scene = SceneManager.AddScene("Scene")
scene.mainCamera.position = Vector3(0, 5, -10)
scene.mainCamera.eulerAngles = Vector3(20, 0, 0)

To get the Light, use scene.gameObjects[1]. The Light component has 3 attributes: intensity, color and type. In our script, we make the intensity of our light 100 so that it can light up the entire box.

Main Code

Finally, the part that you are all waiting for! We first define two Behaviours. To help integrate Behaviours with the PyUnity editor, the way we define attributes that can be edited in the Editor is using ShowInInspector. This class will be detected when we subclass Behaviour and will be replaced by the default value that we want to give it. The Start function is called as soon as the scene starts, so in the Behaviour CameraController, we can access the other component and its transform.position to get the offset.

A Rigidbody makes an object move. For a Rigidbody to work, it must have Colliders that define its collision box, which is what SphereCollider and AABBoxCollider do. A PhysicMaterial has two properties, restitution and friction. For now, we make friction 0 so that our ball will keep rolling along.

For an example of using ShowInInspector and HideInInspector , here is the Python code:

class CustomScript(Behaviour):
editorProperty = ShowInInspector(GameObject)
hiddenProperty = HideInInspector(int, 5)
privateProperty = True

And the C# code that would be used in Unity:

public class CustomScript : MonoBehaviour {
public GameObject editorProperty;
[HideInInspector] private int hiddenProperty = 5;
private bool privateProperty = true;
}

Summary

If you ran the code, you may notice that the ball doesn’t rotate. As of now (version 0.8.2), the PyUnity physics engine has no rotation physics, but it won’t matter much for now because a rotating ball and a non-rotating ball look virtually the same. Here is a picture of what it looks like:

Roll A Ball in PyUnity

If you have any improvements, feel free to share! Here is the main repository for PyUnity: https://github.com/pyunity/pyunity

--

--

Ray Chen
Ray Chen

Written by Ray Chen

Just an aspiring programmer that loves to do things that no one approves of. Sometimes it works out, sometimes not!

No responses yet