Ursina Game Engine Tutorial for Dummies


Summary



This is a tutorial that explains you how to setup your environment, create a window and render a coloured cube with really simple controls.

A passing knowledge in Python is beneficial but not required, this tutorial assumes you have no idea what you are doing at all.

Once you finish this tutorial you are expected to know:
    - How to setup Python in any computer.
    - How to install a python library
    - Select your IDE
    - Write a program
    - Execute a program
    - Create and destroy a graphics window using Ursina
    - Draw a simple mesh
    - Control the mesh using the keyboard.
    - Hierarchies
    - Texturing
    - Texture animations
    - Alpha blending
    - Mouse collisions
    - Text and UI




Introduction



So you want to write your own game, sell it by tons, and get rich and famous? Then you are on the right track, but let me be straight and tell you this is not going to happen anytime soon. Many people writing games in big companies started somewhere and to get to the top you need to understand the basics and that is exactly why you are here, so consider this tutorial as the stepping stone into the game development industry.

Ursina is a Python wrapper around the Panda3D game engine, as such Python is used to control every aspect of the engine from the initialisation, rendering, game control, playing sounds, and shutdown, therefore you will need to build up your Python skills. Python has become one of the most used and well respected scripting languages and plenty of companies and game development studios use Python to automate many functions so this is a fantastic tool to have in your belt.

Panda3D is an Open Source game engine created by Disney and still used for production development. Panda3D already has a Python wrapper, but Ursina enhances the engine making it seamless to work across platforms and providing tools that make the development experience much more comfortable.

Before we start, keep in mind the engine has a reference page here https://www.ursinaengine.org/cheat_sheet.html in case you want to check even more options.




Install Python3



If you are ready to start writing your game, you need to get your computer ready and the first step is to install Python3.6. Python is an universal language so it is available for most operating systems. Please follow these instructions to install it in your computer.

https://docs.python-guide.org/
https://docs.python-guide.org/starting/install3/osx
https://docs.python-guide.org/starting/install3/win
https://docs.python-guide.org/starting/install3/linux

Once done, you should be able to run:
python3 --version Python 3.7.4




Setup a virtual environment (venv) (optional)



A Python virtual environment or venv is the way Python maintains a private space for the libraries you need for a project. You can have any number of virtual environments in your computer and be sure all libraries installed in one venv do not mix with libraries in the other venv so for example you may create a "gamedev" venv and install all your game development libraries, then create a "science" venv and install libraries for number crunching, then create another called "music" and install all your libraries you need to work with sound files. You may even install multiple versions of the same libraries on each venv and be sure they don't mix. Once you are done with a venv (or maybe it becomes too large because you have been installing too many things) you can delete the venv and recreate it from scratch.


Installing the Ursina game engine



To install Ursina we need to use pip, the Python package manager.

Jump into your venv:
source gamedev/bin/activate

Now install the engine:
pip install ursina

This will download and setup ursina and other libraries required by the engine. If you want to see which libraries are installed run:
pip list

You will see some libraries like Panda3d in there.

Now let me show why Python is so popular. Python has tons of other libraries available and all of them are located in a repository called the Python Package index or PyPI (https://pypi.org). There are hundreds of thousands of libraries available for almost anything you may need and they are organised and searchable. So for example you need a library to work your sound files, you may run:
pip search sound

Or maybe you need to read a YAML file (yaml), or machine learning (ml), or cover image formats. Just run a pip search and you will get something.

So  say you want the "playsound" library installed. Run:
pip install playsound

Of course, we don't really need this library because ursina can play sound as well. This is just an example.
Once you are done with this library run:
pip uninstall playsound




Installing an IDE



This is a tricky one. There are plenty programming IDEs available and its all a matter of choice. Essentially you may code in command line but installing a nice IDE will help you be more effective. Some options:
    - Sublime Text
    - Atom
    - PyCharm
    - Visual Studio Code
    - Notepad++
    - Eclipse

Sublime is a good choice for a beginner as it is very lightweight (and I'm writing this tutorial in Sublime right now), Pycharm is favoured by Python developers, but go on search and install and test, everything in that list is useful.




Creating your first program



Probably the most intimidating step into writing games is to initialise the window. There are so many concepts that go with this initialisation 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.

So let's start, using your file manager or your IDE, create a new folder (i.e. ursinatutorial). Inside that folder create a new file called window.py and write the three lines between the dashes (------)

from ursina import * # Import the ursina engine app = Ursina() # Initialise your Ursina app app.run() # Run the app

The text after the # is considered comments until the end of the line and are ignored. Use comments to help reading your code.

So now make sure your gamedev venv is active so that python can find the Ursina engine. Then go to the folder where your program is and run:
python windows.py

You will see a window with a red cross on top.

Congratulations, you have created your first window and you are a step closer to your dream!




Improving your window



So the window looks good, but it might improve with some work, edit your window.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 title window.borderless = False # Show a border window.fullscreen = False # Do not go Fullscreen window.exit_button.visible = False # Do not show the in-game red X that loses the window window.fps_counter.enabled = True # Show the FPS (Frames per second) counter app.run() # Run the app

The "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 can not. 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:

from ursina import * # Import the ursina engine 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 value app = Ursina() # Initialise your Ursina app ... The rest of the program


The 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 its 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 presente.

Game engines are pretty much like filming a video. What you see in 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 just need to put your objects in your scene and they appear in 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 window.py program BEFORE the app.run() command:

... all your program cube = Entity(model='cube', color=color.orange, scale=(2,2,2)) app.run() # Run the app

Here we are creating a cube and we set the colour 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 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 value

So 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 colour of the cube, Ursina has a list of predefined colours that you might find here. https://www.ursinaengine.org/cheat_sheet.html#color Try setting the colour to yellow or red.

Ursina also allows setting a colour using Red Green Blue (RGB) components with values from 0 to 255, try using color.rgb(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.  add some randomness to this cube.

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 window.py file like this:

from ursina import * # Import the ursina engine import random # Import the random library random_generator = random.Random() # Create a random number generator ....

Now  paint this cube randomly. Each time random() is called a number from 0 to 1 is created, so  do that when the R key is pressed the cube is painted in some random colour.

def update(): cube.rotation_y += time.dt * 100 # Rotate every time update is called if held_keys['r']: # If r is pressed red = random_generator.random() * 255 green = random_generator.random() * 255 blue = random_generator.random() * 255 cube.color = color.rgb(red, green, blue)

Now run and press the r key a few times and you should see the cube changing colours.

While this looks cool, you might notice that if you leave the r key pressed, the cube will flash changing colous 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 colours.

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': red = random_generator.random() * 255 green = random_generator.random() * 255 blue = random_generator.random() * 255 cube.color = color.rgb(red, green, blue)

Now try using Random to change the scale and position of the cube. Try to keep both scale and position to be between 0 and 5.




Moving the camera



So, the cube is moving, now  work out the camera. The camera is an Entity which 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 # Rotate the cube every time update is called if held_keys['q']: # If q is pressed camera.position += (0, time.dt, 0) # move up vertically if held_keys['a']: # If a is pressed camera.position -= (0, time.dt, 0) # move down vertically

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 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 string where you just 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 = [] # Create the list cube = Entity(model='cube', color=color.orange, scale=(2,2,2)) cubes.append(cube) # Add the cube to the list

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': red = random_generator.random() * 255 green = random_generator.random() * 255 blue = random_generator.random() * 255 cube.color = color.rgb(red, green, blue) # Note I still can reference any individual object I want if key == 'c': x = random_generator.random() * 10 - 5 # Value between -5 and 5 y = random_generator.random() * 10 - 5 # Value between -5 and 5 z = random_generator.random() * 10 - 5 # Value between -5 and 5 s = random_generator.random() * 1 # Scale between 0 and 1 # Create the new cube and add it to the list 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.rgb() 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 througn all entities in the scene and move them all, not just the cube. Update your update() function as:

def update(): for entity in cubes: # Go through the cube list entity.rotation_y += time.dt * 100 # Rotate all the cubes every time update is called if held_keys['q']: # If q is pressed camera.position += (0, time.dt, 0) # move up vertically if held_keys['a']: # If a is pressed camera.position -= (0, time.dt, 0) # move down vertically

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 iterating through lists.




Hierarchies (The knee bone's connected to the thigh bone, the thigh bone's connected to the hip bone...)



So far we have discussed how objects are added to an scene, but how about more complex hierarchies like the sun and planets and moons. These are more complicated scenes where some objects depend on the movement of their parents.

In game engines are usually organised in hierarchies and there is usually a way to define the parent relationship during creation. In the Ursina engine when an object is created it is automatically assigned to the scene but you can alter that. Remember when you added a new cube when the letter c is pressed? Change that a bit as:

... red = random_generator.random() * 255 green = random_generator.random() * 255 blue = random_generator.random() * 255 newcube = Entity(parent=cube, model='cube', color=color.rgb(red, green, blue), position=(x, y, z), scale=(s,s,s)) ...

So we are telling the newly created Entity that the parent is not the main scene but our beloved cube. Try and run this and press "c" to create new cubes.

So now when the cube rotates, all objects rotate with it. In fact as we are still rotating all the cubes in the list, if you notice the cubes are also rotating independently.

Now, when an object is in hierarchy with another object, its position is relative to the other object, no longer to the center of the scene. In fact its parent is now considered the starting coordinate. Let's change this so that when a cube is added, another cube is added with the new cube as its parent, something like this:

... red = random_generator.random() * 255 green = random_generator.random() * 255 blue = random_generator.random() * 255 newcube = Entity(parent=cube, model='cube', color=color.rgb(red, green, blue), position=(x, y, z), scale=(s,s,s)) cubes.append(newcube) # Create another child cube and add it to the list but using the newcube as the parent, keep the same colour, make it smaller childcube = Entity(parent=newcube, model='cube', color=color.rgb(red, green, blue), position=(1, 0, 0), scale=(s/2, s/2, s/2)) cubes.append(childcube) ...




Adding some texture



So you are seeing a lot of things that look like cubes but don't really feel like cubes. That is because a lot of the 3D feeling is really provided by the object texture so  explore them.

Textures are (usually) square images loaded into the video card memory. As with any image, a small image is fast to load into memory and fast to render, but provides less detail, a large image uses more memory and is slower to render, but provides more detail. You may tend to think that with larger textures your game will look neater but it may lead to slow rendering times. It all depends on your game and what you want to show. If it is a model that will be seen only from far away, why do you need high detail? If its too close the texture will look bad. It's something you need to agree on with your graphics artist.

Anyway, get a texture from somewhere, for example get https://www.google.com/search?q=texture+3d+square+crate and save it next to your program file as a JPG. For this tutorial name it crate.jpg and make sure it is square like 128x128 or 256x256 or 512x512 pixels.

Now, when creating an entity, add the texture parameter, for example:

cube = Entity(model='cube', color=color.orange, scale=(2,2,2), texture="crate")

Notice while the file is called "crate.jpg", you only name it "crate". The engine will look for JPG, PNG or even PSD files automatically. Feel free to add the parameter to all places where you create a cube.

Now, something you will notice is that while the texture is applied to all cubes, each one has a different colour. This is because the colour of the texture is affected by the colour of the cube itself, so if the cube is red the whole object will look reddish, this operation is called a colour multiplication.

This is pretty much the same problem you have when trying to paint a wall when there is another colur behind. Colours mix and you don't get what you want. This is exactly why painters start with a white canvas and why you prime a wall white before painting it with the colour you want. The way to get the right texture colour is to provide a full white colour to the cube, so in every entity creation replace the cube colour for "colour.white" or color.rgb(255,255,255) like:

cube = Entity(model='cube', color=color.white, scale=(2,2,2), texture="crate")

Now run your program again and you should see the real texture colour. You may still press space to change the central cube colour.




Texture UVs



You might notice in this example, the texture is nicely applied to each side of the cube. However sometimes you want the texture to map in a different way or alter how a texture is applied so we will work a bit on this.

So the easiest way to imagine a texture is like a square piece of rubber with each corner extending from 0 to 1. So the top left corner is a 0,0 and the lower right corner is a 1,1. Now in this models we are using, each face of the cube is also marked  with the same coordinates in the corners, so some one corner reads as (0,0), (0,1) , (1,0) and (1,1). Then the engine matches the corners of the texture to the corners of the cube and all of them are nicely aligned. This coordinates in the mesh that pins the texture against the model are called UV coordinates. The U stands for the horizontal "x" axis and the V stands for the vertical "y" axis.

But then what will heppen if we change the model but not the texture? Ursina engine has some basic primitives like 'cube', 'quad', 'sky_dome', 'sphere', try them out. Use "q" to move the camera to try and see the objects from the top. Observe how the texture wraps around each figure in a different way. If you think the texture is a rubber surface that stretches to match the object, then you will notice that what makes it stretch around is how the figures are defined.




Animating UVs (Advanced stuff)



In a normal scenario, the artists will provide models with their textures and UV coordinates set as they have to, as a developer you just have to load and render everything in the engine.
However, not all the textures are static and there are many cases where textures need to be animated, this is specially true to simulate fluids like water or lava, for example to create a river or a waterfall.

So let's get a texture first, in this case we want a waterfall so  search the internet for something useful, in this case I want a waterfall texture that is seamless, meaning I can move it around and we shouldn't notice the borders.
https://www.google.com/search?q=game+water+texture+seamless

There are so many of them, many are free but many are paid. I decided to go for this one. Download it (click the ... icon next to the image) and save it next to your window.py program as water.jpg
https://www.pinterest.com.au/pin/210050770099370255/

Now change the texture in the central cube, and also make the cube larger so we notice the effect like this:

cube = Entity(model='cube', color=color.white, scale=(2,6,2), texture="waterfall")

Also make it slower to rotate so it is more visible, instead of multiplying the rotation by 100 make it by 5.

for entity in cubes: entity.rotation_y += time.dt * 5 # Rotate all the cubes every time update is called

Run this to see how are we doing. You should be able to see a texture on the cube, looks like water but it's not moving so it looks dull. Let's do some animation.

Let's start by creating a global variable to keep the texture movement, add it after the line where we initialise the random generator. Call it texoffset.
.... random_generator = random.Random() # Create a random number generator texoffset = 0.0 # define a variable that will keep the texture offset ....

Now, in your update function, update the texture offset and use that to update an attribute called texture_offset like this:
... def update(): for entity in cubes: entity.rotation_y += time.dt * 5 # Rotate all the cubes every time update is called if held_keys['q']: # If q is pressed camera.position += (0, time.dt, 0) # move up vertically if held_keys['a']: # If a is pressed camera.position -= (0, time.dt, 0) # move down vertically global texoffset # Inform we are going to use the variable defined outside this function texoffset += time.dt * 0.2 # Increment this variable just a little based on the time setattr(cube, "texture_offset", (0, texoffset)) # Assign the new texture offset to the entity ...

Note we are updating only the "V" (y, vertical) element of the texture, we are not updating the U (x, horizontal) coordinate as we want water falling down.

Now rerun and you will see the new waterfall effect!

But you know what the problem is with effects... you can always make it better! In this case we are going to add a second wall of water also with a vertical movement but with that is a bit transparent.




Transparency



So we have always talked about solid colour like red, white, blue, orange, etc. And we have used a function called color.rgb() to set the colour, however in computer graphics colours are as solid as you want them to be.

There is a fourth component to the colour that is called "alpha" which is the term for "transparency" and is a component of a colour the same as red, green or blue and it also goes from 0 to 255 where 0 means completely transparent and 255 means completely opaque so you can't see through. To set this transparency you can use the function color.rgba() which allows setting the alpha level.

To test this, we are going to create a second cube on top of the first cube, but in this case we will set the colour to a semi transparent (128) alpha. We also make this second box larger wider and deeper than the other box (but the same height) so it surrounds it.

... cube = Entity(model='cube', color=color.white, scale=(2,6,2), texture="waterfall") cubes.append(cube) # Add the cube to the list cube2 = Entity(model='cube', color=color.rgba(255,255,255,128), scale=(2.5,6,2.5), texture="waterfall") cubes.append(cube2) # Add the cube to the list ...

If you run now, you will be able to see the new box and if you notice the borders you can see it is semi transparent.

Now create a second variable so we can move the water just after the first texoffset variable:

... random_generator = random.Random() # Create a random number generator texoffset = 0.0 # define a variable that will keep the texture offset texoffset2 = 0.0 # define a variable that will keep the texture offset ...

Finally, move the texture in the second cube but a bit faster than the first cube thus giving a sense of water falling at different speeds:

... def update(): for entity in cubes: entity.rotation_y += time.dt * 5 # Rotate all the cubes every time update is called if held_keys['q']: # If q is pressed camera.position += (0, time.dt, 0) # move up vertically if held_keys['a']: # If a is pressed camera.position -= (0, time.dt, 0) # move down vertically global texoffset # Inform we are going to use the variable defined outside global texoffset2 # Inform we are going to use the variable defined outside texoffset += time.dt * 0.2 # Add a small number to this variable setattr(cube, "texture_offset", (0, texoffset)) # Assign as a texture offset texoffset2 += time.dt * 0.3 # Add a value to this variable, but different to the first one setattr(cube2, "texture_offset", (0, texoffset2)) # Assign as a texture offset of the second cube ...




Adding text and buttons.



So the final work is to add some text to our demo. Before continuing you need to be aware that in most games there is a layer on top of the scene called the UI layer or front layer. This is very similar to what you see during a sports game or during news where there are letters on top of the scene you are watching.

In the same way, the scene object in the Ursina engine has something called the UI and you can create text and bitmaps and buttons on top of it so you create an interface your players can use. In that UI you can display your compass, character health, timers, etc.

So  start by adding some text.  create it just before starting the app, after we create the cubes:

... Text.size = 0.05 Text.default_resolution = 1080 * Text.size info = Text(text="A powerful waterfall roaring on the mountains") info.x = -0.5 info.y = 0.4 info.background = True info.visible = False # Do not show this text app.run() # opens a window and starts the game.

The coordinate systems x and y of the UI are a bit complicated, (0,0) refers to the center of the screen. To understand the coordinate system refer to this document. https://www.ursinaengine.org/coordinate_system.html

If you run the code above you will probably see nothing because we intentionally started with the info.visible property set to False, now we want to show it. We are going to use a function of the mouse cursor which gets you which entity it is hovering at. Let's update the update() function to this:

... def update(): for entity in cubes: entity.rotation_y += time.dt * 5 # Rotate all the cubes every time update is called if held_keys['q']: # If q is pressed camera.position += (0, time.dt, 0) # move up vertically if held_keys['a']: # If a is pressed camera.position -= (0, time.dt, 0) # move down vertically global texoffset # Inform we are going to use the variable defined outside global texoffset2 # Inform we are going to use the variable defined outside texoffset += time.dt * 0.2 # Add a small number to this variable setattr(cube, "texture_offset", (0, texoffset)) # Assign as a texture offset texoffset2 += time.dt * 0.3 # Add a small number to this variable setattr(cube2, "texture_offset", (0, texoffset2)) # Assign as a texture offset if mouse.hovered_entity == cube: # If the mouse is hovering over the cube entity info.visible = True # Make the text visible else: # else info.visible = False # hide it again ...

Now run this program and hover the mouse on top of the cascade, you will notice when the mouse touches the box the text is displayed, and when you hover out it hides.

Now try using the mouse docs https://www.ursinaengine.org/cheat_sheet.html#mouse and check if you can do the text appear only if the cursor if over the cascade and only if the left button is clicked (hint: mouse.left = True)




The end



So if you got to this point, cheers, a big hooray and kudos to you! In this time you have learned so many things that you really deserve a treat! Your final program should look like this:

from ursina import * # this will import everything we need from ursina with just one line. import random # Import the random library random_generator = random.Random() # Create a random number generator texoffset = 0.0 # define a variable that will keep the texture offset texoffset2 = 0.0 # define a variable that will keep the texture offset def update(): for entity in cubes: entity.rotation_y += time.dt * 5 # Rotate all the cubes every time update is called if held_keys['q']: # If q is pressed camera.position += (0, time.dt, 0) # move up vertically if held_keys['a']: # If a is pressed camera.position -= (0, time.dt, 0) # move down vertically global texoffset # Inform we are going to use the variable defined outside global texoffset2 # Inform we are going to use the variable defined outside texoffset += time.dt * 0.2 # Add a small number to this variable setattr(cube, "texture_offset", (0, texoffset)) # Assign as a texture offset texoffset2 += time.dt * 0.3 # Add a small number to this variable setattr(cube2, "texture_offset", (0, texoffset2)) # Assign as a texture offset if mouse.hovered_entity == cube: info.visible = True else: info.visible = False def input(key): if key == 'space': red = random_generator.random() * 255 green = random_generator.random() * 255 blue = random_generator.random() * 255 cube.color = color.rgb(red, green, blue) if key == 'c': x = random_generator.random() * 10 - 5 # Value between -5 and 5 y = random_generator.random() * 10 - 5 # Value between -5 and 5 z = random_generator.random() * 10 - 5 # Value between -5 and 5 s = random_generator.random() * 1 # Value between 0 and 1 newcube = Entity(parent=cube, model='cube', color=color.white, position=(x, y, z), scale=(s,s,s), texture="crate") cubes.append(newcube) '''Create another child cube and add it to the list but using the newcube as the parent, keep the same colour, make it smaller''' childcube = Entity(parent=newcube, model='cube', color=color.white, position=(1, 0, 0), scale=(s/2, s/2, s/2), texture="crate") cubes.append(childcube) app = Ursina() window.title = 'My Game' # The window title window.borderless = False # Show a border window.fullscreen = False # Go Fullscreen window.exit_button.visible = False # Show the in-game red X that loses the window window.fps_counter.enabled = True # Show the FPS (Frames per second) counter cubes = [] # Create the list cube = Entity(model='cube', color=color.white, scale=(2,6,2), texture="waterfall", collider="box") cube2 = Entity(model='cube', color=color.rgba(255,255,255,128), scale=(2.5,6,2.5), texture="waterfall") cubes.append(cube) # Add the cube to the list cubes.append(cube2) # Add the cube to the list Text.size = 0.05 Text.default_resolution = 1080 * Text.size info = Text(text="A powerful waterfall roaring on the mountains") info.x = -0.5 info.y = 0.4 info.background = True info.visible = False # Do not show this text app.run() # opens a window and starts the game.

Try playing with this tutorial. Add some planes to the top and bottom of your cascade and make them move slowly like calm water, add some cubes with rock textures and maybe some with grass textures to the sides to make a scene, cleanup the flying boxes.

Then, try making a solar system using spheres.

The key is practicing until your feel comfortable with your skills, then we can move into more complex programs.

Good luck!