赛博红兔的科技博客

CyberHongTu shares news, insights, and musings on fascinating technology subjects.


和我一起做3A游戏《银河争霸》

大家好,我是赛博红兔。欢迎继续收看我的《和我一起做3A游戏》系列!今天我们将深入探讨pygame,并介绍一款全新的射击游戏——《银河争霸》。经过前三集的《归乡之路》,相信大家已对pygame有了实际的了解。今天,我给大家推荐第二款我编写的游戏:《银河争霸》。《银河争霸》是一款基于Pygame平台开发的动感射击游戏,游戏中玩家将控制飞船在浩瀚的星际中展开激烈的对战。这款游戏的设计精美,操作简便,是射击游戏爱好者的不错选择。在《银河争霸》中,两位玩家分别操作为红色飞船和黄色飞船。红色飞船使用方向键移动,右Ctrl键射击;黄色飞船则通过WASD键移动,左Ctrl键进行射击。每架飞船在一定时间内发射的子弹数量有限制,精确射击和灵巧的走位是取胜的关键。游戏中央设有边界,飞船不能越过中间的界限,这增加了游戏的策略性和挑战性。每个飞船的尾部设有血条显示,每次被击中,生命值都会减少,生命值归零时,飞船坠毁,游戏就结束了,屏幕将显示胜利者信息,并在短暂停留后返回标题屏幕。游戏画面以太空为背景,提供了一场视觉和听觉的双重盛宴。整体游戏风格科幻,带玩家进入一个充满未来科技感的宇宙战场。

说了这么多大家是不是想试试啦?我已经把这个游戏用到的资源、代码还有EXE的应用程序打包放在百度网盘了,链接会放在下面,代码也可以去我的GitHub或者博客下载。你可以按照上面的安装说明部署项目,都写得比较详细了。如果,只想玩这个游戏的朋友,可以直接运行我打包好的EXE应用文件,在电脑上直接跑不需要安装python,但是可能需要有声卡。记住,一定要把应用程序和assets也就是游戏资源包放在一起,不然会出错。

想继续学习这游戏怎么做的朋友继续往下看。首先,我在第一集已经介绍了我所有的游戏资源,包括图片、声效、BGM,都是从这两个开源网站下载的,大家也可以共享一下自己了解的游戏资源库。接下来我们看到的是游戏源代码,这里,我主要会重点介绍之前没有见过的pygame功能和游戏特色部分的代码。具体大伙可以按照我的源代码去学习和拓展。我们的项目里就一个主文件,asset文件夹里都是游戏的资源文件,没有上几集settings的配置文件,因为游戏常量不多所以都放在主文件里了。 先导入了pygame和os的库,对pygame进行初始化,接下来都是游戏常量我们在用到的时候再来介绍。我们来一起看一下游戏本体的Game类,这个游戏比较简单所以这里我们没有建立任何精灵类。

Game类初始化

        self.yellow_ship = pygame.Rect(
            100, 300, SPACESHIP_WIDTH, SPACESHIP_HEIGHT)
        self.red_ship = pygame.Rect(
            700, 300, SPACESHIP_WIDTH, SPACESHIP_HEIGHT)

这两行代码初始化了两个飞船(黄色和红色),每个飞船在游戏窗口中的初始位置及大小。pygame.Rect 是 Pygame 中用来存储矩形对象的类,这里用它来定位飞船,并设置其宽度和高度。

        self.yellow_bullets = []
        self.red_bullets = []
        self.yellow_health = 10
        self.red_health = 10
        self.winner_text = ''
        self.game_active = False
        self.flash_counter = 0

创建两个列表,用来存储黄色飞船和红色飞船发射的子弹。这些列表在游戏运行过程中将被用来追踪所有活动的子弹。设置了每个飞船的初始健康值(或称生命值)为10。这个数值在飞船被子弹击中时会减少。初始化一个空字符串用来存储游戏结束时的胜利者信息。设置一个布尔变量,用于判断当前游戏是否处于活动状态。当游戏未开始或已结束时,此变量为False。最后,计数器用于控制一些闪烁效果的频率,如游戏开始前的提示信息。

        # Play BGM
        pygame.mixer.music.set_volume(0.3)
        pygame.mixer.music.play(loops=-1)

这部分代码负责播放背景音乐。首先设置音量为0.3,然后开始播放,其中 loops=-1 表示音乐会无限循环播放。

draw_window 方法

    def draw_window(self, yellow_ship, red_ship, yellow_bullets, red_bullets, yellow_health, red_health):
        # Draw the game background
        GAME_DISPLAY.blit(GAME_BACKGROUND, (0, 0))
        # Draw the border
        pygame.draw.rect(GAME_DISPLAY, 'black', BORDER)

draw_window 方法接收多个参数:黄色和红色飞船的位置、子弹列表和各自的健康值,用于在游戏窗口中绘制相应的元素。使用 blit 方法将游戏背景图像(GAME_BACKGROUND)绘制到游戏显示窗口(GAME_DISPLAY)的指定位置,这里是窗口的左上角坐标(0, 0)。最后,绘制一个黑色的边界矩形在游戏窗口中央,使用的是 BORDER 矩形对象,这条线分隔游戏窗口为两部分,限制两个飞船的活动区域。

        # Draw the yellow health bar
        yellow_health_bar = pygame.Rect(
            yellow_ship.x - 10, yellow_ship.y - 20, 10, 100)
        yellow_current_health_bar = pygame.Rect(
            yellow_ship.x - 10, yellow_ship.y - 20, 10, yellow_health * 10)
        pygame.draw.rect(GAME_DISPLAY, (255, 255, 255), yellow_health_bar, 1)
        pygame.draw.rect(GAME_DISPLAY, 'green', yellow_current_health_bar)
        
        # Draw the red health bar
        red_health_bar = pygame.Rect(red_ship.x + 40, red_ship.y - 20, 10, 100)
        red_current_health_bar = pygame.Rect(
            red_ship.x + 40, red_ship.y - 20, 10, red_health * 10)
        pygame.draw.rect(GAME_DISPLAY, (255, 255, 255), red_health_bar, 1)
        pygame.draw.rect(GAME_DISPLAY, 'green', red_current_health_bar)

这几行代码负责绘制飞船的生命值条。首先定义了生命值条的背景和当前生命值。生命条的长度是根据飞船的当前生命值动态计算的。生命条的背景是白色的,当前生命值用绿色表示。

        # Draw the yellow spaceship
        GAME_DISPLAY.blit(YELLOW_SPACESHIP, (yellow_ship.x, yellow_ship.y))

        # Draw the red spaceship
        GAME_DISPLAY.blit(RED_SPACESHIP, (red_ship.x, red_ship.y))

        for bullet in yellow_bullets:
            pygame.draw.rect(GAME_DISPLAY, 'yellow', bullet)

        for bullet in red_bullets:
            pygame.draw.rect(GAME_DISPLAY, 'red', bullet)

将飞船图像绘制在飞船的当前位置。遍历子弹列表,在游戏窗口中绘制每一颗子弹。

yellow_handle_movememnt方法

 def yellow_handle_movememnt(self, keys_pressed, yellow_ship):
        if keys_pressed[pygame.K_a] and yellow_ship.x - SPACESHIP_VEL > 0:  # Left Key
            yellow_ship.x -= SPACESHIP_VEL
        if keys_pressed[pygame.K_d] and yellow_ship.x + yellow_ship.w // 1.3 + SPACESHIP_VEL < BORDER.x:  # right Key
            yellow_ship.x += SPACESHIP_VEL
        if keys_pressed[pygame.K_w] and yellow_ship.y - SPACESHIP_VEL > 0:  # up Key
            yellow_ship.y -= SPACESHIP_VEL
        if keys_pressed[pygame.K_s] and yellow_ship.y + yellow_ship.h + SPACESHIP_VEL < HEIGHT - 15:  # down Key
            yellow_ship.y += SPACESHIP_VEL

我们拿左侧的黄色飞船举例子,它接收两个参数:keys_pressed(一个记录了所有按键状态的字典)还有一个是飞船的矩形对象。接下来,检查是否按下了“A”键(K_a)。如果按下,并且黄色飞船的当前x坐标减去定义的飞船速度(SPACESHIP_VEL)后大于0(确保飞船不会离开屏幕边界),那么飞船向左移动。检查是否按下了“D”键(K_d)。如果按下,并且黄色飞船当前的x坐标加上飞船宽度的一定比例再加上飞船速度小于屏幕中央的边界位置(BORDER.x),确保飞船不会穿越到红色飞船的区域,那么飞船向右移动。检查是否按下了“W”键(K_w)。如果按下,并且飞船的当前y坐标减去飞船速度大于0(确保飞船不会离开屏幕顶部),那么飞船向上移动。最后,检查是否按下了“S”键(K_s)。如果按下,并且黄色飞船的当前y坐标加上飞船的高度再加上飞船速度小于屏幕的高度减去15(这里的15是为了留一些边距,确保飞船不会离开屏幕底部),那么飞船向下移动。所以,总的来说这个方法通过读取玩家的键盘输入来控制黄色飞船的方向,确保飞船的移动在屏幕内,并且不越过中间的边界线。

handle_bullets 方法

for bullet in yellow_bullets:
            bullet.x += BULLET_VEL
            if red_ship.colliderect(bullet):
                pygame.event.post(pygame.event.Event(RED_HIT))
                yellow_bullets.remove(bullet)
            elif bullet.x > WIDTH:
                yellow_bullets.remove(bullet)

        for bullet in red_bullets:
            bullet.x -= BULLET_VEL
            if yellow_ship.colliderect(bullet):
                pygame.event.post(pygame.event.Event(YELLOW_HIT))
                red_bullets.remove(bullet)
            elif bullet.x < 0:
                red_bullets.remove(bullet)

接下来是处理游戏中的子弹逻辑,包括移动子弹和处理子弹与飞船的碰撞。我们定义了 handle_bullets 这个方法,它接收四个参数:黄色子弹列表 yellow_bullets,红色子弹列表 red_bullets,以及黄色和红色飞船的矩形对象 yellow_ship 和 red_ship。这段代码遍历黄色子弹列表中的每一颗子弹,将每颗子弹的 x 坐标增加 BULLET_VEL(子弹的速度),使子弹向右移动。然后,我们检查每颗黄色子弹是否与红色飞船碰撞(也就是是否击中飞船)。如果有击中了,那么将发布一个 RED_HIT 事件到 Pygame 事件队列,并从黄色子弹列表中移除这颗子弹,表示子弹击中了目标后消失。这是不使用精灵组的方法。如果子弹错过飞船了,我们检查每颗黄色子弹是否超出了屏幕的右边界。如果是,那么从黄色子弹列表中移除这颗子弹,防止它无限移动下去。下面的红色子弹是同样的道理,我们就不多解释了。整个 handle_bullets 方法负责处理子弹的移动和子弹与飞船的碰撞逻辑,通过不断更新子弹位置并在必要时移除子弹。

draw_title_screen 方法

def draw_title_screen(self):
        '''Draw the title page of the game, including the game message, guide text, game name, and spaceship images.'''
        def blit_centered(surface, x_factor, y_factor):
            x = (WIDTH * x_factor - surface.get_width() // 2)
            y = (HEIGHT * y_factor - surface.get_height() // 2)
            GAME_DISPLAY.blit(surface, (x, y))

        GAME_DISPLAY.blit(GAME_TITLE_SCREEN, (0, 0))

        if self.flash_counter % FPS < 30:
            blit_centered(GAME_MESSAGE, 1/2, 1/1.2)

        blit_centered(GAME_GUIDE1, 1/4, 1/1.5)
        blit_centered(GAME_GUIDE2, 1/1.35, 1/1.5)
        blit_centered(GAME_NAME, 1/2, 1/7)
        blit_centered(YELLOW_SPACESHIP, 1/4, 1/2)
        blit_centered(RED_SPACESHIP, 1/1.35, 1/2)

        pygame.display.update()

这块代码比较有特色,它负责绘制一个闪烁效果的文本,也就是游戏开始时候的“按空格键开始”的字样。我们通过 self.flash_counter % FPS 产生一个周期性的数字,用这个数字决定是否显示提示信息。这里 < 30 表示每秒钟会有半秒钟的时间显示这个信息,以引起玩家的注意。

main_loop 方法游戏的主循环

if self.game_active:
                    if event.type == pygame.KEYDOWN:  # Shoot bullets
                        if event.key == pygame.K_LCTRL and len(self.yellow_bullets) < MAX_BULLETS:
                            bullet = pygame.Rect(
                                self.yellow_ship.x + self.yellow_ship.w // 2, self.yellow_ship.y + self.yellow_ship.h//2 + 4, 10, 5)
                            self.yellow_bullets.append(bullet)
                            BULLET_FIRE_SOUND.play()

                        if event.key == pygame.K_RCTRL and len(self.red_bullets) < MAX_BULLETS:
                            bullet = pygame.Rect(
                                self.red_ship.x, self.red_ship.y + self.red_ship.h//2 + 4, 10, 5)
                            self.red_bullets.append(bullet)
                            BULLET_FIRE_SOUND.play()

                    if event.type == YELLOW_HIT:  # Yellow ship is hit event
                        self.yellow_health -= 1
                        BULLET_HIT_SOUND.play()

                    if event.type == RED_HIT:  # Red ship is hit event
                        self.red_health -= 1
                        BULLET_HIT_SOUND.play()
                else:
                    if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
                        self.game_active = True
                        self.flash_counter = 0
                        self.yellow_health = 10
                        self.red_health = 10
                        self.yellow_bullets.clear()
                        self.red_bullets.clear()
                        self.winner_text = ''

一些基本的操作包括设置游戏的时钟、FPS和退出,我就不多讲了,不了解的可以从这个系列的第一集开始看。我们来看一些重点代码块。我们来看一下从 Pygame 事件队列中遍历获取的所有事件的代码块。在这里,如果 self.game_active 为真(代表游戏正在进行中),且按下了控制键(左Ctrl或右Ctrl),且当前子弹数小于最大允许数 MAX_BULLETS,咱们可以修改这个常量,这里我们设为3颗子弹。满足上面这些条件,就创建一颗新的子弹并加入到相应的子弹列表中,同时播放子弹发射的声音。当一个飞船被击中时,相应的事件 YELLOW_HIT 或 RED_HIT 会被触发,相应的飞船健康值将减少,并播放被击中的声音效果。如果游戏不在激活状态,这段代码允许玩家通过按空格键来开始游戏,同时重置游戏状态,包括双方的健康值和子弹列表,准备新一轮的游戏。

if self.game_active:
                # Generate Game Result
                if self.red_health <= 0 and self.yellow_health <= 0:
                    self.winner_text = "It's a tie!"
                elif self.red_health <= 0:
                    self.winner_text = 'Yellow Wins!'
                elif self.yellow_health <= 0:
                    self.winner_text = 'Red Wins!'
                if self.winner_text != '':
                    self.draw_winner(self.winner_text)
                    pygame.time.delay(2000)
                    self.game_active = False

                # Battle Ship's Movement
                keys_pressed = pygame.key.get_pressed()
                self.yellow_handle_movememnt(keys_pressed, self.yellow_ship)
                self.red_handle_movememnt(keys_pressed, self.red_ship)

                # Handle Bullets
                self.handle_bullets(self.yellow_bullets, self.red_bullets,
                                    self.yellow_ship, self.red_ship)
                self.draw_window(self.yellow_ship, self.red_ship, self.yellow_bullets,
                                 self.red_bullets, self.yellow_health, self.red_health)

            else:
                # Generate Title Screen
                self.draw_title_screen()

接下来这部分代码在游戏激活状态下判断游戏胜负。根据双方的健康值来确定胜利者,或者是打平。并且显示胜利的信息。延迟两秒后重新设置游戏为非激活状态,等待重新开始。接下里,就是处理飞船的移动、子弹的行为以及绘制当前游戏状态。它首先获取当前按键的状态,然后调用之前定义的飞船移动和子弹处理方法,并在每次循环中不停地更新游戏窗口。如果游戏不在激活状态,则绘制标题屏幕,等待玩家开始游戏。总的来说,游戏主循环是游戏运行的核心,负责接收用户输入,更新游戏状态,以及在屏幕上绘制游戏的实时画面。

大功告成!现在我们就一起进入《银河争霸》这款游戏的世界。好了,接下来,就由大家亲自体验《银河争霸》的魅力吧!愿大家都能找到一起玩这个游戏的小伙伴。遇到什么问题或者BUG请给我留言。未来,我们还将一起探索更多精彩的“3A”游戏,让我们在游戏的世界里一起成长!那就这样,咱们下期再见!



Leave a comment