1. 游戏设计与数据设计
感谢您选择pyera,如果有任何问题想反馈和讨论可以加群:
py/era 技术吹水群 543014634
完整贪吃蛇代码参考:https://github.com/qsjl11/pyera_snake
贪吃蛇是一款家喻户晓的游戏,因为游戏系统非常简单,因此合适作为各种引擎的练习用,这里我们也用这个作为一个练习的起点。
在正式码代码之前,我们要做好设计工作。好的准备才能保证后期的顺利。
作为教程本游戏的设计比较粗浅,只要大家能够明白意思就好。
游戏设计:
1. 游戏本身采用n*n的正方形矩阵。n是可以自定义的
2. 每个矩阵包含3种状态,分别是苹果、蛇身子和空地。
3. 每一回合,蛇头会向指定方向挪动一格。后面的蛇身会跟随前一蛇身移动。
4. 苹果随机出现在空地上,直到被蛇吃掉。
5. 每吃掉一格苹果,蛇身会加长一格
6. 当蛇头前进方向受到边界或者蛇身阻碍的时候,游戏结束
7. 地图左上角为(0,0),向右为x轴,向下为y轴
8. 游戏可以保存和读取
数据设计:
1. 设置地图大小的变量map_size
2. 地图保存在dict中,dict的键由元胞(x,y)构成,值为整数【0,1】,分别代表空地,苹果
3. 蛇身体由一个list保存,每个元素是一个含有两个元素的list,表示x,y坐标
4. 一个list [x,y] 表示苹果的位置。
流程设计:
1. 开始游戏/读取游戏
2. 游戏循环
* 根据地图矩阵来绘制地图
* 绘制命令,【上下左右】【保存读取】
* 等待输入
* 移动蛇,更新地图信息
* 判断蛇能够吃到苹果,能吃到的话,长度+1
2. 配置开发环境
工欲善其事必先利其器。开发环境是非常重要的。这里不赘述基本的配置方法,只讨论涉及本游戏的配置。基本配置方法请参考文档【开发环境配置】,选择‘完全配置环境”进行配置。
文档 http://www.jianshu.com/p/79096b775d40
当基本配置完成之后,我们需要先删除一些多余的东西。因为源代码包中含有很多展示性的东西,这些东西对于我们来说是没有必要的。
这里要说明,data文件夹保存配置数据,script保存运行的游戏脚本,因此只有这两个文件中有内容需要删除。Pyera_engine和build.py文件请勿删除。
要保留的内容有由红线标出:
右键删除时如果弹出窗口,记得勾掉sage delete 和search in comments and strings. 否则可能不让删除那些文件。
清理过后的窗口如图
双击mainflow.py, 将里面的代码全部删除后改为并保存:
# -*- coding: UTF-8 -*-
import core.game as game
import script.base_lib as lib
def open_func():
pass
def main_func():
pass
此时右键运行pyera_engine/pyera.py尽管没有内容,但是还是可以打开运行窗口的。如图:
不妨在open_func函数中写下一句:
def open_func():
game.pl('pyera贪吃蛇游戏加载中')
再次运行后可以看到
你所写下的内容已经出现在了游戏窗口当中。
至此,配置环境的工作完成了。
3. 简单的数据配置
根据之前的游戏设计,需要有一个数据来保存map_size。本章中,则展现pyera的数据配置功能。
pyera中所有的预先设定的数据都在data文件夹中。
“core_cfg.json”包含系统的基本配置。
“core_event_sort.json"则与事件顺序系统有关,暂时不用管这个功能。
本章节中将建立一个名为“map”的json数据文件,里面保存一个“map_size"的变量。
新建map文件。右键点击data文件夹->new->file
输入:map.json“,点击确定。
双击打开map.json文件,并输入一下代码,注意大括号和双引号为英文半角字符:
{
"map_size": 10
}
Ctrl+s 保存后数据配置就算结束了,但是怎么调用呢。这里演示一下。打开mainflow.py, 将open_func函数改为:
def open_func():
game.pl('pyera贪吃蛇游戏加载中')
map_size=game.data['map']['map_size']
game.pl('地图大小设置为 '+str(map_size))
运行pyera.py,可以看到结果。如果更改map.json文件中的数值的话,游戏内的输出也会改变。
这里解释一下open_func中的各句意思
a. 第一句单纯的输出。game.pl() 函数输出一句话并自动换行.
b. 第二句为读取数据。game.data是一个dict,第一个键值为文件名,第二键值为保存的内容。
c. 第三句输出map_size, str()函数用来使map_size变为字符串,进而可以与其他字符串做+运算。
4. 开始写代码,“开始游戏”与”读取游戏“
本节将构建开始游戏界面,并利用现成的存储/读取游戏界面。
如前所述,mianflow.py/open_func()是程序的入口。现在我们将利用open_func()中添加一条分割线和两个按钮:“开始游戏”和“读取游戏”
添加分割线
在open_func()添加“game.pline()“即可,可以运行测试一下,会多出一条线。
添加读取游戏按钮
open_func()中接如下代码
game.pcmd('[001] 读取游戏', 1, lib.load_func, arg=(open_func,))
这里详细说一下pcmd命令的使用方法。pcmd会打印一个字符串。当点击这个字符串或者输入对应数字命令的时候,程序会跳转到对应命令指定的函数,并可以传递参数。这里,
第一个参数是显示的字符串;
第二参数是对应的苏子命令,这里是1;
第三个参数是跳转函数 为 lib.load_func,
第四个为传递的参数,为open_func。
Savelaod.load_func就是读取列表函数,该函数的参数表示当执行“退出”命令时执行哪个函数。这里,参数open_func的意思是如果不去读档直接退出的话,则执行open_func。
注意:这里open_func后面不加括号!
至此,mainflow文件应当如下:
# -*- coding: UTF-8 -*-
import core.game as game
import script.base_lib as lib
def open_func():
game.pl('pyera贪吃蛇游戏加载中')
map_size=game.data['map']['map_size']
game.pl('地图大小设置为 '+str(map_size))
game.pline()
game.pcmd('[001] 读取游戏', 1, lib.load_func, arg=(open_func,))
def main_func():
pass
运行后如图:
点击读取游戏或者在输入栏输入‘1’可以进入读取界面,退出则返回,大家可以自己尝试一下。
添加新的游戏按钮
尽管还有其他的输入命令的方式,pyera建议使用关联函数的办法绘制按钮。其他方式可以参考详细文档。
添加开始游戏按钮,需要如下几步:
a. 添加一个新函数,用来做处理开始游戏的各种变量设置。添加函数 def newgame_func()
b. 在open_func中追加输出命令,将开始游戏关联到 newgame_func()上
game.pcmd('[002] 开始游戏', 2, newgame_func)
相关部分代码文件如下:
def open_func():
game.pl('pyera贪吃蛇游戏加载中')
map_size=game.data['map']['map_size']
game.pl('地图大小设置为 '+str(map_size))
game.pline()
game.pcmd('[001] 读取游戏', 1, lib.load_func, arg=(open_func,))
game.pcmd('[002] 开始游戏', 2, newgame_func)
def newgame_func():
pass
版面修整。
运行后我们会发现,此时两个命令紧紧的挨在一起不好看。最好是分成两行。
因此在两个pcmd命令之间加入一句话 game.pl() 这样可以引入一个换行。完成后如图:
至此,开始界面就算搭建完成了。代码如下:
# -*- coding: UTF-8 -*-
import core.game as game
import script.base_lib as lib
def open_func():
game.pl('pyera贪吃蛇游戏加载中')
map_size=game.data['map']['map_size']
game.pl('地图大小设置为 '+str(map_size))
game.pline()
game.pcmd('[001] 读取游戏', 1, lib.load_func, arg=(open_func,))
game.pl()
game.pcmd('[002] 开始游戏', 2, newgame_func)
def newgame_func():
pass
def main_func():
pass
def open_menu():
pass
newgame_func的具体工作在下一节中讲解。
5. newgame_func与相关函数
在点击开始游戏后,会执行newgame_func。这个函数将用来做一些初始化工作, newgame_func执行后会执行main_func。这里先把整段函数写出来。再一一解释
def newgame_func():
# 初始化地图数据dic,0是空地,1是苹果
map_size = game.data['map']['map_size']
game.data['mapdata'] = {}
for y in range(0, map_size):
for x in range(0, map_size):
game.data['mapdata'][(x, y)] = 0
# 初始化蛇list, 三格长度,靠左下角放置
snake_list = []
snake_list.append((0, 2))
snake_list.append((0, 1))
snake_list.append((0, 0))
game.data['snake_list'] = snake_list
main_func()
def main_func():
# 添加一个苹果
create_apple()
#绘制地图
draw_map()
初始化地图数据
初始化过程的第一步就是获得map_size。然后建立一个空dict来存储地图数据。地图数据以键值方式存储。键是一个(x,y)的元胞,而值是‘0’空地或者‘1’苹果。不过初始化的过程中全部用0.
需要注意的是range函数。Range( 1,3)表示[1,2],而不是[1,2,3],因此这里的x和y的取值范围为0<=x,y<map_size.
初始化蛇list。
首先建立一个空list,然后用append函数来附加位置。分别是(0,2),(0,1),(0,0).这里list[0]元素为蛇头。
进入main_func()
Main_func()是游戏的主体函数,不过这里只完成制作苹果和显示两个步骤,以示效果。
添加苹果
添加苹果是一个单独的函数create_apple(), 函数如下:
def create_apple():
map_size = game.data['map']['map_size']
# 随机苹果的位置,当位置与蛇重合的时候,重新随机
import random
apple_position = (random.randint(0, map_size - 1), random.randint(0, map_size - 1))
while apple_position in game.data['snake_list']:
apple_position = (random.randint(0, map_size - 1), random.randint(0, map_size - 1))
# 将mapdata中的对应苹果位置改为1
game.data['mapdata'][apple_position] = 1
首先依然是获得map_size, 然后使用random模块中的randint()函数获得一个随机整数。这里需要注意的是,因为randint(i,j)的到整数为 i<=value<=j,因此其参数应当为(0,map_size-1)。否则随机的值可能正好为map_size,但实际坐标中最大值为map_size-1,会照成溢出,引起程序错误。
while循环的目的是,当苹果随机的位置恰好落于蛇身体上的时候,则重新随机,直到苹果不产生再蛇身上为止。这样最后apple_position 就是一个有效的位置。
最后将mapdata中对应位置的值改为1,表示该位置是苹果。
绘制地图draw_map(), 其内容如下:
def draw_map():
map_size = game.data['map']['map_size']
snake_list=game.data['snake_list']
mapdata=game.data['mapdata']
for y in range(0, map_size):
for x in range(0, map_size):
pos=(x,y)
if pos in snake_list:
game.p('❖', style='special')
continue
if mapdata[pos]==1:
game.p('❁')
continue
if mapdata[pos]==0:
game.p('﹒')
continue
game.pl()
为了方便调用,map_size, snake_list 和mapdata都用单独的变量引用。通过双重循环,判断如果是蛇身部分,则绘制'❖', 苹果绘制('❁'), 空地用'﹒'
这里有几点需要注意:
循环的时候是外循环为y,内循环为x,目的是按照(0,0)->(1,0)->(2,0)->…->(0,1)->(1,1)->…的方式进行循环。因此绘制的时候是一行一行输出的,只能先变x后变y
continue,因为每个位置只用绘制一种元素,因此绘制之后直接用continue跳过后面的循环部分。
每次内循环完成的时候,都调用game.pl()另起一行,开始下一行打印。
game.p('❖', style='special')中的style是可以自行定义的,具体可以参考详细文档。
目前完整的mainflow.py 如下:
# -*- coding: UTF-8 -*-
import core.game as game
import script.base_lib as lib
def open_func():
game.pl('pyera贪吃蛇游戏加载中')
map_size = game.data['map']['map_size']
game.pl('地图大小设置为 ' + str(map_size))
game.pline()
game.pcmd('[001] 读取游戏', 1, lib.load_func, arg=(open_func,))
game.pl()
game.pcmd('[002] 开始游戏', 2, newgame_func)
def newgame_func():
# 初始化地图数据dic,0是空地,1是苹果
map_size = game.data['map']['map_size']
game.data['mapdata'] = {}
for y in range(0, map_size):
for x in range(0, map_size):
game.data['mapdata'][(x, y)] = 0
# 初始化蛇list, 三格长度,靠左下角放置
snake_list = []
snake_list.append((0, 2))
snake_list.append((0, 1))
snake_list.append((0, 0))
game.data['snake_list'] = snake_list
main_func()
def create_apple():
map_size = game.data['map']['map_size']
# 随机苹果的位置,当位置与蛇重合的时候,重新随机
import random
apple_position = (random.randint(0, map_size - 1), random.randint(0, map_size - 1))
while apple_position in game.data['snake_list']:
apple_position = (random.randint(0, map_size - 1), random.randint(0, map_size - 1))
# 将mapdata中的对应苹果位置改为1
game.data['mapdata'][apple_position] = 1
def draw_map():
map_size = game.data['map']['map_size']
snake_list=game.data['snake_list']
mapdata=game.data['mapdata']
for y in range(0, map_size):
for x in range(0, map_size):
pos=(x,y)
if pos in snake_list:
game.p('❖', style='special')
continue
if mapdata[pos]==1:
game.p('❁')
continue
if mapdata[pos]==0:
game.p('﹒')
continue
game.pl()
def main_func():
# 添加一个苹果
create_apple()
# 绘制地图
draw_map()
当运行并点击开始游戏的时候,应该有如下结果:
其中黄色表示蛇,大白点是苹果,小白点是空地。
下一节将讲述main_func()的构建,来实现完整显示与菜单。
6. Main_func的编写 移动与菜单
本节中将完善main_func, 并创建菜单,使蛇可以真正的移动。同时演示保存和读取功能。注意,本章中将使用python中的闭包特性,如果感到难以理解的话,可以多多百度也可加群讨论。
一个关于闭包的参考资料:http://blog.csdn.net/ChangerJJLee/article/details/52598629
本节中修改的函数为main_func,并添加一个函数next_step()和一个全局变量direction。
Next_step()的功能是生成苹果并更新蛇的位置,direction指示蛇前进的方向。
direction = 'xia'
def next_step():
# 方便调用
global direction
snake_list = game.data['snake_list']
mapdata = game.data['mapdata']
# 判断是否有苹果,没有就创造一个
if 1 not in mapdata.values():
create_apple()
# 更新蛇身
for i in range(len(snake_list) - 1, 0, -1):
snake_list[i] = snake_list[i - 1]
# 更新蛇头位置
x_head = snake_list[0][0]
y_head = snake_list[0][1]
if direction == 'zuo':
x_head = x_head - 1
if direction == 'you':
x_head = x_head + 1
if direction == 'shang':
y_head = y_head - 1
if direction == 'xia':
y_head = y_head + 1
snake_list[0] = (x_head, y_head)
# 返回主函数
main_func()
- 全局变量direction用'shang',‘xia’,‘zuo’,‘you’表示前进方向,初始值为‘xia’。
- Next_step()函数中第一步依然是准备变量。
- 第二步是判断mapdata中是否右苹果,如果没有就创造一个苹果。这里mapdata.values()是返回dict中所有的值
- 第三部更新蛇的身体,这里身体不包含头部(snake_list[0]). 更新的思路很简单蛇的每一块身体都位置都为前一块身体的位置。比如 (2,0)(1,0)(0,0)->(,)(2,0)(1,0)
- 第四步是更新蛇头的位置。首先取出蛇头位置,然后根据direction的不同使x/y加减1,最后再赋值给蛇头snake_list[0]
- 最后返回main_func()
目前next_step()函数还没有全部完成,剩下的部分将在下一节中完成。
下面将写main_func()有什么变化,并对其中较为关键的语句进行解释:
def main_func():
# 新界面准备
game.clr_cmd()
game.pline()
# 画图
draw_map()
def create_func(direction_name):
def func():
global direction
direction = direction_name
next_step()
return func
# 状态显示
game.pline('--')
game.pl('分数:')
# 绘制命令按钮
game.pline('--')
game.pcmd('[1] 左 ', 1, create_func('zuo'))
game.pcmd('[2] 上 ', 2, create_func('shang'))
game.pcmd('[3] 右 ', 3, create_func('you'))
game.pl()
game.p(' ')
game.pcmd('[4] 下 ', 4, create_func('xia'))
game.pl('\n')
game.pcmd('[5] 存储游戏 ', 5, lib.save_func, arg=main_func)
game.pcmd('[6] 读取游戏 ', 6, lib.load_func, arg=(main_func, main_func))
- game.clr_cmd() 的作用是清除之前所有的命令按钮,以防误操作。通常建立新的界面的时候使用
- draw_map() 根据地图数据和蛇身数据绘制地图
- Create_func()是一个较难理解点,这个函数的作用是制作另一个函数,即期内部的函数func().func()的内容有一下几点,声明使用全局变量direction,设置direction为dirction_name, 最后调用next_setp(). 当给定不同的direction_name的时候,产生的func()会有所区别。之所以这么写的目的是简化程序。实际上,python提供的各种功能能够有效减少我们的工作量,是我们更加集中于游戏本身的逻辑上。
- 状态显示,这里预留分数的记录部分。下一节再扩展。需要注意的是game.pline('--')这个函数。当该函数没有参数的时候会绘制一条默认的粗直线,但是也可以给一个参数,这样会根据所给的字符绘制分割线。需要注意的是这个位置应当是一个双字符快读的内容,即一个全角字符或两个半角字符。
- 绘制命令(上下左右):这里create_func()就显出其作用了,create_func 会给出一个新的函数。对于‘左‘按钮,func()会使direction为’zuo‘。以此类推。
- Game.pl('\n')的目的是换两行。因为game.pl()函数只会在当前行不为空的时候另起一行。因此只能换行一次。要换两行的话,需要再输出一个换行符, 即game.pl('\n')
- 存储游戏按钮。参数依然是点击‘退出’按钮的时候,会返回哪个函数。这里说明一下存储读取的原理。木人状态下,存储和读取实际上就是对game.data的存储和读取。因此任何存放在game.data中的数据都会被存储和读取。
- 读取游戏按钮。和之前一样,有所区别的是第一参数为main_func,意思是点退出按钮的时候会退出到main_func
运行一下如下:
蛇已经可以根据上下左右移动了。
后续工作:尽管可以移动,并且存储和读取功能都已完成。但是目前蛇不会和苹果交互也不会碰到墙壁死亡。下一节中我们将对next_step 更改以实现更多的功能
7. 扩展next_step(), 吃苹果与死亡
本节将扩展next_step的功能,首先我们扩展吃苹果的功能,然后扩展碰撞死亡的功能。
def next_step():
# 方便调用
global direction
snake_list = game.data['snake_list']
mapdata = game.data['mapdata']
# 判断是否有苹果,没有就创造一个
if 1 not in mapdata.values():
create_apple()
# 新添加**************************
# 记录蛇身体的最后一节的位置
last_snake_position = snake_list[-1]
# 更新蛇身
for i in range(len(snake_list) - 1, 0, -1):
snake_list[i] = snake_list[i - 1]
# 更新蛇头位置
x_head = snake_list[0][0]
y_head = snake_list[0][1]
if direction == 'zuo':
x_head = x_head - 1
if direction == 'you':
x_head = x_head + 1
if direction == 'shang':
y_head = y_head - 1
if direction == 'xia':
y_head = y_head + 1
snake_list[0] = (x_head, y_head)
# 新添加**************************
apple_position=(-1,-1)
# 获得苹果位置
for k,v in mapdata.items():
if v==1:
apple_position=k
break
# 判断是否吃到苹果,如果吃到苹果则加一节身体
if snake_list[0] == apple_position:
# 苹果消失,所有地格变为0
for k in mapdata:
mapdata[k]=0
#延长蛇身,把动前最后的一个位置添加到蛇身体中
snake_list.append(last_snake_position)
# 返回主函数
main_func()
- 新添加代码有两部分。首先是记录蛇身体最后一节的位置。目的是一旦吃苹果需要添加蛇身,则直接添加在这个位置上。因此需要提前记录一下这个位置。
- 获得苹果位置。首先定义一个变量apple_position默认值为(-1,-1).因为该位置超出地图显示范围,所以是安全的默认值。然后通过for循环,当值为1的时候,将apple_position等于键k。之所以直接break是因为我们应当只有一个苹果,因此无需多余的循环动作。
- 判断是否吃到苹果很简单,就是蛇头的位置是否与苹果位置相等。
- 如果相等的话,则先让苹果消失,即把所有地图数据都值为0.
- 然后延长折身体,用append函数将last_snake_position再附加到snake_list中去。
此时运行,可以发现当蛇头吃的到苹果的时候,蛇身体延长。
不过会发现一个问题,当吃完苹果后只有下一回合会才回产生新苹果。因为不影响游戏进行,所以解决方法就不多说了,大家可以自己开动脑筋解决这个问题。
然后是第二部,碰撞死亡检测,所有的检测都发生在最后,因此next_step 函数应当追加以下内容:
# 身体碰撞检测
if snake_list[0] in snake_list[1::]:
game.pl('蛇碰到了它的身体,你失败了', style='special')
open_func()
return
# 超出边界检测
if snake_list[0][0]<0 or snake_list[0][0]>=map_size or snake_list[0][1]<0 or snake_list[0][1]>=map_size:
game.pl('蛇碰到了墙壁,你失败了', style='special')
open_func()
return
# 返回主函数
main_func()
- 身体碰撞检测,这里注意是snake_list[1::]表示从snake_list[1]到最后所有的元素。这句话的意思,蛇头是否碰到身体的其他部位。
- 超出边界检测,即检测蛇头的x和y是否小于0或者大于等于map_size. 如前几节所说地图的范围是0<=x,y<=map_size-1
- 当检测为真后。做了两间事,第一件事是打印失败信息。然后转到open_func()转到新的一句,最后是return退出next_step()函数。如果不加return的话会导致执行玩open_func()函数又会继续执行main_func()函数。
运行后死亡情况如图:
自此,所有的贪吃蛇开发已经完成了。但是依然有很多可以做的事和可以优化的地方。这里列出几项:
1. 一个苹果被吃掉,立刻产生新的苹果。
2. 蛇头采用不同的图形绘制
3. 添加内部墙壁
4. 即时化
5. 分数系统
总之,这个只是pyera的入门教程,还有很多功能没有介绍,依然列出几项比较有价值的:
1. 事件系统
2. 设定循环主界面
3. 非绑定的输入处理部分,类似于era脚本中的$input部分
4. align排版函数
5. 文字输入功能等。
6. 构建exe运行文件的功能
这些功能可能在以后扩展贪吃蛇游戏的时候慢慢介绍到。大家也可以参考详细文档的部分来了解使用方法。
最后感谢您选择pyera,如果有任何问题想反馈和讨论可以加群:
py/era 技术吹水群 543014634
完整贪吃蛇代码参考:https://github.com/qsjl11/pyera_snake