《永不言弃 Give It Up》,这是一款极具虐心色彩的音乐题材闯关游戏。
这篇文章就来分析这款游戏原理,并用python写出来一个简易版。废话不多说,直接开始分析。
游戏元素,暂且把主角叫做MC, 障碍柱叫做柱子。下边开始冥想透视解析游戏大法,大法命名是我自己随便取的。想象去除游戏中的图片素材,去除背景和音乐,把MC看作画布中的一个矩形,把柱子也看做一个矩形。画布中只剩下了MC矩形和障碍物的矩形。想象到了吗?什么?没有?看下图
没错,这是已经做出来的游戏截图。看着很简单吧,是不是感觉跟之前的flappy bird 有点类似?那我告诉你,部分逻辑还确实有点类似。
不同的是,小鸟是自由落体,点击屏幕才会往上,而MC碰到柱子顶部会自己弹起来。当MC需要往上的时候,给MC一个向上的速度,随着MC的上升,这个速度逐渐减少直到0,MC开始下降。然后下降到一定高度再弹起。这样就模仿了MC的弹起的过程。
这个游戏的重点需要解决的问题就是当遇到障碍,怎样改变MC弹起的高度?
MC在降落到什么位置时开始弹起?
在相同的柱子高度怎么使柱子移动两格,而在后一个柱子比较高高时还保持一次一格的跳跃?
怎么判断MC死亡?
第一个问题:怎么改变高度?
前边提到,MC在弹起时会给一个初始速度和一个固定的加速度,初始速度依次减去加速度,直到为0,MC开始下落。那么想要改变MC弹起的高度,就把初始的速度给增大。这样还会有一个问题,速度增大了,如果加速度不变,那么速度减到0的时间就会变长,那么MC跳跃的间隔就会改变。
可以看到对比非常明显,第二张图因为跳的太高从而打乱了跳跃的节奏。
别忘了,这是一个音乐题材的跑酷游戏,踩点会给人一种节奏一致的快感,如果这个节奏断掉了,那么可玩性就大大降低了。所以在这里在改变初始速度的同时,也要改变这个加速度,让这个MC弹起的更快,这样也会给玩家一个给MC加弹跳力度的感觉。
第二个问题:MC 何时弹起?
给定一个基础的高度,如果MC 的最下端大于或等于这个高度,MC开始改变运动方向,即由下落转为上升。如果所有的柱子的高度是一定的,那么这个基础高度就不用变,那这个游戏也就不用玩了,玩家只用看着游戏就能到终点了。想要改变这个高度很简单,只看MC会落到哪个柱子上,把该柱子上方的高度,设为这个基础高度就可以了。
可以看到我用红点标出的高度,只要把红点的高度设置为MC下落的最低点,就可以了。
那么在什么时候重新设置这个高度呢?这里会引出另一个问题,什么时候移动柱子,达到MC往前跳的错觉呢?
只要在MC往上弹跳时,开始移动所有的柱子,移动的距离是一个柱子的宽度加一个柱子与柱子之间的宽度。当然这个移动的速度要比MC上升的速度快,不然MC都落下来了,柱子还没有移动。当柱子完成移动时,找到MC下方的柱子,把该柱子的上方高度设置成基础高度就行了。
第三个问题:如何判定该移动一格,还是移动两格?
通过判断当前柱子和下一个柱子的高度,如果当前柱子的高度小于后面的柱子,那玩家点屏幕时,增加MC上升的速度和加速度,柱子只移动一格。如果不小于,也就是大于或者等于后面的柱子高度,那就移动两格柱子。
上图中的左图的情况,当MC落地时,下一次应该改变速度,柱子移动一格。而中间图和右图两种情况不改变速度,柱子移动两格。当然右图这时候玩家是不应该跳的,跳了会死翘翘的。
第四个问题:如何判断MC死亡?
该跳不跳,撞柱子死亡
不该跳却跳,导致直接撞柱子死亡
跳到空地,死亡
总结就是与柱子碰撞就死亡,跳到空地就死亡。
主要的逻辑分析完毕,下边就开始撸代码
1. 定义MC类
class Ball(pygame.sprite.Sprite):
def __init__(self, position, disk_group):
pygame.sprite.Sprite.__init__(self)
self.disk_group = disk_group
self.rect = pygame.Rect(*position, BALL_SIZE, BALL_SIZE)
self.is_up = False # MC上升或下落
self.init_speed() # 初始化速度
self.move_disk = False # 移动托盘
self.min_height = BASE_HEIGHT # 设置下落的最小高度
self.can_jump = False # 是否可以跳
self.step = 1 # 托盘移动步长
self.change_up_speed = False # 是否改变初始速度
self.move_speed = (DISK_SIZE[0] + DISK_GAP_WIDTH) / 10 # 托盘移动速度
self.move_width = (DISK_SIZE[0] + DISK_GAP_WIDTH) * self.step # 托盘移动距离
self.current_disk_index = 0 # 当前托盘的下标
def init_speed(self, up_speed=1): # 初始化速度
self.up_speed = INIT_SPEED * up_speed
self.init_a_speed(up_speed)
self.down_speed = 0
def init_a_speed(self, a_speed=1): # 初始化加速度
self.a_speed = A_SPEED * FPS / 1000 * a_speed
def change_speed(self, step, change_up_speed): # 更改速度
self.step = step
self.change_up_speed = change_up_speed
def update(self):
if self.is_up:
# 上升速度越来越小
self.up_speed -= self.a_speed
self.rect.top -= self.up_speed
# 上升速度小于等于0, 改为下降状态
if self.up_speed <= 0:
self.down()
else:
# 下降速度越来越大
self.down_speed += self.a_speed
self.rect.bottom += self.down_speed
if self.rect.bottom >= self.min_height - 1:
self.rect.bottom = self.min_height - 1
self.up()
if not self.next_disk: # 游戏胜利
return 1
if not self.current_disk.show: # 跳到空地
return 2
def up(self):
self.can_jump = False
if self.change_up_speed: # 如果要改变速度, 就改变速度
self.change_up_speed = False
self.init_speed(SPEED)
else:
self.init_speed()
self.is_up = True
self.move_disk = True
def down(self):
self.init_speed()
if self.min_height - self.rect.bottom >= BALL_SIZE * 1.2:
self.init_a_speed(SPEED)
self.is_up = False
self.can_jump = True
@property
def current_disk(self):
try:
return self.disk_group.sprites()[self.current_disk_index]
except:
return None
@property
def next_disk(self):
try:
return self.disk_group.sprites()[self.current_disk_index + 1]
except:
return None
def set_min_height(self): # 设置最小高度
self.min_height = self.current_disk.rect.top
def draw(self, screen): # 画精灵
for disk in self.disk_group:
disk.draw(screen)
pygame.draw.rect(screen, (255, 255, 255), self.rect)
update函数MC一直弹跳的主要逻辑,当上升速度小于0,开始下降,当下降到最小的高度开始上升。up函数中调用了改变速度的代码,当上升时,需要改变速度,就把初始速度和加速度都乘以一定的倍数。
2. 柱子类
class Disk(pygame.sprite.Sprite):
def __init__(self, position, height, level, show=True):
pygame.sprite.Sprite.__init__(self)
self.rect = pygame.Rect(*position, DISK_SIZE[0], height)
self.height = height
self.level = level
self.show = show
def draw(self, screen):
if not self.show:
return
pygame.draw.rect(screen, (255, 255, 255), self.rect, 1)
定义柱子的level和show,用来判断柱子的高度和是否需要显示。
3. 初始化游戏和精灵
def init_game():
pygame.init()
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption('Give it up!')
return screen
def init_disk_sptite(FIRST_DISK_POSITION):
disk_group = pygame.sprite.Group()
disk_group.add(Disk(FIRST_DISK_POSITION, DISK_HEIGHT, 0))
for index, i in enumerate(DISK_LIST):
if i == -1:
show = False
else:
show = True
if i <= 0:
height = DISK_HEIGHT
else:
height = DISK_INCREMENT * i + DISK_HEIGHT
disk_group.add(
Disk((FIRST_DISK_POSITION[0] + (DISK_SIZE[0] + DISK_GAP_WIDTH) *
(index + 1), FIRST_DISK_POSITION[1] - height + DISK_HEIGHT),
height, i, show))
return disk_group
4. 监控事件
def press(ball):
if not ball.next_disk:
return
for event in pygame.event.get():
if event.type == pygame.QUIT: # 点击关闭按钮退出
pygame.quit()
sys.exit()
if event.type == pygame.KEYDOWN:
if event.key == 32 and ball.can_jump:
if ball.current_disk.level < ball.next_disk.level:
step = 1
change_speed = True
else:
step = 2
change_speed = False
ball.change_speed(step, change_speed)
ball.move_width = (DISK_SIZE[0] + DISK_GAP_WIDTH) * ball.step
当按下空格键时,判断当前与下一柱子的高度,来定义下一次柱子移动的步长。
5. 移动柱子
def move_disk(ball):
if ball.move_disk: # 可以移动柱子
speed = ball.move_speed * ball.step # 柱子移动的速度
for disk in ball.disk_group: # 移动所有柱子
disk.rect.left -= speed if ball.move_width > speed else ball.move_width
if disk.rect.right < 0: # 删除超过屏幕的柱子
ball.disk_group.remove(disk)
ball.current_disk_index -= 1
ball.move_width -= speed # 柱子移动的宽度还剩多少
if ball.move_width < speed: # 柱子移动完成
ball.current_disk_index += ball.step
ball.step = 1
ball.move_disk = False
ball.move_width = (DISK_SIZE[0] + DISK_GAP_WIDTH) * ball.step
ball.set_min_height() # 设置基础高度
6.开始结束游戏
def start_or_end_game(screen, ball, win, clock):
while True:
if win == 1:
text = 'You Win'
elif win == 2:
text = 'Press Enter To Restart!'
else:
text = 'Press Space To start!'
font_size = 32
font = pygame.font.SysFont('arial', font_size)
font_width, font_height = font.size(text)
screen.blit(font.render(text, True, (255, 255, 255)),
((SCREEN_WIDTH - font_width) / 2,
(SCREEN_HEIGHT - font_height) / 2.5))
ball.draw(screen)
for event in pygame.event.get():
if event.type == pygame.QUIT: # 点击关闭按钮退出
pygame.quit()
sys.exit()
if event.type == pygame.KEYDOWN:
if event.key == 13 and win == 2:
return
if event.key == 32 and win == 0:
return
# 更新画布
pygame.display.update()
clock.tick(FPS)
7. 主函数
def main():
screen = init_game()
disk_group = init_disk_sptite(FIRST_DISK_POSITION)
ball = Ball(BALL_POSITION, disk_group)
clock = pygame.time.Clock()
win = 0
start_or_end_game(screen, ball, win, clock)
while True:
screen.fill((0, 0, 0))
move_disk(ball) # 移动柱子
press(ball) # 监控按键
win = ball.update() # 移动MC
if win is not None:
break
if ball.next_disk:
if pygame.sprite.spritecollide(ball, ball.disk_group,
False): # 判断是否撞柱子
win = 2
break
ball.draw(screen)
# 更新画布
pygame.display.update()
clock.tick(FPS)
start_or_end_game(screen, ball, win, clock)
if __name__ == "__main__":
while True:
main()
运行效果
欸,您且稍等,故事还没结束。之前写的几款游戏,我只分析了玩法,做了简易版的游戏,并没有加入游戏素材。而这款游戏,没有音乐和音效,感觉少点什么,没内味儿,所以我去找了一些图片和音效,加了进去之后,画风就变成了下面这样。
源码已上传至Github:打代码的shy:用python写游戏系列github.com
初来乍到,请多关照。