学习python?说得通。它拥有大量稳定的机器学习和数据操作库。在本文中,将您的Python研究带到下一个层次。

我的同事和我决定一起做一个小游戏来帮助我们学习语言。这篇文章介绍了我们最初的经验,把一切都放在一起。


这个计划

就像我学过的其他语言一样,我通常喜欢开发一个应用程序,它涉及到一些功能,比如读取文件、网络、用户输入和可视化。这迫使我熟悉了库和语言的功能,这让我加快了重新实现算法和完成教程项目的速度。它还迫使我了解Python的环境,以安装依赖关系和创建发行版。

我们查阅了一些与游戏创建和网络相关的库,并决定使用pygame,因为它似乎提供了一种功能,可以从开发中删除许多单调乏味的内容。它看起来也像Python里有一系列的网络库,所以我们决定在使用它的时候把它弄清楚。

安装Python

Python本身相对容易安装。我们刚刚从网站上下载了自动安装程序,并在一分钟内将运行时准备好。

安装Pygame

事实证明,安装Pygame有点令人沮丧。在我们设法下载脚本并以正确的方式安装它之前,我们尝试了好几次。我们必须找到这个库的正确版本(它与我们安装的Python版本相匹配)在一个不容易找到的依赖项列表上,然后用Python包安装实用程序pip3.exe来提取它。这看起来比实际要困难得多,特别是由于库的不同版本的数量,以及如果我们安装了不同版本的Python,我们需要做些什么。

在这里还是要推荐下我自己建的Python开发学习群:483546416,群里都是学Python开发的,如果你正在学习Python ,小编欢迎你加入,大家都是软件开发党,不定期分享干货(只有Python软件开发相关的),包括我自己整理的一份2018最新的Python进阶资料和高级开发教程,欢迎进阶中和进想深入Python的小伙伴

最终,我们建立了一些东西,并寻找了一个关于如何获得游戏基础的教程。

Drawing a Sprite

在开始使用任何图形的时候,首先要做的事情就是将某些东西(或任何东西)呈现在屏幕上。我们发现了一大堆关于这方面的复杂的教程,并基于他们的例子提出了一个基本的渲染循环:

import pygame, sys
from pygame.locals import *
WIDTH = 400
HEIGHT = 400
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption('Hello World!')
clock = pygame.time.Clock()
thing = pygame.image.load('images/TrashPanda/TrashPanda_front.png')
x = 0
y = 0
whileTrue:
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
clock.tick(30)
screen.fill((0,0,0))
screen.blit(thing, (x, y))
pygame.display.flip()

这段代码产生:


在那之后,我们将重点放在捕获用户输入以移动字符。我们还为玩家角色创建了一个类来将其逻辑内部化:

classMinion:
def__init__(self, x, y):
self.x = x
self.y = y
self.vx = 0
self.vy = 0
defupdate(self):
self.x += self.vx
self.y += self.vy
#this keeps the player character within the bounds of the screen
ifself.x > WIDTH - 50:
self.x = WIDTH - 50
ifself.x < 0:
self.x = 0
ifself.y > HEIGHT - 50:
self.y = HEIGHT - 50
ifself.y < 0:
self.y = 0
defrender(self):
screen.blit(thing, (self.x, self.y))

用户输入在游戏循环中被捕获:

for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
if event.type == KEYDOWN:
if event.key == K_LEFT: cc.vx = -10
if event.key == K_RIGHT: cc.vx = 10
if event.key == K_UP: cc.vy = -10
if event.key == K_DOWN: cc.vy = 10
if event.type == KEYUP:
if event.key == K_LEFT and cc.vx == -10: cc.vx = 0
if event.key == K_RIGHT and cc.vx == 10: cc.vx = 0
if event.key == K_UP and cc.vy == -10: cc.vy = 0
if event.key == K_DOWN and cc.vy == 10: cc.vy = 0

角色的位置被更新和渲染(同样在游戏中):

cc.update()

cc.render()

现在我们有了基本的字符移动工作,我们想开始构建一些简单的多人游戏功能。

我们决定采用一个非常简单的数据传输模型:

· 客户端连接到服务器,然后不断广播自己的字符的位置

服务器将所有字符的位置广播给所有的客户

我们决定使用TCP套接字,因为它们处理诸如连接和断开连接比UDP更容易。另外,这并不是一个性能关键的应用程序。

我们成功地找到了一篇关于在Python中使用Python编写异步服务器的好文章。

基本的服务器代码是这样开始的:

import socket
import asyncore
import random
import pickle
import time
BUFFERSIZE = 512
outgoing = []
#additional logic here...
classMainServer(asyncore.dispatcher):
def__init__(self, port):
asyncore.dispatcher.__init__(self)
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
self.bind(('', port))
self.listen(10)
defhandle_accept(self):
conn, addr = self.accept()
print ('Connection address:' + addr[0] + " " + str(addr[1]))
outgoing.append(conn)
playerid = random.randint(1000, 1000000)
playerminion = Minion(playerid)
minionmap[playerid] = playerminion
conn.send(pickle.dumps(['id update', playerid]))
SecondaryServer(conn)
classSecondaryServer(asyncore.dispatcher_with_send):
defhandle_read(self):
recievedData = self.recv(BUFFERSIZE)
if recievedData:
updateWorld(recievedData)
else: self.close()
MainServer(4321)
asyncore.loop()

这定义了一个负责接受新的TCP连接的主服务器,然后它创建一个二级服务器。辅助服务器处理来自每个客户机的所有传入数据。接收到传入的数据包时,将数据传递给updateWorld。这是定义如下:

classMinion:
def__init__(self, ownerid):
self.x = 50
self.y = 50
self.ownerid = ownerid
minionmap = {}
defupdateWorld(message):
arr = pickle.loads(message)
playerid = arr[1]
x = arr[2]
y = arr[3]
if playerid == 0: return
minionmap[playerid].x = x
minionmap[playerid].y = y
remove = []
for i in outgoing:
update = ['player locations']
for key, value in minionmap.items():
update.append([value.ownerid, value.x, value.y])
try:
i.send(pickle.dumps(update))
except Exception:
remove.append(i)
continue
for r in remove:
outgoing.remove(r)

updateWorld只是负责更新包含每个玩家角色位置的字典。然后,它通过将它们的位置序列化为数组的数组,向每个播放器广播这些位置。

现在客户端已经构建好了,我们可以实现客户端发送和接收更新的逻辑。当游戏开始时,我们添加了一些逻辑来启动一个简单的套接字并连接到一个服务器地址。这可选地获取命令行指定的IP地址,但其他方式连接到本地主机:

serverAddr = '127.0.0.1'
iflen(sys.argv) == 2:
serverAddr = sys.argv[1]
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((serverAddr, 4321))

然后,我们在游戏循环的开始部分添加了一些逻辑,以便从套接字读取。我们使用“select”包只在有数据的情况下从套接字读取传入的包。如果我们使用了插座。如果套接字没有读取的包,则游戏程序将停止。使用“select”允许gameloop继续执行,即使没有什么可读的:

ins, outs, ex = select.select([s], [], [], 0)
for inm in ins:
gameEvent = pickle.loads(inm.recv(BUFFERSIZE))
if gameEvent[0] == 'id update':
playerid = gameEvent[1]
print(playerid)
if gameEvent[0] == 'player locations':
gameEvent.pop(0)
minions = []
for minion in gameEvent:
if minion[0] != playerid:
minions.append(Minion(minion[1], minion[2], minion[0]))

上面的代码处理了服务器可能生成的两个序列化的有效负载。

1. 包含玩家服务器分配标识符的初始包。

客户端使用此方法在所有位置更新中标识自己到服务器。它还用于忽略服务器广播的自己的播放器数据,因此没有一个带有阴影版本的玩家角色。

2. 球员的位置有效载荷

这包含一组包含玩家标识符和字符位置的数组。当检索到现有的Minion对象时,将为每个传输的对象创建新的Minion对象。

其他的小黄人则在游戏循环中呈现:

for m in minions:
m.render()

我们要做的最后一件事是向客户端添加一些代码,告诉服务器玩家的位置。这是通过在gameloop的结尾添加一个广播来序列化当前播放器的位置,然后使用“pickle”,然后将这个bytestream发送到服务器:

ge = ['position update', playerid, cc.x, cc.y]
s.send(pickle.dumps(ge))

一旦这是完整的玩家连接到同一个服务器可以看到其他玩家移动。

一些额外的更新,例如显示基于playerid的不同的化身被实现。

当完成时,当前的迭代有两个参与者是这样的: