一.目的
二.代码实现
1.将地板绘制进去
和player一样,我们需要创建一个地板类,并将其变成精灵,加入精灵组,这样,只需要调用精灵组的draw函数就能顺带把地板给绘制进来了
我们新建一个sprites.py文件,我们会将以后写的所有精灵类都写到这个文件里,由于player这个精灵占的篇幅比较大,所以把它单独写到了player.py里面
现在,我们只需要在sprites.py里书写一个叫Generic的精灵类即可
import pygame
from settings import *
class Generic(pygame.sprite.Sprite):
def __init__(self,pos,surf,groups):
super().__init__(groups)
self.image = surf
self.rect = self.image.get_rect(topleft = pos)
然后把sprites.py中的所有东西都导入到level.py中
from sprites import *
接下来在level.py的setup函数中创建这个精灵类的对象即可
def setup(self):
self.player = Player((640,360),self.all_sprites)
Generic(
pos = (0,0),
surf = pygame.image.load('../graphics/world/ground.png').convert_alpha(),
groups = self.all_sprites
)
如此,就完成了Generic类的对象的创建,并且自动把它加入到了all_sprites这个精灵组里,随着精灵组的draw函数被调用,地板也被绘制了出来
2.z轴的引入
不过此时我们会发现,运行程序后只能看到游戏地板的图片,看不到主角了
因为我们先创建了player这个精灵,再创建了地板这个精灵,绘制的顺序也是这样的,这就导致了地板把我们的主角给覆盖掉了
这是一个2D游戏,这里所谓的Z轴就是告诉系统先画什么再画什么
LAYERS这个常量是一个字典,里面的key值是需要绘制的事物的名称,value值是它的优先级,其中优先级的数字越大,表示越晚绘制。
直到目前,我们只创建了其中的两个,一个是"ground",也就是地板,它的优先级是1,另一个是"main",也就是主角,它的优先级是7
按照这个优先级,系统就直到,应该先绘制地板,再绘制主角,这样主角就会覆盖到地板上面了
原作者在设置这两个类的z轴变量时,分别在player里直接赋值,而在Generic里面采取了传参赋值的方式,实在是很麻烦,如果是我就直接在Generic里面写self.z = LAYERS['ground']了。但是怕以后会用得到,还是按照原作者的方式写的。
这样我们还需要在创建这个对象的时候把z轴作为一个参数传递进去
3.重写精灵组的draw方法
这样,我我们在绘制精灵的时候,不仅要根据z轴的大小来决定先绘制谁,而且还要达到摄像机跟随主角的效果,作者提供的精灵组的draw方法显然是不能满足我们的需求的,因此我们需要重写它的draw方法
首先创建一个CameraGroup类继承自pygame提供的精灵组类,并且与此同时我们的all_sprites就不能是精灵组类的对象了,而是我们自己新写的CameraGroup类的对象。
接下来,我们获取游戏窗口,并且开始重写draw函数,实际上我们并不是重写draw函数,只是写了一个名为custon_draw的函数,并且在level类的run函数中不再调用draw
要实现按照z值的顺序绘制精灵实际上很简单,只需要用for循环遍历LAYERS里面的值,并且每次循环绘制出所有z值与当前所需要绘制的z值相同的精灵即可
最后,不要忘了把level的run函数里面调用的draw函数改为我们重新写的custon_draw函数
实际上,大部分的2D游戏,看似是主角在移动,实际上是背景图片在移动,主角一直牢牢锁定在屏幕的中间,如果要达到主角向右移动的效果,就让背景图片向左移动
这应用到了一些数学的知识,让我们先看看代码怎么写,再来详细讲一下里面的每一个变量代表了什么
self.offset是一个偏移量,代表了玩家 的实际坐标 距离屏幕的中心 的一个向量
图中绿色的方框代表我们的游戏窗口,红色的点代表玩家,此时玩家的坐标为(SCREEN_WIDTH/2,SCREEN_HEIGHT/2)
由self.offset.x = player.rect.centerx - SCREEN_WIDTH / 2 self.offset.y = player.rect.centery - SCREEN_HEIGHT / 2 可以得知,此时的self.offset = (0,0)
又由self.display_surface.blit(sprite.image, offset_rect)可知,我们在进行绘制精灵的时候,绘制的坐标并不是精灵实际的坐标,而是offset_rect这个变量
而offset_rect = sprite.rect.copy() offset_rect.center -= self.offset可知,offset_rect这个变量是由精灵的实际坐标减去self.offset带来的
那么此时,offset_rect = player.rect.center - self.offset = (SCREEN_WIDTH/2,SCREEN_HEIGHT/2) - (0,0) = (SCREEN_WIDTH/2,SCREEN_HEIGHT/2)
其中红色的点是玩家的真是坐标(player.rect.center)
那么self.offset就是 红色的点 减去 窗口的中心点 也就是 图中黑色的线指向的这个矢量
那么offset_rect 就是玩家的真实位置 减去这个矢量self.offset,就又回到了 游戏窗口的中心点
而我们绘制玩家的时候,实际上是把它绘制到offset_rect上,所以,无论玩家的真实坐标在哪,我们始终把它绘制到游戏窗口的正中间
可以看到,游戏一开始,主角在屏幕中间,player_rect_center = (640,360)
此时的offset_rect = (554,298),因为player_rect_center是取的主角的图像的中心点的坐标,而offset_rect是取得精灵图像的左上角的坐标,所以实际上这两个绘制出来是在同一个地方的
然后主角进行移动,player_rect_center不断在变换,但是offset_rect始终是(554,298),所以游戏的主角一直被绘制在窗口的正中间
游戏一开始,玩家的实际位置在窗口的中间,如图所示,绿色为游戏窗口,黄色为游戏的背景图片,红色的点为玩家
然后玩家移动到如图所示的红点位置,self.offset就是图中黑色箭头的向量
然后玩家绘制的位置就是玩家图片的位置减去这个黑色的向量,就是图中紫色的点
同样的,游戏背景绘制的位置也是游戏原本的位置减去这个黑色的向量,也就是图中紫色方框的位置
三.完整代码
import pygame
from settings import *
class Generic(pygame.sprite.Sprite):
def __init__(self,pos,surf,groups,z = LAYERS['main']):
super().__init__(groups)
self.image = surf
self.rect = self.image.get_rect(topleft = pos)
self.z = z
其中player.py只是给Player类中增加了一个名为self.z的变量
import pygame
from settings import *
from support import *
from timer import Timer
class Player(pygame.sprite.Sprite):
def __init__(self,pos,group):
#这个参数可以传一个精灵组,这样就会自动把该精灵加入到该精灵组中
#也可以为空,这样需要在外面手动调用精灵组的add函数来将这个精灵加入到精灵组中
super().__init__(group)
self.import_assets()
self.status = 'down_idle'
self.frame_index = 0
#这里的变量名一定要叫image,因为这是它父类Sprite规定的
self.image = self.animations[self.status][self.frame_index]
#这个get_rect()也是父类中设置的方法
#返回值是有很多,大概有x,y,centerx,centery,center,width,height这几类
#大概就是image的x坐标,y坐标,中心的x坐标,中心的y坐标,中心点的坐标,宽度,高度等
#参数可以不填,那么位置就默认是(0,0),也可以填一个列表,比如(100,100),那么初始的位置就是(100,100)
#也可以是center = 一个坐标,这表示设置该图像的中心在这个坐标上
#同样的这里的变量名也一定要叫rect,这是父类规定的
self.rect = self.image.get_rect(center = pos)
#z轴
self.z = LAYERS['main']
#创建一个二维的向量,参数不填默认是(0,0)
self.direction = pygame.math.Vector2()#速度的方向
self.pos = pygame.math.Vector2(self.rect.center)#位置
self.speed = 200#速度
self.timers = {
'tool use':Timer(350,self.use_tool),
'tool switch': Timer(200),
'seed use': Timer(350, self.use_seed),
'seed switch': Timer(200),
}
self.tools = ['hoe', 'axe', 'water']
self.tool_index = 0
self.selected_tool = self.tools[self.tool_index]
self.seeds = ['corn', 'tomato']
self.seed_index = 0
self.selected_seed = self.seeds[self.seed_index]
def use_tool(self):
pass
def use_seed(self):
pass
def import_assets(self):
self.animations = {'up': [], 'down': [], 'left': [], 'right': [],
'right_idle': [], 'left_idle': [], 'up_idle': [], 'down_idle': [],
'right_hoe': [], 'left_hoe': [], 'up_hoe': [], 'down_hoe': [],
'right_axe': [], 'left_axe': [], 'up_axe': [], 'down_axe': [],
'right_water': [], 'left_water': [], 'up_water': [], 'down_water': []}
for animation in self.animations.keys():
full_path = '../graphics/character/' + animation
self.animations[animation] = import_folder(full_path)
def animate(self, dt):
# 4 是比较合适的数字
# 数字 决定做动作的快慢
# 做一个动作需要 1/4秒
# fream_index += dt 的话,要经过1s,才能变成下一个整数,做下一个动作
# fream_index += 4*dt,那么增加的速度就快了四倍,经过1/4秒就能做下一个动作了
self.frame_index += 4 * dt
# print(self.frame_index)
if self.frame_index >= len(self.animations[self.status]):
self.frame_index = 0
self.image = self.animations[self.status][int(self.frame_index)]
def input(self):
keys = pygame.key.get_pressed()
#已经在使用工具的时候,停止对按键的检测
if not self.timers['tool use'].active:
if keys[pygame.K_UP]:
self.direction.y = -1
self.status = 'up'
elif keys[pygame.K_DOWN]:
self.direction.y = 1
self.status = 'down'
else:
self.direction.y = 0#不要忘记加上这一句,不然按下键盘后再松开也不会停
if keys[pygame.K_RIGHT]:
self.direction.x = 1
self.status = 'right'
elif keys[pygame.K_LEFT]:
self.direction.x = -1
self.status = 'left'
else:
self.direction.x = 0
if keys[pygame.K_SPACE]:
#启动计时器
self.timers['tool use'].activate()
#实用工具的时候是不能移动的
self.direction = pygame.math.Vector2()
#要从第一张图片开始绘制
self.frame_index = 0
#按下左边的shift键更换工具
if keys[pygame.K_LSHIFT] and not self.timers['tool switch'].active:
self.timers['tool switch'].activate()
self.tool_index += 1
self.tool_index = self.tool_index if self.tool_index < len(self.tools) else 0
self.selected_tool = self.tools[self.tool_index]
#按下左边的ctrl键,使用种子
if keys[pygame.K_RCTRL]:
self.timers['seed use'].activate()
self.direction = pygame.math.Vector2()
self.frame_index = 0
#按下e键,切换种子
if keys[pygame.K_RSHIFT] and not self.timers['seed switch'].active:
self.timers['seed switch'].activate()
self.seed_index += 1
self.seed_index = self.seed_index if self.seed_index < len(self.seeds) else 0
self.selected_seed = self.seeds[self.seed_index]
def get_status(self):
# idle
if self.direction.magnitude() == 0:
# self.status += '_idle'
# 上面这种方法不可取因为每一帧都会在字符串后面加上一个_idel
# 所以status会变成 xxx_idle_idle_idle
# 实际上当出现两个_idle的时候就已经报错了
# 下面这种方法
# split('_')是把一个字符串 以 '_' 为分节符分开
# 他返回的是一个列表
# 比如 a_b_c_d
# 返回的就是[a,b,c,d]
# 所以下面的【0】获取的就是_之前的内容
self.status = self.status.split('_')[0] + '_idle'
if self.timers['tool use'].active:
self.status = self.status.split('_')[0] + '_' + self.selected_tool
def update_timers(self):
for timer in self.timers.values():
timer.update()
def move(self,dt):
#向量归一化,比如一个n维向量为(x1,x2,x3...,xn)
#那么向量归一化的操作就是等比例缩放x1到xn的大小让 根号下(x1的平方+x2的平方+...+xn的平方) 等于1
#归一化的目的是如果同时按右键和上,那么direction就会变成(1,1),他的速度向量就是一个大小为根2,方向右上的向量了
#magnitude()返回的是一个float类型的数据,他的大小为根号下(x1的平方+x2的平方+...+xn的平方)
if self.direction.magnitude() > 0:#这表示如果有按键按下,如果向量里面全是0,会使归一化中的数学计算出现错误
self.direction = self.direction.normalize()
#位移 = 方向 * 速度 * 变化的时间()
self.pos.x += self.direction.x * self.speed * dt
self.rect.centerx = self.pos.x
self.pos.y += self.direction.y * self.speed * dt
self.rect.centery = self.pos.y
def update(self,dt):
self.input()
self.get_status()
self.update_timers()
self.move(dt)
self.animate(dt)
import pygame
from settings import *
from player import Player
from overlay import Overlay
from sprites import *
class Level():
def __init__(self):
#得到屏幕的画面,得到的这个画面与main.py中的screen相同
self.display_surface = pygame.display.get_surface()
#创建精灵组
self.all_sprites = CameraGroup()
#调用setup方法
self.setup()
#创建工具和种子显示图层
self.overlay = Overlay(self.player)
def setup(self):
self.player = Player((640,360),self.all_sprites)
Generic(
pos = (0,0),
surf = pygame.image.load('../graphics/world/ground.png').convert_alpha(),
groups = self.all_sprites,
z = LAYERS['ground']
)
def run(self,dt):
#窗口的背景设为黑色
self.display_surface.fill('black')
#调用精灵组的draw方法
self.all_sprites.custom_draw(self.player)
#调用精灵组的update方法
self.all_sprites.update(dt)
self.overlay.display()
class CameraGroup(pygame.sprite.Group):
def __init__(self):
super().__init__()
#获取窗口
self.display_surface = pygame.display.get_surface()
#这是一个偏移量,代表的是玩家的实际位置与屏幕中间的矢量
self.offset = pygame.math.Vector2()
def custom_draw(self,player):
self.offset.x = player.rect.centerx - SCREEN_WIDTH / 2
self.offset.y = player.rect.centery - SCREEN_HEIGHT / 2
for layer in LAYERS.values():#按照z轴从小到达绘制
for sprite in self.sprites():
if sprite.z == layer:#如果该精灵的z值等于当前要绘制的z值,才绘制
offset_rect = sprite.rect.copy()
offset_rect.center -= self.offset
#if sprite == player:
#print("player.rect.center为:(" +
# str(player.rect.centerx)+"," + str(player.rect.centery)+")")
#print("offset_rect为:(" + str(offset_rect.x)
# +"," +str(offset_rect.y)+")")
self.display_surface.blit(sprite.image,offset_rect)