Pygame学习笔记 6 —— 3D游戏

2022-09-01 15:49:39 浏览数 (1)

大家好,又见面了,我是你们的朋友全栈君。

    pygame是是上世纪的产品,虽然不适合最3D游戏,但我可以使用pygame来绘制简单的3D图形,就像在白纸上画立体图形一样。

主要内容: 视觉上的远近、3D空间、绘制一个空间图形


一、视觉上的远近

  人的视觉总是遵循一个原则:近大远小。想象一下,如果有一天躺在草地上什么都不做,仰望星空,天空突然飞过流星雨,离我们近的流星看起来就很狭长而明亮,离我们远的则呈一个暗点状,我们完全可以用pygame模拟出来:

step1:定义Star类

  要描述一颗流星,需要有坐标和速度。在定义一个列表来存储点集:

代码语言:javascript复制
class Star(object):
	def __init__(self, x, y, speed):
		self.x = x
		self.y = y
		self.speed = speed

stars = []

step2:生成随机点   我们需要定时的生成流星

代码语言:javascript复制
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:绘制短线   因为近的流星彗尾看起来更狭长,所以我们可以根据速度来绘制一条短线来表示流星。可以使用pygaame的time库函数Clock测量时间,根据时间和速度在不同位置绘制不同长度的短线:

代码语言:javascript复制
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:查看效果

代码语言:javascript复制
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()

  这里我们需要注意一个细节,我们定义了一个on_screen函数,若一个点在屏幕上则返回值为True,使用filter函数将不在屏幕的函数过滤掉,而保留屏幕上的点(不在屏幕上的点会消耗资源)。

代码语言:javascript复制
def on_screen(star):
			return star.x > 0
			
		stars = list(filter(on_screen, stars))

  我们不断生成长短不一运动的短线,看起来就像流星一样✨


二、3D空间

  首先,我们定义我们的坐标系,z粥朝向我们,x轴朝右,就和pygame窗口的x轴一样,y轴朝上,和pygame窗口的y轴方向相反。

投影:平行投影   投影最简单的办法——丢弃z坐标

代码语言:javascript复制
def parallel_project(vector3):
	return(vector3.x, vector3.y)

  这种方法简单,但得到的效果很不理想,基本上看不出透视效果。

投影:立体投影   这种投影的效果更接近真实,在它利用透视法把远处的物体缩小了。游戏中广泛应用。

代码语言:javascript复制
def perspective_project(vector3, d):
    x, y, z = vector3
    return (x*d/z, -y*d/z)

  由于近大远小,所以我们的结果除以一个z,同时,这里考虑了视距d(观察者到屏幕的距离),如下图。 类比:可以想象通过一个孔观察事物,当眼睛远离孔(视距增大)时,视野(fov)减小,看到的内容自然就变少了,当眼睛靠近孔时,视距减小,视野增大,看到的内容增多了。 类比:可以把z看作观察者到被观测物体之间的观测距离,可以看成照相机到物体的距离,当距离z增大时,看到的物体变小,符合透视原理。

视距:计算视距

代码语言:javascript复制
from math import tan
def calculate_viewing_distance(fov, screen_width):
	d = (screen_width/2.0) / tan(fov/2.0)
	return d

三、绘制一个空间图形

  我们可以根据上面所学的,结合数学基础,可以绘制一个3D图形,关键代码如下:

代码语言:javascript复制
 # 绘制点
        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))

  在这里,我们先定义了一个相机的位置camera_position,它代表了观测者的位置,初始值为(0, 0, -700),因为要除以距离z,所以我们先将z设的比较大(因为如果z很小,如接近0,则(x, y)就接近(∞, ∞)了),之后我们将(x, y)加上center_y, center_y,目的是吧图形绘制在屏幕中央,最后根据ball的尺寸将球绘制在对于的位置。

  完整代码如下:

代码语言:javascript复制
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])

    # 正方体边上的点,每条边16个点
    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)

                # 以(0, 0, 0)为顶点的正方体边的坐标性质
                if sum((edge_x, edge_y, edge_z)) >= 2:

                    # 移动到以(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)

    # 以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大小有范围 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

        # 绘制点
        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))

        # 绘制表
        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)

        # 绘制文字
        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()

下面就是我们最终的效果啦:


第 6 篇pygame学习笔记完结 cheers! ?


参考博客:用Python和Pygame写游戏-从入门到精通 图标引用:Iconfont

发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/141355.html原文链接:https://javaforall.cn

0 人点赞