likes
comments
collection
share

龙年特辑-《小鲤鱼成龙记》

作者站长头像
站长
· 阅读数 4

一、前言

在这个龙年特辑的游戏中,小鲤鱼将探索未知的海洋和天空,迎接挑战,跃龙门,成就传奇!

在新的龙年中祝愿大家心想事成,万事如意!愿新的一年里,我们能够披荆斩棘,勇往直前!

龙年特辑-《小鲤鱼成龙记》

龙年特辑-《小鲤鱼成龙记》

快速上手体验

# 下载克隆项目  
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)30105
小鱼仔海洋生物12
虾米海洋生物12
海龟海洋生物1-32-6
水母海洋生物1-32-6
..
鲨鱼海洋生物
虎鲸海洋生物(Boss)
落雨障碍物2
落石障碍物5
小旋涡障碍物10
龙鳞宝物105
龙币宝物1510
龙角宝物2015
龙珠宝物2520
龙吉祥物宝物105

分数、幸运值、血量计算

海洋生物

  • 分数:等级 * 5

  • 血量:20 * 等级

  • 攻击:5 * 等级

  • 防御:3 * 等级

宝物

  • 分数:10 + 5 * (等级 - 1)

  • 幸运值:5 * 等级

游戏特效

  • 鱼游动、碰撞

三、准备素材

老样子先去 iconfont 看看有没有可以用的素材,然后就是网上搜集,由于不是专门设计一套的素材,可能有些素材会很突兀。开发一个小游戏,游戏素材需要很多,不然动画效果很生硬,还望大家见谅。

游戏关卡背景

  • 海洋

  • 奖励关

游戏素材

  • 龙、小鲤鱼(玩家)

  • 鱼、障碍物、宝物

  • 背景音乐、碰撞特效(todo)

四、部分代码展示

主要使用 pygame 来进行游戏的渲染,pygame.sprite.Sprite 类来组织各游戏角色属性,以及碰撞检测。

整体的游戏框架思想如下图。

龙年特辑-《小鲤鱼成龙记》

主循环,事件处理

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()

上下左右移动小鱼

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) 坐标从而移动小鱼,并记录移动方向用于切换对应的鱼的朝向图,为了让鱼移动更好看一些就多了一些朝向(右上、右下)。

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)的时候,获取当前方位的小鱼图,一图多用(^_^),找素材太难了。

跟随鼠标点击位置移动

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的差值,判断是否继续移动
  • 最后更新角色位置

模拟游动特效

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

由于游戏的特效需要一套连续的图进行渲染,我没找到太多图就简单通过 缩放、反转 原图来进行特效模拟。看起来的效果相对不会太生硬。

碰撞检测

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 模式