Pygame Learning Notes 6-3D Game

Posted by rahish on Sat, 10 Aug 2019 06:34:07 +0200

pygame is a product of the past century. Although it is not suitable for the most 3D games, I can use pygame to draw simple 3D graphics, just like a stereo on white paper.

Main Contents: Visual proximity, 3D space, drawing a spatial graphic


1. Visual proximity

_People's vision always follows a principle: near big, far small.Imagine if one day you lie on the grass and do nothing, look up at the stars, and the sky suddenly flies through the shower. The meteors near us look very long and bright, and the ones far away look like dark spots. We can use pygame to simulate this:

step1: Define the Star class

To describe a shooting star, coordinates and speeds are required.In defining a list to store the point set:

class Star(object):
	def __init__(self, x, y, speed):
		self.x = x
		self.y = y
		self.speed = speed

stars = []

step2: generate random points
_We need to generate shooters regularly

x = float(randint(0, 639))
y = float(randint(0, 479))
speed = float(randint(10, 300))
stars.append(Star(x, y, speed))

while True:
	y = float(randint(0, 479))
	speed = float(randint(10, 300))
	stars.append(Star(640., y, speed))

step3: draw short lines
_Because the tail of a nearby meteor appears to be longer, we can draw a short line to represent the shooting star based on its speed.You can use pygaame's time library function Clock to measure time and draw short lines of different lengths at different locations according to time and speed:

while True:
	for star in stars:
		new_x = star.x - star.speed*time_passed_seconds
		pygame.draw.aaline(screen, white, (new_x, star.y), (star.x+1, star.y))
		star.x = new_x

step4: view the effect

import pygame
from pygame.locals import *
from random import randint

class Star(object):
	def __init__(self, x, y, speed):
		self.x = x
		self.y = y
		self.speed = speed
		
def run():
	pygame.init()
	screen = pygame.display.set_mode((640, 480), 0, 32)

	stars = []
	for k in range(200):
		x = float(randint(0, 639))
		y = float(randint(0, 479))
		speed = float(randint(10, 300))
		stars.append(Star(x, y, speed))

	clock = pygame.time.Clock()
	white = (255, 255, 255)

	while True:
		for event in pygame.event.get():
			if event.type == QUIT:
				pygame.quit()
				return
			elif event.type == KEYDOWN:
				return

		y = float(randint(0, 479))
		speed = float(randint(10, 300))
		stars.append(Star(640., y, speed))

		time_passed = clock.tick(60)
		time_passed_seconds = time_passed / 1000

		screen.fill((0, 0, 0))
		
		for star in stars:
			new_x = star.x - star.speed*time_passed_seconds
			pygame.draw.aaline(screen, white, (new_x, star.y), (star.x+1, star.y))
			star.x = new_x

		def on_screen(star):
			return star.x > 0
			
		stars = list(filter(on_screen, stars))

		pygame.display.update()

if __name__ == "__main__":
	run()

_Here we need to pay attention to a detail, we define an on_screen function, if a point on the screen returns a value of True, use the filter function to filter out functions that are not on the screen, and keep the points on the screen (points that are not on the screen consume resources).

def on_screen(star):
			return star.x > 0
			
		stars = list(filter(on_screen, stars))

_We keep generating short, variable-length, moving lines that look like shooting stars



2. 3D Space

_Firstly, we define our coordinate system with porridge z facing us and the x-axis facing right, just like the x-axis of the pygame window, the y-axis facing up is opposite to the Y-axis of the pygame window.

Projection: Parallel projection
The easiest way to project_is to discard z-coordinates

def parallel_project(vector3):
	return(vector3.x, vector3.y)

_This method is simple, but the result is not ideal, basically no perspective effect can be seen.

Projection: Stereographic projection
_This projection is more realistic in that it uses perspective to reduce distant objects.Game is widely used.

def perspective_project(vector3, d):
    x, y, z = vector3
    return (x*d/z, -y*d/z)

_Because of the size, our result is divided by a z, taking into account the distance d (the distance from the observer to the screen), as shown below.
_Analogue: It can be imagined that when you look through a hole, when the eye is away from the hole (the distance increases), the field of vision (fov) decreases, and the content you see naturally decreases. When the eye is close to the hole, the distance of sight decreases, the field of vision increases, and the content you see increases.
_Analogue: z can be regarded as the distance between the observer and the observed object, and the distance from the camera to the object. As the distance z increases, the object that you see decreases, which conforms to the principle of perspective.


Line of Sight: Calculate Line of Sight

from math import tan
def calculate_viewing_distance(fov, screen_width):
	d = (screen_width/2.0) / tan(fov/2.0)
	return d


3. Draw a spatial graphic

_Based on what we have learned above and combined with the basic mathematics, we can draw a 3D graphic with the following key codes:

 # Draw Points
        for point in points:
            x, y, z = point - camera_position
            x = x * viewing_distance / z
            y = -y * viewing_distance / z
            x += center_x
            y += center_y
            screen.blit(ball, (x-ball_center_x, y-ball_center_y))

_Here, we first define the camera_position of a camera, which represents the observer's position, and the initial value is (0, 0, -700), because to divide by the distance z, we first set Z larger (because if Z is very small, if it is close to 0, (x, y) is close to (,), then we add (x, y) plus center_y, center_y. The purpose is to draw the graphics in the center of the screen, and finally to draw the ball in the opposite position according to the size of the ball.

_The complete code is as follows:

import pygame
from pygame.locals import *
from gameobjects.vector3 import Vector3

from math import *
from random import randint

SCREEN_SIZE = (640, 480)
CUBE_SIZE = 300

def caculate_viewing_distance(fov, screen_width):
    d = (screen_width/2.0) / tan(fov/2.0)
    return d

def run():
    pygame.init()
    screen = pygame.display.set_mode(SCREEN_SIZE, 0, 32)

    my_font = pygame.font.SysFont("arial", 23)

    ball = pygame.image.load("ball.png")

    # 3D points
    points = []

    fov = 90.0 # Field of view
    viewing_distance = caculate_viewing_distance(radians(fov), SCREEN_SIZE[0])

    # Points on the edge of a cube, 16 points per edge
    for x in range(0, CUBE_SIZE+1, 20):
        edge_x = (x == 0 or x == CUBE_SIZE)

        for y in range(0, CUBE_SIZE+1, 20):
            edge_y = (y ==0 or y == CUBE_SIZE)

            for z in range(0, CUBE_SIZE+1, 20):
                edge_z = (z == 0 or z == CUBE_SIZE)

                # Coordinate properties of cube edges with vertices (0, 0, 0)
                if sum((edge_x, edge_y, edge_z)) >= 2:

                    # Move to centered (0, 0, 0)
                    point_x = float(x) - CUBE_SIZE/2
                    point_y = float(y) - CUBE_SIZE/2
                    point_z = float(z) - CUBE_SIZE/2

                    point = Vector3(point_x, point_y, point_z)
                    points.append(point)

    # Sort from large to small in point_z
    def point_z(point):
        return point.z
    points.sort(key=point_z, reverse=True)

    center_x, center_y = SCREEN_SIZE
    center_x /= 2
    center_y /= 2

    ball_w, ball_h = ball.get_size()
    ball_center_x = ball_w / 2
    ball_center_y = ball_h / 2

    camera_position = Vector3(0.0, 0.0, -700.0)
    camara_speed = Vector3(300.0, 300.0, 300.0)

    clock = pygame.time.Clock()

    while True:

        for event in pygame.event.get():
            if event.type == QUIT:
                return

        screen.fill((0, 0, 0))

        pressed_keys = pygame.key.get_pressed()

        time_passed = clock.tick()
        time_pass_seconds = time_passed / 1000.0

        direction = Vector3()
        if pressed_keys[K_LEFT]:
            direction.x = -1.0
        elif pressed_keys[K_RIGHT]:
            direction.x = +1.0

        if pressed_keys[K_UP]:
            direction.y = +1.0
        elif pressed_keys[K_DOWN]:
            direction.y = -1.0

        if pressed_keys[K_q]:
            direction.z = +1.0
        elif pressed_keys[K_a]:
            direction.z = -1.0

        # fov sizes range from 0-179
        if pressed_keys[K_w]:
            fov = min(179.0, fov+0.2)
            w = SCREEN_SIZE[0]
            viewing_distance = caculate_viewing_distance(radians(fov), w)
        elif pressed_keys[K_s]:
            fov = max(1.0, fov-0.2)
            w = SCREEN_SIZE[0]
            viewing_distance = caculate_viewing_distance(radians(fov), w)

        camera_position += direction * camara_speed *  time_pass_seconds

        # Draw Points
        for point in points:

            x, y, z = point - camera_position
            x = x * viewing_distance / z
            y = -y * viewing_distance / z
            x += center_x
            y += center_y
            screen.blit(ball, (x-ball_center_x, y-ball_center_y))

        # Draw Table
        diagram_width = SCREEN_SIZE[0] / 4
        col = (50, 255, 50)
        diagram_points = []
        diagram_points.append((diagram_width/2, 100+viewing_distance/4))
        diagram_points.append( (0, 100) )
        diagram_points.append( (diagram_width, 100) )
        diagram_points.append( (diagram_width/2, 100+viewing_distance/4) )
        diagram_points.append( (diagram_width/2, 100) )
        pygame.draw.lines(screen, col, False, diagram_points, 2)

        # Draw Text
        white = (255, 255, 255)
        cam_text = my_font.render("camera = "+str(camera_position), True, white)
        screen.blit(cam_text, (5, 5))
        fov_text = my_font.render("field of view = %i"%int(fov), True, white)
        screen.blit(fov_text, (5, 35))
        txt = "viewing distance = %.3f"%viewing_distance
        d_text = my_font.render(txt, True, white)
        screen.blit(d_text, (5, 65))

        pygame.display.update()

if __name__ == "__main__":
    run()

Here's our final result:

6th Pgame Learning Notes Complete cheers!

Reference Blog: Write games with Python and Pygame - from getting started to mastering
Icon reference: Iconfont

Topics: Python