一、前言
在这个龙年特辑的游戏中,小鲤鱼将探索未知的海洋和天空,迎接挑战,跃龙门,成就传奇!
在新的龙年中祝愿大家心想事成,万事如意!愿新的一年里,我们能够披荆斩棘,勇往直前!
快速上手体验
bash
# 下载克隆项目
git clone https://github.com/HuiDBK/DragonFeast.git
# 进入项目、安装依赖
pip install -r requirements.txt
# 游戏运行
python main.py
二、简单设计
大鱼吃小鱼游戏模式,使用与龙相关的元素进行设计。主角小鲤鱼(龙)在海洋、天空成长。关卡随机生成海洋生物、宝物(龙鳞、龙珠、龙角、龙吉祥物、金币)、障碍物(落石、旋涡、落雨)、小鲤鱼躲避障碍物、吃小鱼成长。当鲤鱼达到一定分数进入奖励关卡(快速成长)、幸运关卡(小鲤鱼跃龙门)。
小鲤鱼上下左右、(w、a、s、d)控制方向、跟随鼠标点击位置游进,其他随机生成。
游戏结束:空格重玩、esc退出
鱼、宝物、障碍物什么时候随机生成?
-
鱼每隔10秒、少于3只时左右两边随机生成10只
-
宝物每隔26秒、分数整除66,上方掉落宝物
-
每隔15秒、随机障碍物
随机生成的宝物、障碍物什么时候消失?
-
宝物停留 8*fps 的帧数,被吃掉
-
障碍物
-
雨滴、旋涡 停留 2*fps 的帧数,或者碰撞到玩家
-
落石则滚出游戏屏幕
-
碰撞检测处理
小鲤鱼与障碍物碰撞,小鲤鱼扣除(障碍物攻击值-自身的防御值)的血量
小鲤鱼与海洋生物碰撞
-
大等级吃小等级
-
同级的海洋生物,无法一口吃掉,小鲤鱼自身扣除(海洋生物的攻击值-自身的防御值)的血量,反之加成长值、分数(只能越一级攻击)
-
提升奖励值
小鲤鱼与宝物碰撞
- 提升分数与幸运值
关卡切换
成长关卡
-
随机生成 [游戏等级-1,游戏等级 + 2] 等级的海洋生物
-
每满120分关卡升级随机换背景
奖励关卡
- 分数 每满30进入普通奖励
- 幸运值 满100 进入幸运奖励关卡
成长变化
-
等级提升、血量恢复满状态、每提升一级
-
角色变大 10%
-
攻击值提升 20 %
-
防御值提升 10%
-
速度提升 20%
-
血量值:等级 * 初始血量后提升10%
-
游戏角色初始属性设定
角色 | 种类 | 等级 | 血量 | 攻击 | 防御 | 分数 | 幸运值 |
---|---|---|---|---|---|---|---|
小鲤鱼(龙) | 主角 | (1-10) | 30 | 10 | 5 | ||
小鱼仔 | 海洋生物 | 1 | 2 | ||||
虾米 | 海洋生物 | 1 | 2 | ||||
海龟 | 海洋生物 | 1-3 | 2-6 | ||||
水母 | 海洋生物 | 1-3 | 2-6 | ||||
.. | |||||||
鲨鱼 | 海洋生物 | ||||||
虎鲸 | 海洋生物(Boss) | ||||||
落雨 | 障碍物 | 2 | |||||
落石 | 障碍物 | 5 | |||||
小旋涡 | 障碍物 | 10 | |||||
龙鳞 | 宝物 | 10 | 5 | ||||
龙币 | 宝物 | 15 | 10 | ||||
龙角 | 宝物 | 20 | 15 | ||||
龙珠 | 宝物 | 25 | 20 | ||||
龙吉祥物 | 宝物 | 10 | 5 |
分数、幸运值、血量计算
海洋生物
-
分数:等级 * 5
-
血量:20 * 等级
-
攻击:5 * 等级
-
防御:3 * 等级
宝物
-
分数:10 + 5 * (等级 - 1)
-
幸运值:5 * 等级
游戏特效
- 鱼游动、碰撞
三、准备素材
老样子先去 iconfont 看看有没有可以用的素材,然后就是网上搜集,由于不是专门设计一套的素材,可能有些素材会很突兀。开发一个小游戏,游戏素材需要很多,不然动画效果很生硬,还望大家见谅。
游戏关卡背景
-
海洋
-
奖励关
游戏素材
-
龙、小鲤鱼(玩家)
-
鱼、障碍物、宝物
-
背景音乐、碰撞特效(todo)
四、部分代码展示
主要使用 pygame
来进行游戏的渲染,pygame.sprite.Sprite
类来组织各游戏角色属性,以及碰撞检测。
整体的游戏框架思想如下图。
主循环,事件处理
python
def _event_handle(self):
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
if event.type == pygame.MOUSEBUTTONDOWN:
mouse_pos = pygame.mouse.get_pos()
if not self.is_running:
# 选择游戏玩家
self.select_player(mouse_pos)
else:
# 鼠标点击 记录坐标
self.player_target = mouse_pos
if self.is_game_over:
# 游戏结束, 空格重玩,esc 退出
if event.type == pygame.K_SPACE:
self.game_replay()
elif event.type == pygame.K_ESCAPE:
pygame.quit()
sys.exit()
def run_game(self):
clock = pygame.time.Clock()
while True:
# 设置游戏刷新帧率
clock.tick(self.game_fps)
# 游戏事件处理
self._event_handle()
# 绘制背景
self.game_screen.blit(source=self.bg_img, dest=(0, 0))
if not self.is_running:
self.render_start_screen()
pygame.display.flip()
continue
if self.is_game_over:
# 游戏结束不做游戏渲染
continue
# 游戏渲染
self.render_game()
# 碰撞检测处理
self.collision_check()
# 游戏模式切换检测
self.game_scene_switch_check()
# 游戏结束检测
self.game_over_check()
# 刷新游戏窗口
pygame.display.flip()
上下左右移动小鱼
python
def move_dragon(self, keys, dragon_game_obj):
"""移动小龙"""
if self.check_beyond_screen(dragon_game_obj.game_width, dragon_game_obj.game_height):
# 超出边界
return
move_keys = [
pygame.K_UP, pygame.K_DOWN, pygame.K_LEFT, pygame.K_RIGHT,
pygame.K_w, pygame.K_s, pygame.K_a, pygame.K_d
]
for move_key in move_keys:
if keys[move_key]:
# 移动了则去除鼠标点击的位置,避免移动冲突
dragon_game_obj.player_target = None
if keys[pygame.K_LEFT] or keys[pygame.K_a]:
self.move_direct = MoveDirection.LEFT
self.rect.x -= self.speed
if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
self.move_direct = MoveDirection.RIGHT
self.rect.x += self.speed
if keys[pygame.K_UP] or keys[pygame.K_w]:
if self.move_direct in [MoveDirection.RIGHT, MoveDirection.RIGHT_UP, MoveDirection.RIGHT_DOWN]:
# 右上
self.move_direct = MoveDirection.RIGHT_UP
else:
self.move_direct = MoveDirection.UP
self.rect.y -= self.speed
if keys[pygame.K_DOWN] or keys[pygame.K_s]:
if self.move_direct in [MoveDirection.RIGHT, MoveDirection.RIGHT_DOWN, MoveDirection.RIGHT_UP]:
# 右下
self.move_direct = MoveDirection.RIGHT_DOWN
else:
self.move_direct = MoveDirection.DOWN
self.rect.y += self.speed
就是根据鼠标事件来判断 上下左右来改变小鱼的 (x, y) 坐标从而移动小鱼,并记录移动方向用于切换对应的鱼的朝向图,为了让鱼移动更好看一些就多了一些朝向(右上、右下)。
python
class DragonSprite(BaseGameSprite):
"""小鲤鱼(龙)"""
init_hp = 30
init_speed = 5
distance_threshold = 1e-6
# 设置一个足够小的值,表示小龙已经非常接近目标点
close_enough_threshold = 2
def __init__(self, image_path):
super().__init__(image_path)
self.level = 1
self.attack_value = 10
self.defense_value = 5
self.speed = self.level * self.init_speed
self.move_direct = MoveDirection.LEFT
# 初始化各个方位的图像
self.images = {
MoveDirection.LEFT: self.image,
MoveDirection.RIGHT: pygame.transform.flip(self.image, True, False),
MoveDirection.UP: pygame.transform.rotozoom(self.image, -30, 1),
MoveDirection.DOWN: pygame.transform.rotozoom(self.image, 30, 1),
MoveDirection.RIGHT_UP: pygame.transform.flip(pygame.transform.rotozoom(self.image, -30, 1), True, False),
MoveDirection.RIGHT_DOWN: pygame.transform.flip(pygame.transform.rotozoom(self.image, 30, 1), True, False),
}
def update(self, *args, keys=None, dragon_game_obj=None, **kwargs):
self.move_dragon(keys, dragon_game_obj)
# 根据移动方向更新图像
self.image = self.images[self.move_direct]
DragonSprite
一开始初始化的时候,就会根据原图进行反转、旋转来生成对应方向的小鱼各个方位的素材,然后在渲染小鱼(update)的时候,获取当前方位的小鱼图,一图多用(^_^),找素材太难了。
跟随鼠标点击位置移动
python
def move_to(self, target_pos):
"""移动到目标位置"""
# 计算两点之间的直线路径
x1, y1 = self.rect.x, self.rect.y
x2, y2 = target_pos
distance = ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5
# print(distance)
if distance < self.distance_threshold:
# 距离小于阈值直接返回
return
# 如果小龙非常接近目标点,直接返回
# print("rect.x", self.rect.x, "rect.y", self.rect.y)
# print("target.x", x2, "target.y", y2)
if (abs(self.rect.x - x2) < self.close_enough_threshold or
abs(self.rect.y - y2) < self.close_enough_threshold):
return
# 计算每个步骤的移动量
step_x = (x2 - x1) / distance * self.speed
step_y = (y2 - y1) / distance * self.speed
# 计算方位
direct = self.calc_direct(x1, y1, x2, y2)
self.move_direct = direct
# 移动小龙
self.rect.x += step_x
self.rect.y += step_y
- 计算两点之间的直线路径
- 计算每个步骤的移动量
- 根据鼠标位置计算方位
- 根据阈值以及与目的地x、y的差值,判断是否继续移动
- 最后更新角色位置
模拟游动特效
python
class BaseGameSprite(Sprite):
init_hp = 0
def __init__(self, image_path):
super().__init__()
self.image = pygame.image.load(image_path)
self.rect = self.image.get_rect()
self.level = 1
self.hp = 0
self.attack_value = 0
self.defense_value = 0
self.speed = 0
self.score = 0
self.lucky_value = 0
self.hp = self.init_hp * self.level
self.original_image = self.image # 记录原始图
# 游动效果的帧计数
self.frame_count = 0
def random_pos(self, game_width, game_height):
"""随机位置"""
self.rect.x = random.randint(0, game_width)
self.rect.y = random.randint(0, game_height)
def move_animate(self, use_original_image=True, rotate_angle=1, reverse_image=False):
"""
模拟游动特效
Args:
use_original_image: 是否使用原图,由于DragonSprite每帧都会根据朝向换图,原图又一直是向左的故不能使用原图
rotate_angle: 旋转角度
reverse_image: 反转图片
"""
image = self.image
if use_original_image:
image = self.original_image
# 添加游动的效果
if self.frame_count % 10 == 0:
# 每隔一定帧数切换图像
scale_factor = 1
self.image = pygame.transform.rotozoom(image, rotate_angle, scale_factor) # 缩放、旋转1度模拟游动
if reverse_image:
self.image = pygame.transform.flip(image, True, False) # 反转模拟游动
else:
# 还原
self.image = image
self.frame_count += 1
由于游戏的特效需要一套连续的图进行渲染,我没找到太多图就简单通过 缩放、反转 原图来进行特效模拟。看起来的效果相对不会太生硬。
碰撞检测
python
def eat_fish(self, fish_sprite: FishSprite):
"""吃鱼处理"""
print("fish", fish_sprite.level)
print("dragon", self.dragon_sprite.level)
# 判断鱼的等级
if fish_sprite.level < self.dragon_sprite.level:
# 小于小龙等级,直接吃到,并加分
# 游戏精灵组移除鱼
# self.game_sprites.remove(fish_sprite)
fish_sprite.kill()
self.dragon_sprite.score += fish_sprite.score
self.bonus_score += 1
elif fish_sprite.level >= self.dragon_sprite.level:
# 鱼等级大于等于小龙, 互相攻击, 血量相减
if fish_sprite.hp <= 0:
# 鱼没血了,移除
# self.game_sprites.remove(fish_sprite)
fish_sprite.kill()
self.dragon_sprite.score += fish_sprite.score
self.bonus_score += 1
if (fish_sprite.level - self.dragon_sprite.level) <= 1:
# 只能越一级攻击
fish_sprite.hp -= max(self.dragon_sprite.attack_value - fish_sprite.defense_value, 0)
self.dragon_sprite.hp -= max(fish_sprite.attack_value - self.dragon_sprite.defense_value, 0)
def collision_check(self):
"""碰撞检测"""
# 和小龙碰撞的生物
collided_sprites = pygame.sprite.spritecollide(self.dragon_sprite, self.game_sprites, dokill=False)
for collided_sprite in collided_sprites:
if isinstance(collided_sprite, FishSprite):
# 吃到鱼处理
self.eat_fish(fish_sprite=collided_sprite)
elif isinstance(collided_sprite, ObstacleSprite):
# 碰到障碍物处理
self.dragon_sprite.hp -= collided_sprite.attack_value
collided_sprite.kill()
elif isinstance(collided_sprite, TreasureSprite):
# 吃到宝物处理
self.dragon_sprite.score += collided_sprite.score
self.dragon_sprite.lucky_value += collided_sprite.lucky_value
collided_sprite.kill()
elif isinstance(collided_sprite, BonusSprite):
# 奖励关卡
self.dragon_sprite.score += collided_sprite.score
collided_sprite.kill()
pygame.sprite.spritecollide()
是 Pygame 中用于检测精灵间碰撞的方法之一。它用于检测一个精灵与一个精灵组中的其他精灵之间的碰撞,并返回与该精灵发生碰撞的精灵列表。
参数介绍:
- sprite: 要检测碰撞的精灵对象。通常是一个 Pygame 精灵对象。
- group: 要检测碰撞的精灵组。通常是一个 Pygame 精灵组对象。
- dokill: 一个布尔值参数,用于指定是否在检测到碰撞时从精灵组中删除发生碰撞的精灵。如果设置为 True,则在检测到碰撞时,与
sprite
碰撞的精灵将从group
中删除;如果设置为 False,则不删除。
方法返回与 sprite
发生碰撞的精灵对象的列表。
通过pygame的sprite模块的方法进行碰撞检测就简单很多,只需要判断发生碰撞的精灵的类型是什么,然后做对应的逻辑处理。
五、源代码
还有好多可以优化的地方,大家感兴趣可以拉取源码进行改造
Github:github.com/HuiDBK/Drag...
todo list
- 血量、幸运、奖励值,数值转进度条样子
- 游戏特效优化
- Boss 模式