Ursina is a Python wrapper around the Panda3D game engine
- Introduction
- Creating your first program
- Improving your window
- Responding to events
- Drawing some cubes
- Random values
- Moving the camera
- Adding more cubes
- Rotating more cubes
- Pending
Introduction
Ursina’s most critical parts are written in C++ or shader code, something that allows you to use Python on top of it without incurring any penalties in performance, hence letting you free to enjoy the productivity advantages of writing code in Python.
Panda3D is an Open Source game engine created by Disney and still used for production development.
uv add ursinaCreating your first program
Probably the most intimidating step into writing games is to initialize the window. There are so many concepts that go with this initialization like selecting a screen size, setting the color modes, creating the back buffers, setting up the video card, and writing down the most basic graphics switching system and all that even before we can even see anything.
Fortunately, Ursina makes it so easy that you get all that in three lines. Python is a scripting language, which in essence means you write a text file and the Python interpreter will run it straight, no need to compile or anything.
Paste the following code into main.py:
from ursina import * # Import the ursina engine
app = Ursina() # Initialise your Ursina appapp.run() # Run the appRun the program with uv run main.py: you will see a window with a red cross on top.
Improving your window
So the window looks good, but it might improve with some work, edit your main.py program to match the code below.
from ursina import * # Import the ursina engine
app = Ursina() # Initialise your Ursina app
window.title = 'My Game' # The window titlewindow.borderless = False # Show a borderwindow.fullscreen = False # Do not go Fullscreenwindow.exit_button.visible = False # Do not show the in-game red X that loses the windowwindow.fps_counter.enabled = True # Show the FPS (Frames per second) counter
app.run() # Run the appThe “window” object is part of the application, and you can access it directly. Some elements of the window can be accessed after the game is running, but some cannot. Play with those values setting them to True or False and check the effect. Go to the documentation at https://www.ursinaengine.org/cheat_sheet.html#window to see even more options for the window.
Responding to events
So now you have a window running, try to see how to control things from the game.
Game engines work in “passes”. On each pass it will check input conditions, check sound buffers, compute the AI, update the internal game state and logic, then renders the current scene to the background, then does the “flipping” which updates the contents of the screen.
The Ursina engine simplifies all this process, so no need to do the rendering by yourself, but you are allowed to use the “update” function to test and update your internal logic need, then let the engine do the rendering.
To use the update() function, add it to your window program like this:
def update(): print("Update!") # Print Update every time this loop is executed if held_keys['t']: # If t is pressed print(held_keys['t']) # Print the valueThe update function is what is called a global function. As long as it is defined somewhere, Ursina will run it. What is going here is that the program will print “Update!” to the console (not to the window) every time it is called.
The engine also has an array that checks which keyboard key is pressed, this array has one entry for each key available in the keyboard, and by default its values are set to 0. When a key is pressed, its value is set to 1, when it’s released it is set to 0 again.
The Python if instruction will execute the commands inside its block only if the expression evaluates to anything that is not 0 (or null or empty), so when it finds a 1 in the corresponding key, it prints the value, when the key is released it becomes 0 again so the “if” block is no longer executed.
Drawing some cubes
Right now the engine is running on empty. An engine is supposed to update a scene, then draw some graphics in the background, then flip to the foreground, but we are not doing anything, so we only see a black screen. Let’s do something about this, but before starting you need to understand how an engine is presented.
Game engines are pretty much like filming a video. What you see on the screen is a scene that you film through some camera lens. So the idea is you put objects in the scene which Ursina calls Entities but other engines refer to them in similar ways like actors or game objects. The camera is pointing to the center of the scene, which in coordinates is the 0,0,0.
So you need to put your objects in your scene, and they appear on the screen. Really neat right?
So let’s create a new entity and assign it to a variable. You may see a variable like a reference to the object so you don’t lose it.
Add this object to your main.py program before the app.run() command:
cube = Entity(model='cube', color=color.orange, scale=(2,2,2))Here we are creating a cube, and we set the color to orange and a size of 2. So run your program and you will see an orange square.
And now I will read your mind… “Wait a minute… we are supposed to see a cube!!!!!”
Yeah, of course, but that depends on from where you see the cube, or better said, from where the camera is looking at the cube. Let’s add some rotation to the cube, so let’s do some work on the update function:
def update(): cube.rotation_y += time.dt * 100 # Rotate every time update is called if held_keys['t']: # If t is pressed print(held_keys['t']) # Print the valueSo what we are doing now is rotating the cube around its Y axis (imagine an arrow going up). The engine has a global variable called time.dt which has the time elapsed since the last frame. The += instruction is like saying add to the current value of the Y rotation the new time difference so it is accumulated.
If you run this program and squint a little, you may start noticing the figure is indeed a cube.
Now try changing the color of the cube, Ursina has a list of predefined colors that you might find here. https://www.ursinaengine.org/cheat_sheet.html#color Try setting the color to yellow or red.
Ursina also allows setting a color using Red Green Blue (RGB) components with values from 0 to 255, try using color.rgb32(100, 50, 200) and change the values to see the effect.
Random values
Random values are extremely useful when building games. They can be used to generate enemies or just to calculate probabilities like rolling dice.
To create a new random number generator, use the Python random library. This is part of Python so no need to install anything else.
Try updating your main.py file like this:
import random # Import the random library
rg = random.Random() # Create a random number generatorNow paint this cube randomly. Each time randint(0,255) is called a number from 0 to 255 is created, so do that when the R key is pressed the cube is painted in some random color.
def update(): cube.rotation_y += time.dt * 100 # Rotate every time update is called if held_keys['r']: # If r is pressed red = rg.randint(0, 255) green = rg.randint(0, 255) blue = rg.randint(0, 255) cube.color = color.rgb32(red, green, blue)You can also use color.random_color() to get a random color.
Now run and press the R key a few times, and you should see the cube changing colors.
While this looks cool, you might notice that if you leave the R key pressed, the cube will flash changing colors multiple times. This happens because the change is evaluated every time the update function is invoked, so while the key is pressed, it will keep changing colors.
However, sometimes you want to act on the first press of a key only, so Ursina provides a way to capture this “when pressed” event by using the input() function.
Add this after the update function.
def input(key): if key == 'space': cube.color = color.random_color()Now try using Random to change the scale and position of the cube. Try to keep scale between 1 and 5 and position to be between -5 and 5.
def input(key): if key == 'space': cube.color = color.random_color() cube.scale = rg.randint(1, 5) cube.x = rg.randint(-5, 5) cube.y = rg.randint(-5, 5)Moving the camera
So, the cube is moving, now work out the camera. The camera is an Entity that is part of the scene, and it has a variable reference called “camera”. As an Entity, the camera can be moved using its position and rotation, imagine you are a cameraman moving it around the scene.
For simplicity, try moving it up and down when we press the Q and A keys:
def update(): cube.rotation_y += time.dt * 100 if held_keys['q']: camera.position += (0, time.dt, 0) if held_keys['a']: camera.position -= (0, time.dt, 0)Now try moving the camera left and right. Try rotating the camera on the Z axis.
The camera has other options that are useful depending on what you are trying to render and how you want to display the scene. If you have seen a camera, you know it has a lot of knobs and dials; this is quite similar and the same as a real cameraman you need experience to learn how to use every option. Some options you may play with are here: https://www.ursinaengine.org/cheat_sheet.html#camera
Adding more cubes
So we have been playing with one cube only. Let’s up this a bit and learn how to work with multiple entities. To do that, we need to learn something called a data structure. Data structures are probably the most important pieces of any system, there are many data structures, and learning how and when to use each one makes you a master.
The most basic data structure is the list. Think about it as a string where you put beads one after the other. When you want to work with things inside, you go through the list and operate on each one, one by one.
So let’s create a list and add our cube to it. So surround the cube creation with this:
cubes = []cube = Entity(model='cube', color=color.orange, scale=(2,2,2))cubes.append(cube)So let’s do it so that every time we press C a cube is added to the list in a random space coordinates between -5 and 5 in x, y, and z axis.
def input(key): if key == 'space': cube.color = color.random_color() cube.scale = rg.randint(1, 5) cube.x = rg.randint(-5, 5) cube.y = rg.randint(-5, 5)
if key == 'c': x = rg.randint(-5, 5) y = rg.randint(-5, 5) z = rg.randint(-5, 5) s = rg.random() newcube = Entity(model='cube', color=color.orange, position=(x, y, z), scale=(s, s, s)) cubes.append(newcube)Now we have many cubes but in the same dull orange colour. Use some code similar to what we do when the “space” key is pressed and use color.random_color() instead of color.orange to get a random colour when you create a cube.
Rotating more cubes
So now all the entities are in the scene, but they are not moving.
Go through all entities in the scene and move them all, not just the cube.
Update your update() function as:
def update(): for entity in cubes: entity.rotation_y += time.dt * 100
if held_keys['q']: camera.position += (0, time.dt, 0) if held_keys['a']: camera.position -= (0, time.dt, 0)So instead of just updating the rotation for one cube, we now go through all the entities in the list and update each one. The “for” instruction in Python allows us to iterate through lists.