pygame 游戏模组:摄像机控制

系列 - pygame 模组

本文介绍了为 pygame 编写的摄像机控制模组,可方便地使用键鼠平移、缩放游戏界面,并将游戏视角锁定在某一对象上。

总览

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
import pygame

class Camera(pygame.sprite.Group):
    def __init__(self, size, *sprites):
        super().__init__(*sprites)

        self.width, self.height = size
        self.screen_center = pygame.Vector2(self.width / 2, self.height / 2)
        self.rect = pygame.Rect((-self.width, -self.height), (3 * self.width, 3 * self.height))

        self.move_speed = 300
        self.scale_speed = 0.6
        self.scale = 1.0

        self.offset = self.screen_center.copy()
        self.center = self.screen_center.copy()
        # 鼠标悬停对象
        self.hover = None
        # 鼠标锁定对象
        self.target = None

        self.color_hover  = 'green'
        self.color_target = 'red'
        self.border = 2

    def center_target(self):
        # 如果有锁定对象,将锁定对象固定到屏幕中央
        if sprite := self.target:
            self.center = sprite.position
            self.offset = self.screen_center.copy()

    def zoom(self, dt, factor):
        self.scale *= 1 + factor * self.scale_speed * dt
        
    def keyboard_control(self, dt, pressed_keys):
        if pressed_keys[pygame.K_UP]:
            self.offset.y -= self.move_speed * dt
        elif pressed_keys[pygame.K_DOWN]:
            self.offset.y += self.move_speed * dt
        if pressed_keys[pygame.K_LEFT]:
            self.offset.x -= self.move_speed * dt
        elif pressed_keys[pygame.K_RIGHT]:
            self.offset.x += self.move_speed * dt
        if pressed_keys[pygame.K_MINUS]:
            self.zoom(dt, -1)
        elif pressed_keys[pygame.K_EQUALS]:
            self.zoom(dt, 1)

    # 判断当前鼠标位置是否与游戏中某个对象重合
    def mouse_pick(self, mouse_pos):
        for sprite in self.sprites():
            pos = self.project2screen(sprite.position)
            rect = sprite.image.get_rect(center = pos)
            if rect.collidepoint(mouse_pos):
                return sprite

    def on_click(self, mouse_pos):
        # 当在游戏对象上点击鼠标时
        if sprite := self.mouse_pick(mouse_pos):
            # 将偏移向量设定为锁定目标当前在屏幕上的位置
            self.offset = self.project2screen(sprite.position)
            # 将摄像机锁定在目标对象上,center 随对象运动而改变
            self.center = sprite.position
            self.target = sprite
        # 当在游戏界面空白(无可选对象)处点击鼠标时
        else:
            # 将摄像机解锁,center 不随对象运动而改变
            self.center = self.center.copy()

    def mouse_control(self, dt, mouse_pos, pressed_buttons):
        # 刷新鼠标悬停对象
        if sprite := self.mouse_pick(mouse_pos):
            self.hover = sprite
        else:
            self.hover = None

        if True in pressed_buttons:
            # 鼠标拖拽
            self.offset += pygame.Vector2(pygame.mouse.get_rel())
        else:
            self.mouse_push(dt, mouse_pos)

    def mouse_push(self, dt, mouse_pos):
        padding = 5

        left = padding
        right = self.width - padding
        top = padding
        bottom = self.height - padding

        mouse_out = pygame.Vector2(mouse_pos)
        x_in = max(left, min(mouse_out.x, right))
        y_in = max(top,  min(mouse_out.y, bottom))
        mouse_in = pygame.Vector2(x_in, y_in)

        movement = mouse_out - mouse_in
        try:
            movement.scale_to_length(self.move_speed * dt)
        except:
            pass
        finally:
            self.offset -= movement

    def draw(self, screen):
        sequence = []
        for sprite in self.sprites():

            # 游戏对象进行相应缩放
            sprite.scale_image(self.scale)

            pos = self.project2screen(sprite.position)
            rect = sprite.image.get_rect(center = pos)
            if self.rect.colliderect(rect):
                sequence.append((sprite.image, rect))
        screen.blits(sequence)

        # 鼠标悬停对象周围绘制绿框
        if sprite := self.hover:
            pos = self.project2screen(sprite.position)
            rect = sprite.image.get_rect(center = pos)
            pygame.draw.ellipse(screen, self.color_hover, rect.inflate(self.border * 2, self.border * 2), self.border)

        # 鼠标锁定对象周围绘制红框
        if sprite := self.target:
            # 判断锁定对象是否被移除
            if sprite in self.sprites():
                pos = self.project2screen(sprite.position)
                rect = sprite.image.get_rect(center = pos)
                pygame.draw.ellipse(screen, self.color_target, rect.inflate(self.border * 2, self.border * 2), self.border)
            else:
                self.target = None

    def project2screen(self, pos):
        return (pos - self.center) * self.scale + self.offset

    def project2real(self, pos):
        pos = pygame.Vector2(pos)
        return (pos - self.offset) / self.scale + self.center

    def update(self, dt, mouse_pos, pressed_keys, pressed_buttons):
        self.keyboard_control(dt, pressed_keys)
        self.mouse_control(dt, mouse_pos, pressed_buttons)

Camera 类继承自 pygame.sprite.Group,初始化时只需给定游戏屏幕尺寸。

1
2
3
class Camera(pygame.sprite.Group):
    def __init__(self, size, *sprites):
        super().__init__(*sprites)

后续将所有需要绘制到游戏屏幕的对象加入 camera 实例即可。
游戏主循环中处理完游戏对象之间的相互作用后,只需调用 camera.update()camera.draw() 函数即可在游戏界面上绘制所有游戏对象。

核心函数

1
2
    def project2screen(self, pos):
        return (pos - self.center) * self.scale + self.offset

输入参数 pos 为游戏对象的位置坐标,函数返回值为游戏对象在屏幕上的绘制坐标。 决定摄像机行为(游戏对象在屏幕上呈现效果)的关键参数为:

  • self.center(float, float)
    摄像机锁定(对象)的位置坐标。
  • self.scalefloat
    摄像机的缩放比例。
  • self.offset(float, float)
    摄像机锁定的位置坐标在屏幕上对应的绘制坐标(偏移向量)。
` ( R p E o A s L - R E c G e I n O t N e r ) c e n t e r p o s ` ( p o s - c e n t e r ) ( 0 · , s 0 c ) a l e ` ( o f f s e ` t ( ) c o e f n f t s e e r t * ) p o s * S C R E E N R E G I O N

上图左半部分为游戏对象的实际位置空间(REAL REGION),右半部分圆角框内为屏幕绘制空间(SCREEN REGION)。

  • 摄像机锁定位置坐标 center 投影到屏幕空间的绘制坐标为 center*,由偏移向量 `(offset) 直接决定。
  • 游戏中某一对象的实际位置坐标为 pos,其在位置空间内与摄像机锁定位置坐标 center 的关系由其位置向量 `(pos - center) 表征。
  • 游戏对象在屏幕上的绘制坐标 pos* 由其实际的位置向量 `(pos - center) 点乘缩放比例 scale 再加上偏移向量 `(offset) 决定,即 pos* = `(pos - center) · scale + `(offset)

摄像机控制经由设定或改变 centerscaleoffset 三个参数实现。

平移

改变 self.offset 值。

平移画面

直接移动游戏背景和对象。

键盘控制

按住 / / / 方向键游戏画面持续平滑移动。

1
2
3
4
5
6
7
8
9
    def keyboard_control(self, dt, pressed_keys):
        if pressed_keys[pygame.K_UP]:
            self.offset.y -= self.move_speed * dt
        elif pressed_keys[pygame.K_DOWN]:
            self.offset.y += self.move_speed * dt
        if pressed_keys[pygame.K_LEFT]:
            self.offset.x -= self.move_speed * dt
        elif pressed_keys[pygame.K_RIGHT]:
            self.offset.x += self.move_speed * dt
  • pressed_keys 为按下的键盘按键。
  • 引入每帧持续时间 dt 确保画面平移速度与帧率无关。
  • 游戏画面可斜向移动,且移动速度更快。

鼠标控制

按住  按键并移动鼠标可拖动游戏画面。

1
2
3
4
5
    def mouse_control(self, dt, mouse_pos, pressed_buttons):
        if True in pressed_buttons:
            self.offset += pygame.Vector2(pygame.mouse.get_rel())
        else:
            self.mouse_push(dt, mouse_pos)
  • 函数 pygame.mouse.get_rel() 返回鼠标位置在上次调用该函数后的移动变化。

平移镜头

 移动至屏幕边缘或屏幕外时,摄像机镜头向鼠标位置平移(即游戏画面向相反方向平移)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
    def mouse_push(self, dt, mouse_pos):
        padding = 5

        left = padding
        right = self.width - padding
        top = padding
        bottom = self.height - padding

        mouse_out = pygame.Vector2(mouse_pos)
        x_in = max(left, min(mouse_out.x, right))
        y_in = max(top,  min(mouse_out.y, bottom))
        mouse_in = pygame.Vector2(x_in, y_in)

        movement = mouse_out - mouse_in
        try:
            movement.scale_to_length(self.move_speed * dt)
        except:
            pass
        finally:
            self.offset -= movement
  • padding 定义鼠标距离屏幕边缘多近时触发镜头平移。
  • 同样,引入每帧持续时间 dt 确保画面平移速度与帧率无关。

缩放

改变 self.scale 值。

键盘控制

按住 - / + 键游戏画面持续平滑缩放。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
    def zoom(self, dt, factor):
        self.scale *= 1 + factor * self.scale_speed * dt
        
    def keyboard_control(self, dt, pressed_keys):
        if pressed_keys[pygame.K_MINUS]:
            self.zoom(dt, -1)
        elif pressed_keys[pygame.K_EQUALS]:
            self.zoom(dt, 1)

    def draw(self, screen):
        sequence = []
        for sprite in self.sprites():

            # 游戏对象进行相应缩放
            sprite.scale_image(self.scale)

            pos = self.project2screen(sprite.position)
            rect = sprite.image.get_rect(center = pos)
            if self.rect.colliderect(rect):
                sequence.append((sprite.image, rect))
        screen.blits(sequence)
  • 游戏画面可无限缩放。
  • 同样,引入每帧持续时间 dt 确保画面缩放速度与帧率无关。

鼠标滚轮控制

滚动  滚轮缩放游戏画面。

1
2
3
4
5
    async def start(self):
        while game_running:
            for event in pygame.event.get():
                if event.type == pygame.MOUSEWHEEL:
                    self.camera.zoom(dt, event.y)

悬停

当鼠标悬停在游戏对象上方时,游戏对象周围出现绿框。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
    def __init__(self, size, *sprites):
        # 鼠标悬停对象
        self.hover = None

        self.color_hover  = 'green'
        self.border = 2

    # 判断当前鼠标位置是否与游戏中某个对象重合
    def mouse_pick(self, mouse_pos):
        for sprite in self.sprites():
            pos = self.project2screen(sprite.position)
            rect = sprite.image.get_rect(center = pos)
            if rect.collidepoint(mouse_pos):
                return sprite

    def mouse_control(self, dt, mouse_pos, pressed_buttons):
        # 刷新鼠标悬停对象
        if sprite := self.mouse_pick(mouse_pos):
            self.hover = sprite
        else:
            self.hover = None

    def draw(self, screen):
        # 鼠标悬停对象周围绘制绿框
        if sprite := self.hover:
            pos = self.project2screen(sprite.position)
            rect = sprite.image.get_rect(center = pos)
            pygame.draw.ellipse(screen, self.color_hover, rect.inflate(self.border * 2, self.border * 2), self.border)

锁定

改变 self.center 值。

当在游戏对象上点击鼠标时,锁定游戏对象位置(使游戏对象相对于屏幕静止不动),在游戏对象周围绘制红框。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
    def __init__(self, size, *sprites):
        # 鼠标锁定对象
        self.target = None

        self.color_target = 'red'

    def on_click(self, mouse_pos):
        # 当在游戏对象上点击鼠标时
        if sprite := self.mouse_pick(mouse_pos):
            # 将偏移向量设定为锁定目标当前在屏幕上的位置
            self.offset = self.project2screen(sprite.position)
            # 将摄像机锁定在目标对象上,center 随对象运动而改变
            self.center = sprite.position
            self.target = sprite
        # 当在游戏界面空白(无可选对象)处点击鼠标时
        else:
            # 将摄像机解锁,center 不随对象运动而改变
            self.center = self.center.copy()

    def draw(self, screen):
        # 鼠标锁定对象周围绘制红框
        if sprite := self.target:
            # 判断锁定对象是否被移除
            if sprite in self.sprites():
                pos = self.project2screen(sprite.position)
                rect = sprite.image.get_rect(center = pos)
                pygame.draw.ellipse(screen, self.color_target, rect.inflate(self.border * 2, self.border * 2), self.border)
            else:
                self.target = None

将锁定对象固定至屏幕中央。

1
2
3
4
5
    def center_target(self):
        # 如果有锁定对象,将锁定对象固定到屏幕中央
        if sprite := self.target:
            self.center = sprite.position
            self.offset = self.screen_center.copy()

应用效果

请参考本博客游戏 Demo 恒星形成模拟器


【参考】:

0%