Pygame学习笔记7:精灵冲突及Zombie Mob游戏

Pygame学习笔记7:精灵冲突及Zombie Mob游戏,第1张

Pygame学习笔记7:精灵冲突及Zombie Mob游戏

上一章我们对于冲突的检测只是使用的pygame的最简单的冲突检测,即只是两个精灵之间的冲突检测,但是pygame支持多种冲突检测技术,因此这一章就来学习一下这些冲突检测技术。

冲突检测技术 两个精灵之间的矩形检测

这个方法在上一章中已经讲过了,就是使用pygame.sprite.collide_rect()方法进行一对一的检测,需要传递两个参数,都是pygame.sprite.Sprite派生出来的对象,具体来讲就是关键要有一个rect的Rect属性可用,实例如下:
对于如下的类:

# 类中的X、Y、position用于设置精灵的位置
class MySprite(pygame.sprite.Sprite):
    def __init__(self, image_file):
        # 调用父类的初始化方法
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load(image_file).convert_alpha()
        self.rect = self.image.get_rect()
        self.master_image = None
        self.frame = 0
        self.old_frame = -1
        self.frame_width = 1
        self.frame_height = 1
        self.first_frame = 0
        self.last_frame = 0
        self.columns = 1
        self.last_time = 0

    def getx(self):
        return self.rect.x

    def setx(self, value):
        self.rect.x = value
    x = property(getx, setx)

    def gety(self):
        return self.rect.y

    def sety(self, value):
        self.rect.y = value
    y = property(gety, sety)

    def getpos(self):
        return self.rect.topleft

    def setpos(self, pos):
        self.rect.topleft = pos
    position = property(getpos, setpos)

    def load(self, filename, width, height, columns):
        self.master_image = pygame.image.load(filename).convert_alpha()
        self.frame_width = width
        self.frame_height = height
        self.rect = Rect(0, 0, width, height)
        self.columns = columns
        rect = self.master_image.get_rect()
        self.last_frame = (rect.width // width) * (rect.height // height) - 1

    def update(self, current_time, rate=30):
        # 帧变动
        if current_time > self.last_time + rate:
            self.frame += 1
        if self.frame > self.last_frame:
            self.frame = self.first_frame
        self.last_time = current_time
        # 当帧发生变化时,进行修改
        if self.frame != self.old_frame:
            frame_x = (self.frame % self.columns) * self.frame_width
            frame_y = (self.frame // self.columns) * self.frame_height
            rect = Rect(frame_x, frame_y, self.frame_width, self.frame_height)
            # 将要展示的图片送给image属性,以便展示出来
            self.image = self.master_image.subsurface(rect)
            self.old_frame = self.frame

    def __str__(self):
        return str(self.frame) + "," + str(self.first_frame) + "," + str(self.last_frame) + 
               "," + str(self.frame_width) + "," + str(self.frame_height) + "," + 
               str(self.columns) + "," + str(self.rect)

函数如下:

if __name__ == "__main__":
    pygame.init()
    screen = pygame.display.set_mode((800, 600))
    pygame.display.set_caption("Demo")
    font = pygame.font.Font(None, 18)
    group = pygame.sprite.Group()

    first = MySprite("military.png")
    first.position = 0, 100
    group.add(first)
    second = MySprite("flame.png")
    a = 700
    second.position = a, 300
    group.add(second)

    while True:
        for event in pygame.event.get():
            if event.type == QUIT:
                sys.exit()
        keys = pygame.key.get_pressed()
        if keys[K_ESCAPE]:
            sys.exit()

        screen.fill((0, 0, 200))
        group.draw(screen)

        if pygame.sprite.collide_rect(first, second):
            print_text(font, 400, 300, "oh, my God!")
        else:
            a -= 10
            second.position = a, 300

        pygame.display.update()


运行结果如下:

可以看到,当子d与飞船接触时,画面停止并输出字符串。

这个函数还有一个变体,即pygame.sprite.collide_rect_ratio(),不同之处在于,这个函数有一个额外的参数,是一个浮点数,用来指定检测的矩形的百分比。当一个精灵图像的周围有很多空白空间时,这个函数会很有用使用实例如下,只需要将if语句的判断条件改一下即可:

pygame.sprite.collide_rect_ratio(0.75)(first, second)

可以看到这个函数的使用有点奇怪,那么效果如下:

可以看到,子d已经与飞船重叠了。

两个精灵之间的圆检测

圆检测是基于每个精灵的一个半径值来进行的。可以自己指定半径或者让pygame.sprite.collide_circle()函数自动计算半径,自己指定的话,就需要在扩展的pygame.sprite.Sprite类中设置radius属性,若没有设置这个属性,则函数会根据图像大小来计算半径,但是自动计算的半径并不能总是非常精确地检测出冲突,因为它的半径计算是一个能够包住整个精灵图像的最小的圆的半径。使用方法如下:

if pygame.sprite.collide_circle(first, second):
	print_text(font, 400, 300, "oh, my God!")

同前面的类似,这里不在赘述

同样的,它也有一个变体,即pygame.sprite.collide_circle_radio(),也是多一个浮点数的参数,使用方法类似:

pygame.sprite.collide_circle_ratio(0.75)(first, second)
两个精灵之间的像素精确遮罩检测

pygame.sprite中的最后一个冲突检测函数为pygame.sprite.collide_mask(),要使用这个函数,则必须在精灵类中加上一个mask属性,即包含了针对精灵的冲突像素的遮罩像素的一幅图像,若不设置mask属性而使用这个函数,则函数会自动生成一个mask,但是这样并不好,因为若是这样的话,之后的每一次冲突检测都需要生成mask,消耗会非常大,因此最好就是自己设置一个mask属性。

虽然这个函数的冲突检测效果非常好,但是一般都很少用到,除非是对于那种移动缓慢且高精度的游戏才会需要这个函数。

精灵和组之间的矩形冲突

首先第一个组冲突函数为pygame.sprite.spritecollide(),一次函数调用,则一个组中的所有精灵都会针对另一个单个的精灵进行冲突检测,而发生冲突的精灵的一个列表会作为结果返回。可以传递三个参数,第一个参数为单个的精灵,第二个参数为组,第三个参数为一个布尔值,若为True,则会删除组中发生冲突的那些精灵,为了管理“销毁工作”,则所有删除的精灵都在列表中返回。

collide_list = pygame.sprite.spritecollide(arrow, flock_of_birds, False)

还有一个变体,即pygame.sprite.spritecollideany(),其区别为一个快速版本,即当组中精灵发生冲突时,只是返回一个布尔值,而不是返回所有精灵,使用方法:

if pygame.sprite.spritecollideany(arrow, flock_of_birds):
	print_text(font, 300, 300, "collide!")
两个组之间的矩形冲突检测

pygame.sprite.groupcollideany()函数是用来检测两个精灵组之间的冲突,返回值为一个字典,第一组中的每个精灵都会添加到该字典中,然后第二组中与之相冲突的每个精灵都会添加到字典中第一组的条目中。两个额外的布尔值参数,指定了当发生冲突时,是否应该从第一组或第二组中删除精灵:

hit_list = pygame.sprite.groupcollideany(bombs, cities, True, False)

如上代码中,检测炸d和城市之间的碰撞,发生碰撞之后,则炸d消失。

Zombie Mob游戏

在此之前,由于前面的笔记中已经构建了Point类和MySprite类,以及print_text()函数,这几个都是重复的代码,因此为了简化 *** 作,可以将它们放到一个python文件中,然后我们只需要导入即可,因此我们创建一个名为MyLibrary的python文件:

import random
import sys
import math
import pygame
from pygame.locals import *


# 类中的X、Y、position用于设置精灵的位置
class MySprite(pygame.sprite.Sprite):
    def __init__(self):
        # 调用父类的初始化方法
        pygame.sprite.Sprite.__init__(self)
        self.master_image = None
        self.frame = 0
        self.old_frame = -1
        self.frame_width = 1
        self.frame_height = 1
        self.first_frame = 0
        self.last_frame = 0
        self.columns = 1
        self.last_time = 0

    def getx(self):
        return self.rect.x

    def setx(self, value):
        self.rect.x = value
    X = property(getx, setx)

    def gety(self):
        return self.rect.y

    def sety(self, value):
        self.rect.y = value
    Y = property(gety, sety)

    def getpos(self):
        return self.rect.topleft

    def setpos(self, pos):
        self.rect.topleft = pos
    position = property(getpos, setpos)

    def load(self, filename, width, height, columns):
        self.master_image = pygame.image.load(filename).convert_alpha()
        self.frame_width = width
        self.frame_height = height
        self.rect = Rect(0, 0, width, height)
        self.columns = columns
        rect = self.master_image.get_rect()
        self.last_frame = (rect.width // width) * (rect.height // height) - 1

    def update(self, current_time, rate=30):
        # 帧变动
        if current_time > self.last_time + rate:
            self.frame += 1
        if self.frame > self.last_frame:
            self.frame = self.first_frame
        self.last_time = current_time
        # 当帧发生变化时,进行修改
        if self.frame != self.old_frame:
            frame_x = (self.frame % self.columns) * self.frame_width
            frame_y = (self.frame // self.columns) * self.frame_height
            rect = Rect(frame_x, frame_y, self.frame_width, self.frame_height)
            # 将要展示的图片送给image属性,以便展示出来
            self.image = self.master_image.subsurface(rect)
            self.old_frame = self.frame

    def __str__(self):
        return str(self.frame) + "," + str(self.first_frame) + "," + str(self.last_frame) + 
               "," + str(self.frame_width) + "," + str(self.frame_height) + "," + 
               str(self.columns) + "," + str(self.rect)


class Point(object):
    def __init__(self, x, y):
        self.__x = x
        self.__y = y

    def getx(self):
        return self.__x

    def setx(self, x):
        self.__x = x
    x = property(getx, setx)

    def gety(self):
        return self.__y

    def sety(self, y):
        self.__y = y
    y = property(gety, sety)

    def __str__(self):
        return "{X:" + "{:.0f}".format(self.__x) + ",Y:" + "{:.0f}".format(self.__y) + "}"


def print_text(font, x, y, text, color=(255, 255, 255), shadow=True):
    imgText = font.render(text, True, color)
    screen = pygame.display.get_surface()
    screen.blit(imgText, (x, y))

之后只需要导入该文件即可。

那么接下来要使用的精灵图如下:
僵尸
玩家:

可以看到,都是8行8列,共64帧,文件大小为768768,每个帧为96像素96像素,然后我们选取其中的4个方向,即上下左右,亦即第1、3、5、7行。因此我们需要对已有的MySprite类再添加一些东西:

class MySprite(pygame.sprite.Sprite):
    def __init__(self, image_file):
        # 调用父类的初始化方法
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load(image_file).convert_alpha()
        self.rect = self.image.get_rect()
        self.master_image = None
        self.frame = 0
        self.old_frame = -1
        self.frame_width = 1
        self.frame_height = 1
        self.first_frame = 0
        self.last_frame = 0
        self.columns = 1
        self.last_time = 0
        self.direction = 0
        self.velocity = Point(0.0, 0.0)

即添加了direction属性和velocity属性用于确定玩家的方向和位置。

创建僵尸精灵组

这里创建了10个僵尸,方向和位置都是随机的:

# 创建10个僵尸,位置随机,方向随机
# zombie_image = pygame.image.load("zombie walk.png").convert_alpha()
for n in range(0, 10):
	zombie = MySprite()
    zombie.load("zombie walk.png", 96, 96, 8)
    zombie.position = random.randint(0, 700), random.randint(0, 500)
    zombie.direction = random.randint(0, 3) * 2
    zombie_group.add(zombie)
与僵尸的冲突

代码中对于冲突的检测,首先使用pygame.sprite.spritecollideany()判断是否与某一个僵尸发生了冲突,若返回一个冲突,则使用pygame.sprite.collide_rect_ratio()函数进行更加精确的检测。碰撞之后,僵尸会往后退,玩家掉血:

# 检测玩家是否与僵尸发生碰撞
attacker = None
attacker = pygame.sprite.spritecollideany(player, zombie_group)
if attacker is not None:
	# 若发生碰撞,则进行更加精确的检测
    if pygame.sprite.collide_rect_ratio(0.5)(player, attacker):
		player_health -= 10
    	if attacker.X < player.X:
        	attacker.X -= 10
    	elif attacker.X > player.X:
        	attacker.X += 10
    else:
        attacker = None
血包的位置处理

血包的初始位置固定,之后玩家拾取这个血包可以加30的血量,新的血包随机出现:

# 玩家捡起血包
if pygame.sprite.collide_rect_ratio(0.5)(player, health):
	player_health += 30
    if player_health > 100:
		player_health = 100
	health.position = random.randint(0, 700), random.randint(0, 500)
完整的源代码

源代码如下:

import random
import sys
import pygame
from pygame.locals import *
from MyLibrary import *


def calc_velocity(direction, vel=1.0):
    velocity = Point(0, 0)
    # 上
    if direction == 0:
        velocity.y = -vel
    # 右
    elif direction == 2:
        velocity.x = vel
    # 下
    elif direction == 4:
        velocity.y = vel
    # 左
    elif direction == 6:
        velocity.x = -vel
    return velocity


# 转向
def reverse_direction(sprite):
    if sprite.direction == 0:
        sprite.direction = 4
    elif sprite.direction == 2:
        sprite.direction = 6
    elif sprite.direction == 4:
        sprite.direction = 0
    elif sprite.direction == 6:
        sprite.direction = 2


if __name__ == "__main__":
    pygame.init()
    screen = pygame.display.set_mode((800, 600))
    pygame.display.set_caption("Collision Demo")
    font = pygame.font.Font(None, 40)
    # 使帧变动按照一定的时间间隔发生
    timer = pygame.time.Clock()
    # 创建组
    player_group = pygame.sprite.Group()
    zombie_group = pygame.sprite.Group()
    health_group = pygame.sprite.Group()
    # 创建玩家,初始方向为下
    player = MySprite()
    player.load("farmer walk.png", 96, 96, 8)
    player.position = 80, 80
    player.direction = 4
    player_group.add(player)
    # 创建10个僵尸,位置随机,方向随机
    # zombie_image = pygame.image.load("zombie walk.png").convert_alpha()
    for n in range(0, 10):
        zombie = MySprite()
        zombie.load("zombie walk.png", 96, 96, 8)
        zombie.position = random.randint(0, 700), random.randint(0, 500)
        zombie.direction = random.randint(0, 3) * 2
        zombie_group.add(zombie)
    # 创建血包
    health = MySprite()
    health.load("health.png", 32, 32, 1)
    health.position = 400, 300
    health_group.add(health)

    game_over = False
    player_moving = False
    player_health = 100

    while True:
        # 将帧速率设置为30
        timer.tick(30)
        # 为精灵动画定时
        ticks = pygame.time.get_ticks()

        for event in pygame.event.get():
            if event.type == QUIT:
                sys.exit()
        keys = pygame.key.get_pressed()
        if keys[K_ESCAPE]:
            sys.exit()
        # 玩家向上移动
        elif keys[K_w] or keys[K_UP]:
            player.direction = 0
            player_moving = True
        elif keys[K_d] or keys[K_RIGHT]:
            player.direction = 2
            player_moving = True
        elif keys[K_s] or keys[K_DOWN]:
            player.direction = 4
            player_moving = True
        elif keys[K_a] or keys[K_LEFT]:
            player.direction = 6
            player_moving = True
        else:
            player_moving = False

        if not game_over:
            # 根据玩家方向设置动画帧
            player.first_frame = player.direction * player.columns
            player.last_frame = player.first_frame + player.columns - 1
            if player.frame < player.first_frame:
                player.frame = player.first_frame

            if not player_moving:
                # 玩家不按键就不移动
                player.frame = player.first_frame = player.last_frame
            else:
                # 设置移动的速度
                player.velocity = calc_velocity(player.direction, 1.5)
                player.velocity.x *= 1.5
                player.velocity.y *= 1.5
        # 更新玩家组
        player_group.update(ticks, 50)
        # 手动移动玩家
        if player_moving:
            player.X += player.velocity.x
            player.Y += player.velocity.y
            if player.X < 0:
                player.X = 0
            elif player.X > 700:
                player.X = 700
            if player.Y < 0:
                player.Y = 0
            elif player.Y > 500:
                player.Y = 500
        # 更新僵尸组
        zombie_group.update(ticks, 50)
        # 手动处理所有僵尸
        for z in zombie_group:
            z.first_frame = z.direction * z.columns
            z.last_frame = z.first_frame + z.columns - 1
            if z.frame < z.first_frame:
                z.frame = z.first_frame
            z.velocity = calc_velocity(z.direction)

            z.X += z.velocity.x
            z.Y += z.velocity.y
            if z.X < 0 or z.X > 700 or z.Y < 0 or z.Y > 500:
                reverse_direction(z)

        # 检测玩家是否与僵尸发生碰撞
        attacker = None
        attacker = pygame.sprite.spritecollideany(player, zombie_group)
        if attacker is not None:
            # 若发生碰撞,则进行更加精确的检测
            if pygame.sprite.collide_rect_ratio(0.5)(player, attacker):
                player_health -= 10
                if attacker.X < player.X:
                    attacker.X -= 10
                elif attacker.X > player.X:
                    attacker.X += 10
            else:
                attacker = None
        # 更新血包组
        health_group.update(ticks, 50)
        # 玩家捡起血包
        if pygame.sprite.collide_rect_ratio(0.5)(player, health):
            player_health += 30
            if player_health > 100:
                player_health = 100
            health.position = random.randint(0, 700), random.randint(0, 500)
        # 玩家死亡
        if player_health <= 0:
            game_over = True

        screen.fill((50, 50, 100))
        health_group.draw(screen)
        zombie_group.draw(screen)
        player_group.draw(screen)

        # 画血条
        pygame.draw.rect(screen, (50, 150, 50, 180), Rect(300, 570, player_health * 2, 25))
        pygame.draw.rect(screen, (100, 200, 100, 180), Rect(300, 570, 200, 25), 2)

        if game_over:
            print_text(font, 300, 100, "G A M E O V E R")

        pygame.display.update()


运行结果如下:

任务一:使用定时器变量,使得每10秒增加一个僵尸

只需要在循环中根据定时器变量的值添加僵尸即可:

# ticks是毫秒级,每10秒生成一个新的僵尸
if ticks % 10000 == 0:
	zombie = MySprite()
    zombie.load("zombie walk.png", 96, 96, 8)
    zombie.position = random.randint(0, 700), random.randint(0, 500)
    zombie.direction = random.randint(0, 3) * 2
    zombie_group.add(zombie)

运行结果如下:

可以看到,僵尸数量增加了

任务二:在任务一的基础上添加多个血包

这里可以参考前面的多个僵尸的处理方法,在创建多个血包的地方修改如下:

# 创建3个血包
for n in range(0, 3):
	health = MySprite()
    health.load("health.png", 32, 32, 1)
    health.position = random.randint(0, 700), random.randint(0, 500)
    health_group.add(health)

如上代码即可创建3个血包,之后玩家拾取血包的处理方法也是一样:

# 玩家捡起血包
attacker = None
attacker = pygame.sprite.spritecollideany(player, health_group)
if attacker is not None:
	if pygame.sprite.collide_rect_ratio(0.5)(player, attacker):
		player_health += 30
        if player_health > 100:
        	player_health = 100
		attacker.position = random.randint(0, 700), random.randint(0, 500)

运行结果如下:

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5700830.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存