Python 已经赢得了作为一种优秀的初学者编程语言的声誉。但是,应该从哪里开始呢?
我让人们对编程产生兴趣最喜欢的方式之一是编写游戏。
PursuedPyBear (ppb) 是一个为教学优化的游戏编程库,我最近使用它来教我的孩子们更多关于 我最喜欢的编程语言的知识。
Jupyter 项目是一个基于浏览器的 Python 控制台,最初是为数据科学家玩转数据而设计的。
我有一个 Jupyter Notebook,旨在教您如何制作一个简单的互动游戏,您可以从这里下载。为了打开该文件,您需要安装最新的 Jupyter 项目 JupyterLab。
先决条件
我们将简要配置一个虚拟环境,为所需的库创建一个单独的空间。(您可以在此处了解有关虚拟环境如何工作的更多信息。)
$ git clone https://github.com/moshez/penguin-bit-by-bit.git
$ cd penguin-bit-by-bit
$ python -m venv venv
$ source ./venv/bin/activate
$ pip install -r requirements.txt
$ jupyter lab .
最后一个命令应该在您的默认浏览器中打开 JupyterLab,地址为 https://127.0.0.1:8888/lab。在左侧列中选择 dynamic_penguin.ipynb 文件,我们就可以开始了!

将运行游戏的事件循环
Jupyter 在内部运行一个事件循环,这是一个管理进一步异步操作运行的过程。Jupyter 中使用的事件循环是 asyncio,而 PursuedPyBear 运行自己的事件循环。
我们可以使用另一个库 Twisted 将两者整合在一起,就像胶水一样。这听起来很复杂,但幸运的是,复杂性隐藏在库的背后,它们将为我们完成所有繁重的工作。
Jupyter 中的以下单元格负责前半部分——将 Twisted 与 asyncio 事件循环集成。
需要__file__ = None
来将 PursuedPyBear 与 Jupyter 集成。
from twisted.internet import asyncioreactor
asyncioreactor.install()
__file__ = None
接下来,我们需要一个“setup”函数。“setup”函数是配置关键游戏元素的常用术语。但是,我们的函数只会将游戏“场景”放在全局变量中。可以将其视为我们定义将要在其上玩游戏的桌子。
Jupyter Notebook 中的以下单元格将完成这项工作。
def setup(scene):
global SCENE
SCENE = scene
现在我们需要将 PursuedPyBear 的事件循环与 Twisted 集成。我们为此使用 txppb
模块
import txppb
d = txppb.run(setup)
d.addBoth(print)
末尾的 print
在我们因错误导致游戏崩溃时会有所帮助——它会将回溯打印到 Jupyter 输出。
这将显示一个空白窗口,准备好放置游戏元素。

这就是我们开始利用 Jupyter 的地方——传统上,整个游戏需要在我们开始玩之前编写完成。但是,我们打破常规,立即开始玩游戏!
通过互动让游戏变得有趣
但这并不是一个非常有趣的游戏。它什么都没有,只是坐在那里。如果我们想要一些东西,我们最好添加它。
在视频游戏编程中,屏幕上移动的东西称为“精灵”。在 PursuedPyBear 中,精灵由类表示。精灵将自动使用与类同名的图像。我从 Kenney 获取了一个小企鹅图像,Kenney 是一个免费和开源视频游戏资源集合。
import ppb
class Penguin(ppb.Sprite):
pass
现在让我们把企鹅放在正中间。
SCENE.add(Penguin(pos=(0,0)))

它小心地坐在中间。这比什么都没有稍微有趣一点。这很好——这正是我们想要的。在增量游戏开发中,每一步都应该只稍微有趣一点。
使用 ppb 为我们的企鹅游戏添加移动
但是企鹅不应该静止不动!企鹅应该四处移动。我们将让玩家使用箭头键控制企鹅。首先,让我们将键映射到向量
from ppb import keycodes
DIRECTIONS = {keycodes.Left: ppb.Vector(-1,0), keycodes.Right: ppb.Vector(1,0),
keycodes.Up: ppb.Vector(0, 1), keycodes.Down: ppb.Vector(0, -1)}
现在我们将使用一个实用程序库。set_in_class
函数在类中设置方法。Python 追溯性地向类添加函数的能力真的派上用场了!
from mzutil import set_in_class
Penguin.direction = ppb.Vector(0, 0)
@set_in_class(Penguin)
def on_update(self, update_event, signal):
self.position += update_event.time_delta * self.direction
set_in_class
的代码不长,但它确实使用了一些非平凡的 Python 技巧。我们将把完整的实用程序库放在文章末尾以供回顾,为了流程的顺利,我们现在将跳过它。
回到企鹅!
哦,嗯,好吧。
企鹅正在勤奋地移动……以零速度,精确地说是哪里也没去。让我们手动设置方向,看看会发生什么。
Penguin.direction = DIRECTIONS[keycodes.Up]/4

方向是向上,但有点慢。这给了足够的时间手动将企鹅的方向设置回零。我们现在就这样做!
Penguin.direction = ppb.Vector(0, 0)
为我们的企鹅游戏添加互动性
哎,这很刺激——但不是我们想要的。我们希望企鹅对按键做出反应。从代码中控制它被游戏玩家称为“作弊”。
让我们将其设置为在按下按键时设置方向,并在释放按键时返回零。
@set_in_class(Penguin)
def on_key_pressed(self, key_event, signal):
self.direction = DIRECTIONS.get(key_event.key, ppb.Vector(0, 0))
@set_in_class(Penguin)
def on_key_released(self, key_event, signal):
if key_event.key in DIRECTIONS:
self.direction = ppb.Vector(0, 0)

企鹅有点无聊,不是吗?也许我们应该给它一个橙色的球玩。
class OrangeBall(ppb.Sprite):
pass
再说一遍,我确保有一个名为 orangeball.png
的图像。现在让我们把球放在屏幕的左侧。
SCENE.add(OrangeBall(pos=(-4, 0)))

尽管企鹅可能会尝试,但它无法踢球。当球靠近时,让我们让球远离企鹅。
首先,让我们定义“踢”球的含义。踢球意味着确定球在一秒钟后的位置,然后将其状态设置为“移动”。
首先,我们将通过让第一次更新将其移动到目标位置来移动它。
OrangeBall.is_moving = False
@set_in_class(OrangeBall)
def kick(self, direction):
self.target_position = self.position + direction
self.original_position = self.position
self.time_passed = 0
self.is_moving = True
@set_in_class(OrangeBall)
def on_update(self, update_event, signal):
if self.is_moving:
self.position = self.target_position
self.is_moving = False
现在,让我们踢它!
ball, = SCENE.get(kind=OrangeBall)
ball.kick(ppb.Vector(1, 1))
但这只是传送球;它立即改变了位置。在现实生活中,球会在中间点之间移动。当它移动时,它将在当前位置和需要到达的位置之间进行插值。
天真地,我们将使用 线性插值。但是一个很酷的视频游戏技巧是使用“缓动”函数。在这里,我们使用常见的“平滑步”。
from mzutil import smooth_step
@set_in_class(OrangeBall)
def maybe_move(self, update_event, signal):
if not self.is_moving:
return False
self.time_passed += update_event.time_delta
if self.time_passed >= 1:
self.position = self.target_position
self.is_moving = False
return False
t = smooth_step(self.time_passed)
self.position = (1-t) * self.original_position + t * self.target_position
return True
OrangeBall.on_update = OrangeBall.maybe_move
现在,让我们再次尝试踢它。
ball, = SCENE.get(kind=OrangeBall)
ball.kick(ppb.Vector(1, -1))

但实际上,应该是企鹅踢球。当球看到它与企鹅碰撞时,它会朝相反的方向踢自己。如果企鹅已经正对着它,球将选择一个随机方向。
现在更新函数调用 maybe_move
,并且只有在我们现在没有移动时才会检查碰撞。
from mzutil import collide
import random
OrangeBall.x_offset = OrangeBall.y_offset = 0.25
@set_in_class(OrangeBall)
def on_update(self, update_event,signal):
if self.maybe_move(update_event, signal):
return
penguin, = update_event.scene.get(kind=Penguin)
if not collide(penguin, self):
return
try:
direction = (self.position - penguin.position).normalize()
except ZeroDivisionError:
direction = ppb.Vector(random.uniform(-1, 1), random.uniform(-1, 1)).normalize()
self.kick(direction)

但是只是踢球玩并没有那么有趣。让我们添加一个目标。
class Target(ppb.Sprite):
pass
让我们将目标放在屏幕的右侧。
SCENE.add(Target(pos=(4, 0)))

奖励我们的企鹅
现在,我们希望在企鹅将球踢入目标时奖励它。来条鱼怎么样?
class Fish(ppb.Sprite):
pass
当目标得到球时,它应该移除球并在屏幕的另一端创建一个新球。然后,它会使一条鱼出现。
@set_in_class(Target)
def on_update(self, update_event, signal):
for ball in update_event.scene.get(kind=OrangeBall):
if not collide(ball, self):
continue
update_event.scene.remove(ball)
update_event.scene.add(OrangeBall(pos=(-4, random.uniform(-3, 3))))
update_event.scene.add(Fish(pos=(random.uniform(-4, -3),
random.uniform(-3, 3))))

我们希望企鹅吃鱼。当鱼看到企鹅时,它应该消失。
Fish.x_offset = 0.05
Fish.y_offset = 0.2
@set_in_class(Fish)
def on_update(self, update_event,signal):
penguin, = update_event.scene.get(kind=Penguin)
if collide(penguin, self):
update_event.scene.remove(self)
它奏效了!
迭代游戏设计对于企鹅和人来说都很有趣!
这具有游戏的所有要素:玩家控制的企鹅将球踢入目标,得到一条鱼,吃掉鱼,然后踢出一个新球。这可以作为游戏“刷级”的一部分,或者我们可以添加障碍物来让企鹅的生活更艰难。
无论您是经验丰富的程序员,还是刚刚入门,编程视频游戏都很有趣。PursuedPyBear 与 Jupyter 结合了经典 2D 游戏的所有乐趣以及经典环境(如 Logo 和 Smalltalk)的互动编程功能。是时候享受一点 80 年代的复古风了!
附录
这是我们的实用程序库的完整源代码。它提供了一些有趣的概念来使游戏板工作。有关其工作原理的更多信息,请阅读有关 碰撞检测、setattr 和 __name__ 属性。
def set_in_class(klass):
def retval(func):
setattr(klass, func.__name__, func)
return func
return retval
def smooth_step(t):
return t * t * (3 - 2 * t)
_WHICH_OFFSET = dict(
top='y_offset',
bottom='y_offset',
left='x_offset',
right='x_offset'
)
_WHICH_SIGN = dict(top=1, bottom=-1, left=-1, right=1)
def _effective_side(sprite, direction):
return (getattr(sprite, direction) -
_WHICH_SIGN[direction] *
getattr(sprite, _WHICH_OFFSET[direction], 0))
def _extreme_side(sprite1, sprite2, direction):
sign = -_WHICH_SIGN[direction]
return sign * max(sign * _effective_side(sprite1, direction),
sign * _effective_side(sprite2, direction))
def collide(sprite1, sprite2):
return (_extreme_side(sprite1, sprite2, 'bottom') <
_extreme_side(sprite1, sprite2, 'top')
and
_extreme_side(sprite1, sprite2, 'left') <
_extreme_side(sprite1, sprite2, 'right'))
11 条评论