一.目标
二.代码实现
我们接下来的很多操作都是依赖于土壤完成的,不仅仅是开垦土壤,还有种植,浇水,收获等。因此我们需要新建一个文件来完成这些功能,新建的文件叫做soil.py
接下来,我们要完成一个管理所有土地的类,我们对土地的操作都要经过这个类来实现
首先我们在以后需要all_sprites这个精灵组,所以先把它当作参数传递进来
其次开垦过的土地长得和正常的土地不一样,我们还需要把开垦过的土地的图片给导入进来
从.tmx文件中可以看出,我们的地图分为很多个64*64像素的小块,我们可以把每一个小块当成一块单独的土地
那么每一块土地是否能开垦,是否已经被开垦过了,是否种上了粮食,是否交过水了,这些状态都是需要保存的,因此我们想创建一个三维的列表,其中的前二维代表地图的行和列,也就是土地块的坐标,第三维就是存放这块土地的一系列性质
为了获取这样的一个列表,我们书写了如下的函数,当然,我们又一次需要导入tmx文件,用到了pytmx这个工具包,记得在文件开头把它import进来
作者给提供的tmx文件里有一个叫Farmable的图层,他是由如下的绿色方块组成的,代表的是可以耕种的土地的范围
这样,我们就可以在self.grid这个变量中,查看那些土地块是可以被我们耕种的了,如果该土地块的列表中有'F'这个属性,代表它是可以被耕种的
现在,我们只是知道了第几行第几列的土地块是能被耕种的,这个坐标是相对于tmx文件来说的,所以我们还得获取相对于pygame的坐标系能被耕种的区域,于是就有了如下的函数
该函数会把所有能被耕种的土地块,以(x坐标,y坐标,宽度,高度)的形式,存到一个叫hit_rects的列表里
思路还是,当玩家使用锄头的时候,会遍历我们刚刚获得的列表hit_rects,也就是遍历所有可耕种的土地块,如果有土地块和玩家的锄头的坐标发生了重叠,说明玩家正在耕种这块土地,就接着进行别的操作
在此之前,我们得先把SoilLayer这个类实例化出来一个对象,并且还能再player里面访问这个对象的hit_rects
再实例化对象,按理说是写到setup函数里面的,但是原作者写到了init函数里面,原作者写的逻辑真的很乱,毕竟是一镜到底的视频, 很多东西都没能考虑周全
其实只要在player对象实例化之前把SoilLayer实例化出来就行了,写哪里无所谓
接下来,如果玩家使用锄头,就可以调用soil_layer的get_hit函数了
遍历列表hit_rects中所有能被开垦的土地,如果有土地与传入的坐标重合了,说明正在开垦这个土地,这时候就给grid中的这个土地块的列表里新增一个标记'X',来说明这个土地是被开垦过的。接下来,我们还需要把被开垦过的土地的贴图更换一下,这一点功能,我们写道create_soil_tiles这个函数里面
首先要明确我们的思路,当开垦过土地之后,我们要新建一个精灵,把精灵加入我们初始化的时候创建的soil_sprites精灵组里面,此外因为需要把他绘制出来,还要放到all_sprites精灵组里,把精灵的贴图设为我们初始化的时候导入进来的开垦过的土地的贴图。然后把这个精灵绘制出来即可达到视觉效果了
每次调用create_soil_tiles函数的时候,我们先把soil_sprites精灵组清空,也就是先把之前创建的所有的开垦过的土地的精灵给消除掉,然后再查询grid,把里面所有带标记'X'的土地块的位置都在创建一个开垦过的土地的精灵
实际上,这是一种极其低效的算法,每次新开垦一块土地,原作者并不知道新开垦的土地在哪,干脆把之前创建的精灵都删了,然后再把所有开垦过的地方都再创建一遍。
每次开垦都会重复大量的精灵删除与创建,这会导致当我们开垦过多的土地后,游戏变得十分卡顿。
一种解决方法是,既然我们都已经在get_hit中获得了被新开垦的土地块的坐标了,为何不干脆直接在那里把SoilTile创建出来,还非得在哪里调用一个函数,在这个函数里把所有的土地块都遍历一遍呢。
原理就是根据土地左边右边上边下边是否也有土地,分成了好几张图片
然后每次绘制的时候,对每一块土地的上下左右分别判断有没有土地,再选择对应的图片来进行绘制
每次新增一块土地,调用这个函数,都会遍历所有的土地一遍,还要再加上很多很多的判断
于是尝试有没有别的算法能够实现这种效果,发现效果都不太好,于是决定放弃这种效果了
这样,create_soil_tiles这个函数就可以删掉或者注释掉了
到这里,我们还没写SoilTile这个类呢,就是一个简单的作为展示作用的精灵而已,如下图所示
这样,进入游戏,我们就可以开垦土地了,而且开垦了这么多土地一点也不卡
我在运行原作者的原程序时,开垦了超过20块土地后,就卡的不行了,人物基本都是瞬移的,基本上可玩性为0了,各位不妨按原代码跑一下试试看到底有多卡
三.完整代码
import pygame
from settings import *
from pytmx.util_pygame import load_pygame
class SoilTile(pygame.sprite.Sprite):
def __init__(self, pos, surf, groups):
super().__init__(groups)
self.image = surf
self.rect = self.image.get_rect(topleft = pos)
self.z = LAYERS['soil']
#管理所有土地的类
class SoilLayer:
def __init__(self,all_sprites):
#all_sprites精灵组
self.all_sprites = all_sprites
#创建开垦过的土地精灵组
self.soil_sprites = pygame.sprite.Group()
#导入开垦过的土地的图片
self.soil_surf = pygame.image.load('../graphics/soil/o.png')
self.create_soil_grid()
self.create_hit_rects()
def create_soil_grid(self):
#导入地图的图片
ground = pygame.image.load('../graphics/world/ground.png')
#地图的宽度,高度除以64,就是一共有多少块(64*64)的土地
h_tiles, v_tiles = ground.get_width() // TILE_SIZE, ground.get_height() // TILE_SIZE
#创建一个三维列表,最一开始,所有土地的属性都是空的
self.grid = [[[] for col in range(h_tiles)] for row in range(v_tiles)]
#如果是可以被耕种的土地块,就给他的列表添加一个标记'F'
for x, y, _ in load_pygame('../data/map.tmx').get_layer_by_name('Farmable').tiles():
self.grid[y][x].append('F')
def create_hit_rects(self):
#列表里存放的是所有能被耕种的土地,数据类型是pygame里提供的Rect数据类型,包含了坐标和宽高
self.hit_rects = []
for index_row, row in enumerate(self.grid):
for index_col, cell in enumerate(row):
#如果是能被耕种的
if 'F' in cell:
x = index_col * TILE_SIZE
y = index_row * TILE_SIZE
rect = pygame.Rect(x, y, TILE_SIZE, TILE_SIZE)
self.hit_rects.append(rect)
def get_hit(self, point):
#point是传入的参数,代表的是玩家挥舞的锄头的落点的坐标
for rect in self.hit_rects:
#如果该可耕种的土地块与玩家的锄头坐标发生了重叠,说明玩家锄的就是这块地,就给他的属性再加上一个标记'X'
if rect.collidepoint(point):
#相反,这里得把pygame的坐标系转换成tmx文件中的坐标系
x = rect.x // TILE_SIZE
y = rect.y // TILE_SIZE
if 'F' in self.grid[y][x]:
self.grid[y][x].append('X')
SoilTile(
pos=(rect.x,rect.y),
surf=self.soil_surf,
groups=[self.all_sprites, self.soil_sprites])
#self.create_soil_tiles()
"""
def create_soil_tiles(self):
self.soil_sprites.empty()
for index_row, row in enumerate(self.grid):
for index_col, cell in enumerate(row):
if 'X' in cell:
SoilTile(
pos=(index_col * TILE_SIZE, index_row * TILE_SIZE),
surf=self.soil_surf,
groups=[self.all_sprites, self.soil_sprites])"""
import pygame
from settings import *
from player import Player
from overlay import Overlay
from sprites import *
from pytmx.util_pygame import load_pygame
from support import *
from transition import Transition
from soil import SoilLayer
class Level():
def __init__(self):
#得到屏幕的画面,得到的这个画面与main.py中的screen相同
self.display_surface = pygame.display.get_surface()
#创建精灵组
self.all_sprites = CameraGroup()
#具有碰撞箱的精灵组
self.collision_sprites = pygame.sprite.Group()
#树木精灵组
self.tree_sprites = pygame.sprite.Group()
#特殊区域精灵组
self.interaction_sprites = pygame.sprite.Group()
#调用setup方法
self.setup()
#创建工具和种子显示图层
self.overlay = Overlay(self.player)
#创建transition对象
self.transition = Transition(self.reset,self.player)
def setup(self):
#土地管理类
self.soil_layer = SoilLayer(self.all_sprites)
#载入.tmx文件
tmx_data = load_pygame('../data/map.tmx')
#绘制房子与栅栏,他们都属于Generic类
for layer in ['HouseFloor', 'HouseFurnitureBottom']:
for x, y, surf in tmx_data.get_layer_by_name(layer).tiles():
Generic((x * TILE_SIZE, y * TILE_SIZE), surf, self.all_sprites, LAYERS['house bottom'])
for layer in ['HouseWalls', 'HouseFurnitureTop','Fence']:
for x, y, surf in tmx_data.get_layer_by_name(layer).tiles():
Generic((x * TILE_SIZE, y * TILE_SIZE), surf, [self.all_sprites, self.collision_sprites])
#水流
water_frames = import_folder('../graphics/water')
for x, y, surf in tmx_data.get_layer_by_name('Water').tiles():
Water((x * TILE_SIZE, y * TILE_SIZE), water_frames, self.all_sprites)
#树木
for obj in tmx_data.get_layer_by_name('Trees'):
Tree(
pos=(obj.x, obj.y),
surf=obj.image,
groups=[self.all_sprites, self.collision_sprites, self.tree_sprites],
name=obj.name,
player_add=self.player_add)
#野花
for obj in tmx_data.get_layer_by_name('Decoration'):
WildFlower((obj.x, obj.y), obj.image,[self.all_sprites, self.collision_sprites])
#空气墙
for x, y, surf in tmx_data.get_layer_by_name('Collision').tiles():
Generic((x * TILE_SIZE, y * TILE_SIZE), pygame.Surface((TILE_SIZE, TILE_SIZE)), self.collision_sprites)
#玩家
for obj in tmx_data.get_layer_by_name('Player'):
if obj.name == 'Start':
self.player = Player(
pos=(obj.x, obj.y),
group=self.all_sprites,
collision_sprites=self.collision_sprites,
tree_sprites=self.tree_sprites,
interaction = self.interaction_sprites,
soil_layer = self.soil_layer)
if obj.name == 'Bed':
Interaction((obj.x, obj.y), (obj.width, obj.height), self.interaction_sprites, obj.name)
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()
#如果在睡觉,执行相应的函数
if self.player.sleep:
self.transition.play()
def player_add(self, item):
#item是一个str类型的数据,代表要对哪一种物品加一
self.player.item_inventory[item] += 1
def reset(self):
#苹果重新长在树上
for tree in self.tree_sprites.sprites():
for apple in tree.apple_sprites.sprites():
apple.kill()
tree.create_fruit()
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 sorted(self.sprites(),key = lambda sprite: sprite.rect.centery):
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)
import pygame
from settings import *
from support import *
from timer import Timer
class Player(pygame.sprite.Sprite):
def __init__(self,pos,group,collision_sprites,tree_sprites,interaction,soil_layer):
#这个参数可以传一个精灵组,这样就会自动把该精灵加入到该精灵组中
#也可以为空,这样需要在外面手动调用精灵组的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.hitbox = self.rect.copy().inflate((-126,-70))
self.collision_sprites = collision_sprites
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]
#玩家的背包
self.item_inventory = {
'wood': 0,
'apple': 0,
'corn': 0,
'tomato': 0
}
#接收树木精灵组
self.tree_sprites = tree_sprites
#接收特殊区域精灵组
self.interaction = interaction
#是否在睡觉
self.sleep = False
#泥土管理
self.soil_layer = soil_layer
def use_tool(self):
if self.selected_tool == 'hoe':
self.soil_layer.get_hit(self.target_pos)
if self.selected_tool == 'axe':
for tree in self.tree_sprites.sprites():
if tree.rect.collidepoint(self.target_pos):
tree.damage()
if self.selected_tool == 'water':
pass
def get_target_pos(self):
# 获取工具的作用区域坐标
self.target_pos = self.rect.center + PLAYER_TOOL_OFFSET[self.status.split('_')[0]]
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 and not self.sleep:
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
#按下右边的shift键,切换种子
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]
#按下回车键,进行睡觉
if keys[pygame.K_RETURN]:
#返回值是与第一个参数的精灵发生碰撞的精灵
collided_interaction_sprite = pygame.sprite.spritecollide(self, self.interaction, False)
#参数False表示,如果发生了重叠,不会kill掉第二个参数的精灵,如果是True,就Kill掉第二个参数的精灵
if collided_interaction_sprite:
#交易的部分以后再说
if collided_interaction_sprite[0].name == 'Trader':
pass
else:
#为了视觉效果设置成面朝床边
self.status = 'left_idle'
self.sleep = True
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 collision(self,direction):
# 遍历碰撞箱精灵组的所有精灵
for sprite in self.collision_sprites.sprites():
# 如果该精灵有一个叫hitbox的属性
#实际上collision_sprites精灵组内的所有精灵都应该有hitbox这个属性,这句话属实多余
if hasattr(sprite,'hitbox'):
#如果该精灵的hitbox与玩家的hitbox有重叠
if sprite.hitbox.colliderect(self.hitbox):
#如果此时正在水平方向移动
if direction == 'horizontal':
if self.direction.x > 0: #玩家正在向右移动
self.hitbox.right = sprite.hitbox.left
if self.direction.x < 0: # 玩家正在向左移动
self.hitbox.left = sprite.hitbox.right
self.rect.centerx = self.hitbox.centerx
self.pos.x = self.hitbox.centerx
#如果此时正在竖直方向移动
if direction == 'vertical':
if self.direction.y > 0: # 玩家正在向下移动
self.hitbox.bottom = sprite.hitbox.top
if self.direction.y < 0: # 玩家正在向上移动
self.hitbox.top = sprite.hitbox.bottom
self.rect.centery = self.hitbox.centery
self.pos.y = self.hitbox.centery
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.hitbox.centerx = round(self.pos.x)
self.rect.centerx = self.hitbox.centerx
self.collision('horizontal')
#竖直方向
self.pos.y += self.direction.y * self.speed * dt
self.hitbox.centery = round(self.pos.y)
self.rect.centery = self.hitbox.centery
self.collision('vertical')
def update(self,dt):
self.input()
self.get_status()
self.update_timers()
self.get_target_pos()
self.move(dt)
self.animate(dt)