Making Minecraft Using Ursina & Python! (1/19/25 blog post)

Ursina is an easy-to-use, open source 3D game engine that can run better than Pygame. Since Pygame has some issues with frame-rate, poor performance, and is not the best when making games with 3D capability, I won’t be using it (for this project). This is 3D games-wise, though. Pygame is still better and MUCH easier to use when making simpler looking games. Ursina only needs a few lines of code to really make a game, and is beginner friendly when making a 3D program. However, like Pygame, it does have some bugs that come with it such as player model issues, blend files unable to load, and collider issues when making a player model. Unfortunately, Ursina isn’t available for MacOS (you can use Panda3D though, which is available for MacOS and Windows).

Besides these issues, Ursina is quite powerful and a good option for a 3D game engine for beginners. Now, let’s get started.

INTRO TO URSINA

First and possibly the most obvious step is to install Ursina.

  • Open your terminal in whatever code editor you use and type in “pip install Ursina”
pip install Ursina
  • We’re now able to start coding. first import all objects of Ursina using the * character.
from ursina import *
  • Now, we must create and instance of the game. You can name this whatever you’d like, but for this it will be named “app” (I know, so creative).
app = Ursina()

*Make sure to add parenthesis at the end or the code will yell at you ( I learnt this the hard way numerous times).

  • Now, lets run it. This will create the interface where the game will be displayed.
from ursina import *
app = Ursina()
app.run()

* If you thought that it would look like something such as unity or unreal engine, remember that we are using Python, not C# or C++ lol.

  • The astronomically microscopic numbers on the top right-hand corner of the screen shows the frame rate, the number of entities on the screen, and the number of collisions.
  • Lets add an entity, which is a base object for anything in Ursina. This can be anything, like a square, a circle, a cube, etc… Anything you see on the screen will count as an entity. It will be automatically added to the game, rather than calling it in a loop. An entity doesn’t need a color (it will default to white if no color is stated).
test_square = Entity(model='circle', color=color.blue)
  • This will create circle at the center of the screen when we run the program.
  • Let’s change it to a different shape.
test_square = Entity(model='quad', color=color.blue) 
  • This will change the circle into a square which it known as a “quad” in Ursina.

* You are able to add 3D objects like cubes, but since we’re looking at only one side at the moment, it’s kind of redundant to call a 3D object at the moment. We’ll get to that later, though.

  • You can also scale the entity by passing a number for the x and y axies.
test_square = Entity(model='quad', color=color.blue, scale = (1, 4))
  • In this instance, the 1 is how wide the entity will be, and the 4 is how long.
  • You can also change the position of the entity. It uses the same way we changed the scale of the entity, but this time we use “position” (or “pos” if you’re too lazy to write the whole word out).
test_square = Entity(model='quad', color=color.blue, scale = (1, 4), position = (5, 1))
  • This will move the square up 1 unit and right 5 units.
  • It should be obvious, but the center of the screen is the origin point (0,0). Making the y-axis positive or negative will scale or move the entity up or down, same with the x-axis (I know this is obvious but this 3D engine is interesting and fun to mess around in).
  • Now, lets make a function to move the entity. You will simply call the object you want to move in the function, the axis you wish to change, and how fast it will move.
def update():
    test_square.x -= 1
  • This will move the entity to the left at a speed of 1. Now, that may sound slow, but watch this:
Don’t blink or you’ll miss it lol
  • Now obviously we need to slow it down, so we’ll use “time.dt”. This will be multiplied by the speed at which the object is going, and will adjust to any frame-rate.
def update():
    test_square.x -= 1 * time.dt
  • It will now look like this:

*Note to self: going to add more in the future!

1/10/25 BLog Post (Happy New Year!)

Attempting to finish up the Drum Kit in Python!!!!

What it does so far…

ADDING A PLAY/PAUSE BUTTON

First, create the button in the main loop, and place it on the screen.

play_pause = py.draw.rect(screen, grey, [50, HEIGHT - 150, 200, 100], 0, 5)
    play_text = label_font.render('Play/Pause', True, white)
    screen.blit(play_text, (70, HEIGHT - 130))

After that, create a new variable called “mediumFont” as in indicator if the program is playing or not. This will be connected to “play_text2”.

 if playing:
        play_text2 = mediumFont.render('Playing', True, darkGrey)
    else:
        play_text2 = mediumFont.render('Paused', True, darkGrey)
        screen.blit(play_text2, (70, HEIGHT - 100))

The “play_text2” will have “Playing” or “Pausing” shown on screen when the “Play/Pause” button is pressed.

So… we made the button show up on screen, but we haven’t made it actually work yet.

Down into where the buttons pressed on your keyboard are called, make a new event type of ‘MOUSEBUTTONUP”. This will make the program able to play and pause when the button is clicked.

if event.type == py.MOUSEBUTTONUP:
            if play_pause.collidepoint(event.pos):
                if playing:
                    playing = False
                elif not playing:
                    playing = True

RESULTS:

11/8/24 Blog Post

MAKING (still) A BEAT MAKER IN PYTHON WHOO!!!!

FULL CODE(so far)

import pygame as py

from pygame import mixer
py.init()

WIDTH, HEIGHT = 1400, 800

black = (0,0,0)
white = (255,255,255)
grey = (128, 128, 128)
green = (0, 255, 0)
gold = (212, 175, 55)
blue = (0, 255, 255)


screen = py.display.set_mode([WIDTH, HEIGHT])
py.display.set_caption("Beat Maker")
label_font = py.font.Font('Roboto-Bold.ttf', 32)

FPS = 60
timer = py.time.Clock()
beats = 8
instrumnents = 6
boxes = []
clicked = [[-1 for _ in range(beats)] for _ in range(instrumnents)]
bpm = 240
playing = True
active_length = 0
active_beat = 1
beat_changed = True

# ---------Sounds---------#
hi_hat = mixer.Sound('sounds\hi hat.WAV')
snare = mixer.Sound('sounds\snare.WAV')
clap = mixer.Sound('sounds\clap.wav')
kick = mixer.Sound('sounds\kick.WAV')
crash = mixer.Sound('sounds\crash.wav')
tom = mixer.Sound('sounds\\tom.WAV')
py.mixer.set_num_channels(instrumnents * 3)
# ------------------------#


def play_notes():
    '''plays the sound of the instrument'''
    for i in range(len(clicked)):
        if clicked[i][active_beat] == 1:
            if  i == 0:
                hi_hat.play()
            if  i == 1:
                snare.play()
            if  i == 2:
                kick.play()
            if  i == 3:
                crash.play()
            if  i == 4:
                clap.play()
            if  i == 5:
                tom.play()


def draw_grid(clicks, beat):
    # Making the boxes for the interface
    left_box = py.draw.rect(screen, grey, [0, 0, 200, HEIGHT - 200], 5)
    bottom_box = py.draw.rect(screen, grey, [0, HEIGHT - 200, WIDTH, 200], 5)
    boxes = []
    colors = [grey, white, grey]
    # Making the names of all the instruments
    hi_hat_text = label_font.render('Hi-hat', True, white)
    screen.blit(hi_hat_text, (30, 30))
    snare_text = label_font.render('Snare', True, white)
    screen.blit(snare_text, (30, 130))
    kick_text = label_font.render('Kick', True, white)
    screen.blit(kick_text, (30, 230))
    crash_text = label_font.render('Crash', True, white)
    screen.blit(crash_text, (30, 330))
    clap_text = label_font.render('Clap', True, white)
    screen.blit(clap_text, (30, 430))
    floor_tom_text = label_font.render('Floor Tom', True, white)
    screen.blit(floor_tom_text, (30, 530))
    # draw some lines between the instrument names
    for i in range(instrumnents):
        py.draw.line(screen, grey, (0, (i * 100) + 100), (200, (i * 100) + 100), 5)
    # check how many boxes there are for each instrument
    for i in range(beats):
        for j in range(instrumnents):
            if clicks[j][i] == -1:
                color = grey
            else:
                color = green
                # Making a tri-color effect with rects
            rect = py.draw.rect(screen, color, 
            [i * ((WIDTH - 200) // beats) + 205, (j * 100) + 5, ((WIDTH - 200) // beats) - 10, 
            ((HEIGHT- 200)// instrumnents) - 10], 0, 3)
            # Blank/ NOT colorful rect
            py.draw.rect(screen, gold, 
            [i * ((WIDTH - 200) // beats) + 200, (j * 100), ((WIDTH - 200) // beats), 
             ((HEIGHT- 200)// instrumnents)], 5, 5)
            py.draw.rect(screen, black, 
            [i * ((WIDTH - 200) // beats) + 200, (j * 100), ((WIDTH - 200) // beats), 
             ((HEIGHT- 200)// instrumnents)], 2, 5)
            boxes.append((rect, (i, j)))
        active = py.draw.rect(screen, blue, [beat * ((WIDTH-200)//beats) + 200, 0, 
                ((WIDTH-200)//beats), instrumnents*100], 5, 3)
    return boxes


# main loop
run = True
while run:
    timer.tick(FPS)
    screen.fill(black)
    boxes = draw_grid(clicked, active_beat)
    if beat_changed:
        play_notes()
        beat_changed = False

    for event in py.event.get():
        if event.type == py.QUIT:
            run = False
        if event.type == py.MOUSEBUTTONDOWN:
            for i in range(len(boxes)):
                if boxes[i][0].collidepoint(event.pos):
                    coords = boxes[i][1]
                    clicked[coords[1]][coords[0]] *= -1

    beat_length = 3600 // bpm

    if playing:
       if active_length < beat_length:
           active_length += 1
       else:
           active_length = 0
           if active_beat < beats - 1:
               active_beat += 1
               beat_changed = True
           else:
               active_beat  = 0
               beat_changed = True

    py.display.flip()
py.quit()

What it does so far:

Adding later: Different kits, a menu to select the kits, bpm change, and more.

def play_notes does as the name says… it plays the notes.

def draw_grid just makes the interface and puts them on the screen. It shows the names and the initial design of the program.

def draw_grid:

def draw_grid(clicks, beat):
    # Making the boxes for the interface
    left_box = py.draw.rect(screen, grey, [0, 0, 200, HEIGHT - 200], 5)
    bottom_box = py.draw.rect(screen, grey, [0, HEIGHT - 200, WIDTH, 200], 5)
    boxes = []
    colors = [grey, white, grey]
    # Making the names of all the instruments
    hi_hat_text = label_font.render('Hi-hat', True, white)
    screen.blit(hi_hat_text, (30, 30))
    snare_text = label_font.render('Snare', True, white)
    screen.blit(snare_text, (30, 130))
    kick_text = label_font.render('Kick', True, white)
    screen.blit(kick_text, (30, 230))
    crash_text = label_font.render('Crash', True, white)
    screen.blit(crash_text, (30, 330))
    clap_text = label_font.render('Clap', True, white)
    screen.blit(clap_text, (30, 430))
    floor_tom_text = label_font.render('Floor Tom', True, white)
    screen.blit(floor_tom_text, (30, 530))
    # draw some lines between the instrument names
    for i in range(instrumnents):
        py.draw.line(screen, grey, (0, (i * 100) + 100), (200, (i * 100) + 100), 5)
    # check how many boxes there are for each instrument
    for i in range(beats):
        for j in range(instrumnents):
            if clicks[j][i] == -1:
                color = grey
            else:
                color = green
                # Making a tri-color effect with rects
            rect = py.draw.rect(screen, color, 
            [i * ((WIDTH - 200) // beats) + 205, (j * 100) + 5, ((WIDTH - 200) // beats) - 10, 
            ((HEIGHT- 200)// instrumnents) - 10], 0, 3)
            # Blank/ NOT colorful rect
            py.draw.rect(screen, gold, 
            [i * ((WIDTH - 200) // beats) + 200, (j * 100), ((WIDTH - 200) // beats), 
             ((HEIGHT- 200)// instrumnents)], 5, 5)
            py.draw.rect(screen, black, 
            [i * ((WIDTH - 200) // beats) + 200, (j * 100), ((WIDTH - 200) // beats), 
             ((HEIGHT- 200)// instrumnents)], 2, 5)
            boxes.append((rect, (i, j)))
        active = py.draw.rect(screen, blue, [beat * ((WIDTH-200)//beats) + 200, 0, 
                ((WIDTH-200)//beats), instrumnents*100], 5, 3)
    return boxes

Voxel Engine in Python and OpenGL(Blog Post 10/4/24)

MAKING THE OPENGL WINDOW

To install OpenGL and other modules:

pip install PyOpenGL
pip install pygame moderngl PyGLM numba

First import the common modules for the program in “settings.py”.

from numba import njit
import numpy as np
import glm
import math

Put in the respective background color and window resolution in “settings.py”.

# Window resolution
WIN_RES = glm.vec2(1600, 900)

# Colors
BG_COLOR = glm.vec3(0.1, 0.16, 0.25)

Import modules in “main.py” and create a class of VoxelEngine.

from settings import *
import moderngl as mgl
import pygame as pg
import sys

class VoxelEngine:

class VoxelEngine:
    def __init__(self):
        pg.init()
        pg.display.gl_set_attribute(pg.GL_CONTEXT_MAJOR_VERSION, 3)
        pg.display.gl_set_attribute(pg.GL_CONTEXT_MINOR_VERSION, 3)
        pg.display.gl_set_attribute(pg.GL_CONTEXT_PROFILE_MASK, pg.GL_CONTEXT_PROFILE_CORE)
        pg.display.gl_set_attribute(pg.GL_DEPTH_SIZE, 24)

        pg.display.set_mode(WIN_RES, flags=pg.OPENGL | pg.DOUBLEBUF)
        self.ctx = mgl.create_context()

        self.ctx.enable(flags=mgl.DEPTH_TEST | mgl.CULL_FACE | mgl.BLEND)
        self.ctx.gc_mode = 'auto'

        self.clock = pg.time.Clock()
        self.delta_time = 0
        self.time = 0

        self.is_running = True

    def update(self):
        self.delta_time = self.clock.tick()
        self.time = pg.time.get_ticks() * 0.001
        pg.display.set_caption(f'{self.clock.get_fps() :.0f}')

    def render(self):
        self.ctx.clear(color=BG_COLOR)
        pg.display.flip()

    def handle_events(self):
        for event in pg.event.get():
            if event.type == pg.QUIT or (event.type == pg.KEYDOWN and event.key == pg.K_ESCAPE):
                self.is_running = False

    def run(self):
        while self.is_running:
            self.handle_events()
            self.update()
            self.render()
        pg.quit()
        sys.exit()

if __name__ == '__main__':
    app = VoxelEngine()
    app.run()

(this will get larger overtime)

Define the attributes for the program (in “def __init__”).

pg.init()
        pg.display.gl_set_attribute(pg.GL_CONTEXT_MAJOR_VERSION, 3)
        pg.display.gl_set_attribute(pg.GL_CONTEXT_MINOR_VERSION, 3)
        pg.display.gl_set_attribute(pg.GL_CONTEXT_PROFILE_MASK, pg.GL_CONTEXT_PROFILE_CORE)

Set the window resolution and create OpenGL context

  pg.display.set_mode(WIN_RES, flags=pg.OPENGL | pg.DOUBLEBUF)
        self.ctx = mgl.create_context()

Enable the context and set the garbage collection mode(gc_mode) to ‘auto’

self.ctx.enable(flags=mgl.DEPTH_TEST | mgl.CULL_FACE | mgl.BLEND)
        self.ctx.gc_mode = 'auto'

After that, set a time variable to keep the window on the screen a variable to run things in the main function

self.clock = pg.time.Clock()
        self.delta_time = 0
        self.time = 0

        self.is_running = True

In “def update()”, set the delta time and update its tick for every frame, then put it as a caption on the top left-hand corner of the screen.

self.delta_time = self.clock.tick()
        self.time = pg.time.get_ticks() * 0.001
        pg.display.set_caption(f'{self.clock.get_fps() :.0f}')

Plaster the background color from “settings.py” and update it to the screen in “def render()”

 self.ctx.clear(color=BG_COLOR)
        pg.display.flip()

In “def handle_events()”, make a for loop that checks if the player has clicked the “x” on the window or the down arrow key and the “escape” key (Esc) and will turn “is_running” to be false, which closes the window.

def handle_events(self):
        for event in pg.event.get():
            if event.type == pg.QUIT or (event.type == pg.KEYDOWN and event.key == pg.K_ESCAPE):
                self.is_running = False

Initialize all functions in “def run()”.

def run(self):
        while self.is_running:
            self.handle_events()
            self.update()
            self.render()
        pg.quit()
        sys.exit()

and this too for the code to actually work 🙂

if __name__ == '__main__':
    app = VoxelEngine()
    app.run()

RESULT:

W/O the background color:

WITH the background:

I’m now working on the initial setup 😀

9/36/24 Blog Post

making 2048 in Python.

SETTING UP THE SCREEN

Made a blank pygame screen.

Made it using the WINDOW constant variable.

WINDOW = py.display.set_mode((WIDTH, HEIGHT))

The tuple (WIDTH, HEIGHT) are also constants set at 800 by 800.

WIDTH, HEIGHT = 800, 800

DRAWING THE WINDOW

Made a function of draw to fill the window with the background color (BG_COLOR).

Outside draw()

BG_COLOR = (205, 192, 180)

Used the variable to fill the window color and then displaying it on the window.

def draw()

def draw(window):
    window.fill(BG_COLOR)
    draw_grid(window)
    py.display.update()

Added draw() to the main loop

Main Loop:

def main(window):
    clock = py.time.Clock()
    run = True
    while run:
        clock.tick(FPS)
 
        for event in py.event.get():
            if event.type == py.QUIT:
                run = False
                break
        draw(window)
            
    py.quit()

RESULT:

9/15/24 Blog Post

MADE COLLISION AND GRAVITY FOR THE JETPACK TO WORK!!!!!

Added a y velocity and gravity for the player and jetpack.

y_vel = 0 

grav = 0.2

made a if statement for the jetpack to work

if not pause:
        if booster:
            y_vel -= grav
            #makes player go up when pressing button
        else: y_vel += grav

        if (colliding[0] and y_vel > 0) or (colliding[1] and y_vel < 0):
            y_vel = 0
        #makes player go down when not pressing button
        player_y += y_vel

if the player presses the key down, the y vel will become negative, causing the player to go up (- gravity). if the player lets go of the key, the y vel will become positive, causing the player to go down (+ gravity).

colliding is a function to check_colliding. colliding[0] is the top of the game’s screen, and colliding[1] is the bottom. when the player goes up. check_colliding will check if the player has reached the top, and will add collision. Same thing will happen to the bottom of the screen.

check_colliding function

def check_colliding():
    coll = [False, False]
    if player.colliderect(bottom_plat):
        coll[0] = True
    elif player.colliderect(top_plat):
        coll[1] = True
    return coll

RESULT.

9/5/24 Blog post: Jetpack joyride

FULL code (so far)

#Jetpack Joyride in Python

import random
import pygame as py

py.init()

WIDTH = 1000
HEIGHT = 600

screen = py.display.set_mode([WIDTH, HEIGHT])
surface = py.Surface((WIDTH, HEIGHT), py.SRCALPHA)
py.display.set_caption("Jetpack Joyride Remake")
FPS = 60
timer = py.time.Clock()
font = py.font.Font('freesansbold.ttf', 32)
BG_COLOR = (128, 128, 128)
lines = [0, WIDTH//4, 2* WIDTH//4, 3* WIDTH//4]
game_speed = 2
pause = False
init_y = HEIGHT - 130
player_y = init_y
booster = False
counter = 0


#Drawing the background screen
def draw_screen(line_list):
    screen.fill("black")
    py.draw.rect(surface, (BG_COLOR[0], BG_COLOR[1], BG_COLOR[2], 50), [0, 0, WIDTH, HEIGHT])
    screen.blit(surface, (0, 0))
    top_rect = py.draw.rect(screen, 'gray', [0, 0, WIDTH, 50])
    bottom_rect = py.draw.rect(screen, 'gray', [0, HEIGHT - 50, WIDTH, 50])

    #making the infinite lines on the screen
    for i in range(len(line_list)):
        py.draw.line(screen, 'black', (line_list[i], 0), (line_list[i], 50), 3)
        py.draw.line(screen, 'black', (line_list[i], HEIGHT - 50), (line_list[i], HEIGHT), 3)
        if not pause:
            line_list[i] -= game_speed
        if line_list[i] < 0:
            line_list[i] = WIDTH
    return line_list, top_rect, bottom_rect


#Dawing the player and animated states
def draw_player():
    player_hitbox = py.rect.Rect((120, player_y + 10), (25, 60)) # Player' hitbox
    if player_y < init_y or pause:
        if booster:
            py.draw.ellipse(screen, 'red', [100, player_y + 50, 20, 30])# overall body of the flame
            py.draw.ellipse(screen, 'orange', [105, player_y + 50, 10, 30])# detail
            py.draw.ellipse(screen, 'yellow', [110, player_y + 50, 5, 30])# also detail
        py.draw.rect(screen,'#ffca69', [128, player_y + 60, 10, 20], 0, 3)#ligher leg
        py.draw.rect(screen,'#ffa600', [130, player_y + 60, 10, 20], 0, 3)#darker leg
    else:
        #drawing the animated legs
        if counter < 10:
            py.draw.line(screen, '#ffca69', [128, player_y + 60], [140, player_y + 80], 10)#lighter leg
            py.draw.line(screen, '#ffa600', [130, player_y + 60], [120, player_y + 80], 10)#darker leg
        elif 10 <= counter < 20:
            py.draw.rect(screen,'#ffca69', [128, player_y + 60, 10, 20], 0, 3)#ligher leg
            py.draw.rect(screen,'#ffa600', [130, player_y + 60, 10, 20], 0, 3)#darker leg
        elif 20 <= counter < 20:
            py.draw.line(screen, '#ffca69', [128, player_y + 60], [120, player_y + 80], 10)#lighter leg
            py.draw.line(screen, '#ffa600', [130, player_y + 60], [140, player_y + 80], 10)#darker leg
        else:
            py.draw.rect(screen,'#ffca69', [128, player_y + 60, 10, 20], 0, 3)#ligher leg
            py.draw.rect(screen,'#ffa600', [130, player_y + 60, 10, 20], 0, 3)#darker leg

    # Player's body        
    py.draw.rect(screen, 'white', [100, player_y + 20, 20, 30], 0, 5) # Player's jetpack
    py.draw.ellipse(screen, '#ffa600', [120, player_y + 20, 20, 50]) # Player's main body
    py.draw.circle(screen, '#ffa600', (135, player_y + 15), 10)# Player's head
    py.draw.circle(screen, 'black', (138, player_y + 12), 3)# Player's head
    return player_hitbox

# Main loop
run = True
while run:
    timer.tick(FPS)
    if counter < 40:
        counter += 1
    else: 
        counter = 0
    lines, top_plat, bottom_plat = draw_screen(lines)
    player = draw_player()

    for event in py.event.get():
        if event.type == py.QUIT:
            run = False

    py.display.flip()
py.quit()

DRAWING THE BACKGROUND SCREEN (and making the lines)!!!

“BG_COLOR” is the gray rectangle at the top and bottom of the screen evenly separated by the lines. They are then made to be transparent where the player and obstacles are going to be placed.

(Kinda) Small Weather App project

main.py

get_weather() gets the weather info from the OpenWeatherApp Api website.

def get_weather(city):
    '''gets weather info from OpenWeatherMap API'''
    API_key = "***********************"
    url = f"https://api.openweathermap.org/data/2.5/weather?q={city}&appid={API_key}"
    res = requests.get(url)
    
    if res.status_code == 404:
        messagebox.showerror("Error", "City not found")
        return None
    
    # Parse response JSON to get weather info
    weather = res.json()
    icon_id = weather['weather'][0]['icon']
    temperature = weather['main']['temp'] - 273.15

it also gets the temperature and icon to make it into readable text.

search() allows the user to search for the current weather of a city.

mian.py

def search():
    '''Searches the current weather of a city'''
    city = city_entry.get()
    result = get_weather(city)
    if result is None:
        return

The rest is the set up for the GUI.

window = ttkbootstrap.Window(themename="morph")
window.title("Weather App")
window.geometry("400x400")

# Entry widget - enter city name
city_entry = ttkbootstrap.Entry(window, font="Helvetica, 18")
city_entry.pack(pady=10)

# Button widget - search for weather information
search_button = ttkbootstrap.Button(window, text="Search", 
command=search, bootstyle="warning")
search_button.pack(pady=10)

# label widget - show country/city name
location_label = tk.Label(window, font="Helvetica, 25")
location_label.pack(pady=20)

# Label widget - show weather icon
icon_label = tk.Label(window)
icon_label.pack()

# Label widget - show the temperature
temperature_label = tk.Label(window, font="Helvetica, 20")
temperature_label.pack()

# Label widget - show weather desc.
desc_label = tk.Label(window, font="Helvetica, 20")
desc_label.pack()

window.mainloop()

That’s really all I did… 1. because I started a little late and 2. it took too long for me to get an account for the API key

March 14 Blog Post: Mario Maker Game

Added the Y tile lines for the game

editor.py

for row in range(rows + 1):
            y = origin_offset.y + row * TILE_SIZE
            pygame.draw.line(self.display_surface, LINE_COLOR, (0,y), (WINDOW_WIDTH,y))

Same as x, but it makes lines horizontal from the left to the right of the window

This is what the it should look like. The y tile lines will be horizontal, and the x tile lines will be vertical.

Result:

Tried to make the lines green so it will look better and…

oh boy…

Okay I took out the green and made the lines a little transparent.

Changing the mouse cursor

Changed the mouse cursor to this:

What we need in order to change the mouse is to find the clickable area, which would be somewhere around the tip of the mouse, the rest is the attached stuff to it.

main.py

surf = load('.//graphics//cursor//mouse.png').convert_alpha()
		cursor = pygame.cursors.Cursor((0,0), surf)
		pygame.mouse.set_cursor(cursor)

We load the image in, then we set where it should be clickable. In this case, it’s (o,0). After that, we replace the mouse with the cursor that we have.

result:

Creating the menu

To make the menu work, in editor.py we’ll have a variable called selection_index that will have value between 2 and 18. Each number represents a certain kind of tile in the editor.

Ex. 2: water, 3; terrain, 4: gold coin and so on.

The selection_index can be changed by clicking on the menu or via hotkeys (this is where that colossal file settings.py comes in).

settings.py

# general setup
TILE_SIZE = 64
WINDOW_WIDTH = 1280
WINDOW_HEIGHT = 720
ANIMATION_SPEED = 8

# editor graphics 
EDITOR_DATA = {
	0: {'style': 'player', 'type': 'object', 'menu': None, 'menu_surf': None, 'preview': None, 'graphics': '../graphics/player/idle_right'},
	1: {'style': 'sky',    'type': 'object', 'menu': None, 'menu_surf': None, 'preview': None, 'graphics': None},
	
	2: {'style': 'terrain', 'type': 'tile', 'menu': 'terrain', 'menu_surf': '../graphics/menu/land.png',  'preview': '../graphics/preview/land.png',  'graphics': None},
	3: {'style': 'water',   'type': 'tile', 'menu': 'terrain', 'menu_surf': '../graphics/menu/water.png', 'preview': '../graphics/preview/water.png', 'graphics': '../graphics/terrain/water/animation'},
	
	4: {'style': 'coin', 'type': 'tile', 'menu': 'coin', 'menu_surf': '../graphics/menu/gold.png',    'preview': '../graphics/preview/gold.png',    'graphics': '../graphics/items/gold'},
	5: {'style': 'coin', 'type': 'tile', 'menu': 'coin', 'menu_surf': '../graphics/menu/silver.png',  'preview': '../graphics/preview/silver.png',  'graphics': '../graphics/items/silver'},
	6: {'style': 'coin', 'type': 'tile', 'menu': 'coin', 'menu_surf': '../graphics/menu/diamond.png', 'preview': '../graphics/preview/diamond.png', 'graphics': '../graphics/items/diamond'},

	7:  {'style': 'enemy', 'type': 'tile', 'menu': 'enemy', 'menu_surf': '../graphics/menu/spikes.png',      'preview': '../graphics/preview/spikes.png',      'graphics': '../graphics/enemies/spikes'},
	8:  {'style': 'enemy', 'type': 'tile', 'menu': 'enemy', 'menu_surf': '../graphics/menu/tooth.png',       'preview': '../graphics/preview/tooth.png',       'graphics': '../graphics/enemies/tooth/idle'},
	9:  {'style': 'enemy', 'type': 'tile', 'menu': 'enemy', 'menu_surf': '../graphics/menu/shell_left.png',  'preview': '../graphics/preview/shell_left.png',  'graphics': '../graphics/enemies/shell_left/idle'},
	10: {'style': 'enemy', 'type': 'tile', 'menu': 'enemy', 'menu_surf': '../graphics/menu/shell_right.png', 'preview': '../graphics/preview/shell_right.png', 'graphics': '../graphics/enemies/shell_right/idle'},
	
	11: {'style': 'palm_fg', 'type': 'object', 'menu': 'palm fg', 'menu_surf': '../graphics/menu/small_fg.png', 'preview': '../graphics/preview/small_fg.png', 'graphics': '../graphics/terrain/palm/small_fg'},
	12: {'style': 'palm_fg', 'type': 'object', 'menu': 'palm fg', 'menu_surf': '../graphics/menu/large_fg.png', 'preview': '../graphics/preview/large_fg.png', 'graphics': '../graphics/terrain/palm/large_fg'},
	13: {'style': 'palm_fg', 'type': 'object', 'menu': 'palm fg', 'menu_surf': '../graphics/menu/left_fg.png',  'preview': '../graphics/preview/left_fg.png',  'graphics': '../graphics/terrain/palm/left_fg'},
	14: {'style': 'palm_fg', 'type': 'object', 'menu': 'palm fg', 'menu_surf': '../graphics/menu/right_fg.png', 'preview': '../graphics/preview/right_fg.png', 'graphics': '../graphics/terrain/palm/right_fg'},

	15: {'style': 'palm_bg', 'type': 'object', 'menu': 'palm bg', 'menu_surf': '../graphics/menu/small_bg.png', 'preview': '../graphics/preview/small_bg.png', 'graphics': '../graphics/terrain/palm/small_bg'},
	16: {'style': 'palm_bg', 'type': 'object', 'menu': 'palm bg', 'menu_surf': '../graphics/menu/large_bg.png', 'preview': '../graphics/preview/large_bg.png', 'graphics': '../graphics/terrain/palm/large_bg'},
	17: {'style': 'palm_bg', 'type': 'object', 'menu': 'palm bg', 'menu_surf': '../graphics/menu/left_bg.png',  'preview': '../graphics/preview/left_bg.png',  'graphics': '../graphics/terrain/palm/left_bg'},
	18: {'style': 'palm_bg', 'type': 'object', 'menu': 'palm bg', 'menu_surf': '../graphics/menu/right_bg.png', 'preview': '../graphics/preview/right_bg.png', 'graphics': '../graphics/terrain/palm/right_bg'},
}

NEIGHBOR_DIRECTIONS = {
	'A': (0,-1),
	'B': (1,-1),
	'C': (1,0),
	'D': (1,1),
	'E': (0,1),
	'F': (-1,1),
	'G': (-1,0),
	'H': (-1,-1)
}

LEVEL_LAYERS = {
	'clouds': 1,
	'ocean': 2,
	'bg': 3,
	'water': 4,
	'main': 5
}

# colors 
SKY_COLOR = '#ddc6a1'
SEA_COLOR = '#92a9ce'
HORIZON_COLOR = '#f5f1de'
HORIZON_TOP_COLOR = '#d1aa9d'
LINE_COLOR = 'black'

what we will need are the indexes 2-18. 0 and 1 are ignored b/c 0 is the player and 1 is the sky. They will also be in the editor so they don’t need to be created.

2-18 the player can create like the terrain, coin, etc…

Making the hotkeys for the menu

editor.py

def selection_hotkeys(self, event):
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_RIGHT:
                self.selection_index += 1
            if event.key == pygame.K_LEFT:
                self.selection_index -= 1

Detects if the user is pressing a button. It’s not checking if we’re holding down a button so there’s no need for a timer.

The problem is that when you repeatedly press the button the number can go below 0 and above 18.

Above 18 when pressing right arrow.

Below 0 when pressing left arrow.

To fix it we just needed one line of code.

editor.py

self.selection_index = max(2,min(self.selection_index,18))

This will cap the max at 18 and the min at 2.

Building the menu

First, we’ll create the general area of the menu.

In the menu area we’ll have smaller squares the user can click on.

menu.py

size = 180
margin = 6

The width and height of the menu will be size. The left and top of the menu will be the WINDOW_WIDTH – size – margin.

Then we draw it on the window.

menu.py

pygame.draw.rect(self.display_surface, 'blue', self.rect)

The menu took a little bit for me b/c there was something wrong with the place of the rectangle. It kept going offscreen (I put a random number for one of the window dimensions instead of its variable).

March 13 Blog Post: Mario Maker Game

Made the scroll wheel move the origin up, down, left and right

editor.py

if event.type == pygame.MOUSEWHEEL:
            if pygame.key.get_pressed()[pygame.K_LCTRL]:
                self.origin.y -= event.y * 50
            else:
                self.origin.x -= event.y * 50

Allows the origin to move or right WITHOUT holding down left CTRL and up or down when holding down left CTRL.

Without holding down left CTRL:

Holding down left CTRL:

That’s really it for the pan input. Nothing extremely complex. Later on we’ll have to add more stuff, but for now this is good.

Drawing the tile lines to find what location we are in the grid

Making the tile lines should be infinite and relative to the origin point.

All of the lines should start at the position 0 (start of the window) and end at the height (the bottom) of the window. The x position for the top and bottom of the line should be the same.

Very rough sketch of what I’m trying to explain:

We would find this out by this code:

editor.py

for col in range(cols):
            x = self.origin.x + col * TILE_SIZE
            pygame.draw.line(self.display_surface, LINE_COLOR, (x,0), (x,WINDOW_HEIGHT))

Let’s say that the position of the origin point is (0,0). We would be getting that from self.origin.x. x = 0. We then add that to the column (which is 0) and then multiply by the TILE_SIZE (which will always be 64). The answer to that equation will be 0, which will be the line at the left side of the window.

if col is 1 and we multiply that by 64, we’ll get 64, which will move from the first point to the point to the right and so on until we reach the end of the columns.

If we run it, this is our result:

When you move the origin point the lines will move with it.

There’s a problem though. We run out of points when we move and there’s just a giant empty white space. The same thing happens when you move the origin point to the left.

What we have to do is make sure that the lines are always on the display and that we’re never running out of lines.

We will have to create columns between the origin’s position and the next column so that we can move the origin wherever we want (and so they don’t go out the window).

To do this, we have to divide origin_offset with the TILE_SIZE. Let’s say it is 100. We divide that by the TILE_SIZE (64) and then multiply that number by the TILE_SIZE, which would be 1, which means we’re in the 1st column. Same thing for y.

editor.py

origin_offset = vector(
        offset_vector.x - int(self.origin.x / TILE_SIZE)* TILE_SIZE, 
        y = self.origin.y - int(self.origin.y / TILE_SIZE)* TILE_SIZE)

Result:

That’s all I did. Keep in mind that this tutorial is over 10 hours long…