烦恼一般都是想太多了。

0%

Evennia建立动态地图和静态地图

在 Evennia 中,实际上地图这个概念,表示的是各个房间之间的关联关系。而我们用一定的图形将其表达出来。反过来,我们定义了一个地图(就是各个房间之间的布局和关系),那么我们就可以将其在游戏中建立起来。

静态地图

这里介绍在游戏内建立一个基于预绘制出来的地图在游戏内中的显示。也介绍了如何使用 https://github.com/evennia/evennia/wiki/Batch-code-processor 来进行高级的构建世界。在下一节什么会介绍 动态地图 的建立,其是基于已经存在的房间来建立一个动态的地图。

Evennia 并不要求其内的 房间 被 “逻辑的” 进行位置设定。我们的 出口 可以被任意命名。我们可以将一个名叫 west 的出口引导向其实是一个位于很远的北方的一个房间。我们可以在房间内放置其他房间,而这样的房间在空间集合上是无法描述坐标的。

不过,多数游戏确实是将房间进行逻辑组合的。这样做的话,游戏就可以用地图的形式来展示了。本节的目的就在于此。

我们将本文章规划为几节来进行讲述:

  1. 规划地图——在这里,我们将提供一个小的示例地图,供本教程的其余部分使用。
  2. 建立一个地图对象——这将展示如何制作角色可以拾取并观看的静态游戏中“地图”对象。
  3. 构建地图区域——在这里,我们实际上将根据我们之前设计的地图创建一个小的示例区域。
  4. 地图代码——这会将地图链接到该位置,因此我们的输出如下所示:
crossroads(#3)
↑╚∞╝↑
≈↑│↑∩ The merger of two roads. To the north looms a mighty castle.
O─O─O To the south, the glow of a campfire can be seen. To the east lie
≈↑│↑∩ the vast mountains and to the west is heard the waves of the sea.
↑▲O▲↑

Exits: north(#8), east(#9), south(#10), west(#11)

我们会建设自己的游戏目录是mygame ,并且我们没有修改默认的命令。

地图规划

MUD 中的地图有不同的形状和大小。有些看起来只是用线连接起来的盒子。而某些呢是复杂的图形。

我们的地图就用简单的文字来进行显示,但并不意味着我们就只能使用字母或者数字。根据所选的字体,我们能用的字符可是非常多的。而且我们使用 utf-8 编码。

我们的地图看起来是这样的:

≈≈↑↑↑↑↑∩∩
≈≈↑╔═╗↑∩∩ account 能访问的地方用 "o" 来标记。
≈≈↑║O║↑∩∩ 最上方是一个可被访问的承包。
≈≈↑╚∞╝↑∩∩ 右边是一排屋舍,左边和沙滩。
≈≈≈↑│↑∩∩∩ 最下方是一个有帐篷的营地。
≈≈O─O─O⌂∩ 中间就是开始的地方。一个十字路口连接这几个区域
≈≈≈↑│↑∩∩∩
≈≈↑▲O▲↑∩∩
≈≈↑↑▲↑↑∩∩
≈≈↑↑↑↑↑∩∩

制作地图的时候,根据我们想要实现的东西有很多需要考虑的东西。在这里我们就显示一个我们周围 5*5 的地方。这就确认在每个能能访问的地方都会计算周围的2个字符。

建立地图对象

我们建立一个地图对象,然后 accout 可以捡起来和丢弃,事实上我们只是利用来其描述来显示代表地图的文字而已。

Evennia 提供了 Batch-Processors 用作游戏外创建的输入文件。在此例子中我们会使用两个给力的批处理器:Batch-Code-Processors,以命令 @batchcode 调用。这允许我们用 Python 文件来建立我们的世界。这些文件有访问 Evennia 中的 API 的能力。

下面是一个简单的示例:

# mygame/world/batchcode_map.py

from evennia import create_object
from evennia import DefaultObject

# We use the create_object function to call into existence a
# DefaultObject named "Map" wherever you are standing.

map = create_object(DefaultObject, key="Map", location=caller.location)

# We then access its description directly to make it our map.

map.db.desc = """
≈≈↑↑↑↑↑∩∩
≈≈↑╔═╗↑∩∩
≈≈↑║O║↑∩∩
≈≈↑╚∞╝↑∩∩
≈≈≈↑│↑∩∩∩
≈≈O─O─O⌂∩
≈≈≈↑│↑∩∩∩
≈≈↑▲O▲↑∩∩
≈≈↑↑▲↑↑∩∩
≈≈↑↑↑↑↑∩∩
"""

# This message lets us know our map was created successfully.
caller.msg("A map appears out of thin air and falls to the ground.")

然后我们在游戏中用命令进行调用:

unquell
@batchcode batchcode_map
@batchcode batchcode_map
Running Batch-code processor - Automatic mode for batchcode_map ...
01/01: from evennia import create_object\nfrom evennia import D[...]
A map appears out of thin air and falls to the ground.
End of batch file.
Batchfile 'batchcode_map' applied.

look Map
Map(#75)

≈≈↑↑↑↑↑∩∩
≈≈↑╔═╗↑∩∩
≈≈↑║O║↑∩∩
≈≈↑╚∞╝↑∩∩
≈≈≈↑│↑∩∩∩
≈≈O─O─O⌂∩
≈≈≈↑│↑∩∩∩
≈≈↑▲O▲↑∩∩
≈≈↑↑▲↑↑∩∩
≈≈↑↑↑↑↑∩∩

然后就会执行此脚本内的内容。你会看到,有一个 map 已经落到了地上,可以将他捡起来。

构建地图区域

我们先前构建了地图对象,但是实际上地图中的位置是还没有建立的。现在我们来写一个构建这些位置的脚本。我们有个五个区域需要建立:城堡,山寨,营地,海岸,十字路口,我们的例子将会如下:

# mygame/world/batchcode_world.py

from evennia import create_object, search_object
from typeclasses import rooms, exits

# We begin by creating our rooms so we can detail them later.

centre = create_object(rooms.Room, key="crossroads")
north = create_object(rooms.Room, key="castle")
east = create_object(rooms.Room, key="cottage")
south = create_object(rooms.Room, key="camp")
west = create_object(rooms.Room, key="coast")

# This is where we set up the cross roads.
# The rooms description is what we see with the 'look' command.

centre.db.desc = """
The merger of two roads. A single lamp post dimly illuminates the lonely crossroads.
To the north looms a mighty castle. To the south the glow of a campfire can be seen.
To the east lie a wall of mountains and to the west the dull roar of the open sea.
"""

# Here we are creating exits from the centre "crossroads" location to
# destinations to the north, east, south, and west. We will be able
# to use the exit by typing it's key e.g. "north" or an alias e.g. "n".

centre_north = create_object(exits.Exit, key="north",
aliases=["n"], location=centre, destination=north)
centre_east = create_object(exits.Exit, key="east",
aliases=["e"], location=centre, destination=east)
centre_south = create_object(exits.Exit, key="south",
aliases=["s"], location=centre, destination=south)
centre_west = create_object(exits.Exit, key="west",
aliases=["w"], location=centre, destination=west)

# Now we repeat this for the other rooms we'll be implementing.
# This is where we set up the northern castle.

north.db.desc = "An impressive castle surrounds you. " \
"There might be a princess in one of these towers."
north_south = create_object(exits.Exit, key="south",
aliases=["s"], location=north, destination=centre)

# This is where we set up the eastern cottage.

east.db.desc = "A cosy cottage nestled among mountains " \
"stretching east as far as the eye can see."
east_west = create_object(exits.Exit, key="west",
aliases=["w"], location=east, destination=centre)

# This is where we set up the southern camp.

south.db.desc = "Surrounding a clearing are a number of " \
"tribal tents and at their centre a roaring fire."
south_north = create_object(exits.Exit, key="north",
aliases=["n"], location=south, destination=centre)

# This is where we set up the western coast.

west.db.desc = "The dark forest halts to a sandy beach. " \
"The sound of crashing waves calms the soul."
west_east = create_object(exits.Exit, key="east",
aliases=["e"], location=west, destination=centre)

# Lastly, lets make an entrance to our world from the default Limbo room.

limbo = search_object('Limbo')[0]
limbo_exit = create_object(exits.Exit, key="enter world",
aliases=["enter"], location=limbo, destination=centre)

执行命令:

@batchcode batchcode_world

游戏中的小地图

现在我们已经有了平面图和游戏中的区域了,但实际上我们需要的是看到周围的小地图,这将会随着我们的移动而变化。

我们 可能可以 手动在建立各个小地图,然后在小地图的描述上写入地图的一部分。但是有的 MUD 有数以千计的房间,这样做是不现实的。当我们地图有变更的时候,想要进行房间描述的变化更是一个灾难。

因此,我们应该建立一个中心模块来保持地图。房间 引用此中心模块来获取地图。当我们的地图变更,那么我们重新执行一下 batch Code 就行了。

想要制作我们自己的小地图,我们就需要将我们的地图进行分隔。对于我们的字符串显示的地图,很好的是,Python 允许我们将字符串看到字符的列表,因此我们可以从字符串内抓取要显示的字符。

# We place our map into a sting here.
world_map = """\
≈≈↑↑↑↑↑∩∩
≈≈↑╔═╗↑∩∩
≈≈↑║O║↑∩∩
≈≈↑╚∞╝↑∩∩
≈≈≈↑│↑∩∩∩
≈≈O─O─O⌂∩
≈≈≈↑│↑∩∩∩
≈≈↑▲O▲↑∩∩
≈≈↑↑▲↑↑∩∩
≈≈↑↑↑↑↑∩∩
"""

# This turns our map string into a list of rows. Because python
# allows us to treat strings as a list of characters, we can access
# those characters with world_map[5][5] where world_map[row][column].
world_map = world_map.split('\n')

def return_map():
"""
返回整个地图
"""
map = ""

#实际上是将地图分隔成行进行保存
for valuey in world_map:
map += valuey
map += "\n"

return map

def return_minimap(x, y, radius = 2):
"""
返回地图的一部分。
返回位置(x,y) 处半径为2的所有字符
"""
map = ""

#For each row we need, add the characters we need.
for valuey in world_map[y-radius:y+radius+1]:
for valuex in valuey[x-radius:x+radius+1]:
map += valuex
map += "\n"

return map

在此地图模块建立后,我们用其替换我们硬编码的 batchcode_map.py

# mygame/world/batchcode_map.py

from evennia import create_object
from evennia import DefaultObject
from world import map_module

map = create_object(DefaultObject, key="Map", location=caller.location)

map.db.desc = map_module.return_map()

caller.msg("A map appears out of thin air and falls to the ground.")

进入游戏,执行此脚本:

@batchcode batchcode_map

我们要先删除我们之前的地图,记住这个问题。

现在我们将注意力转移到我们的游戏 房间上return_minimap 将会被用来在我们的房间内,使其包含一个简单的地图。这是有一点复杂的。

我们可以用 room.db.desc = map_string + description_string 的形式来显示地图,但事实上我们想要的是让小地图紧挨着 room.db.desc。所以会使用 EvTable 来实现这个目的。我们会建立一个 一行 两列 的 EvTable 。一列用来放描述,一行用来放地图。

# mygame\world\batchcode_world.py

# Add to imports
from evennia.utils import evtable
from world import map_module

# [...]

# Replace the descriptions with the below code.

# The cross roads.
# We pass what we want in our table and EvTable does the rest.
# Passing two arguments will create two columns but we could add more.
# We also specify no border.
centre.db.desc = evtable.EvTable(map_module.return_minimap(4,5),
"The merger of two roads. A single lamp post dimly " \
"illuminates the lonely crossroads. To the north " \
"looms a mighty castle. To the south the glow of " \
"a campfire can be seen. To the east lie a wall of " \
"mountains and to the west the dull roar of the open sea.",
border=None)
# EvTable allows formatting individual columns and cells. We use that here
# to set a maximum width for our description, but letting the map fill
# whatever space it needs.
centre.db.desc.reformat_column(1, width=70)

# [...]

# The northern castle.
north.db.desc = evtable.EvTable(map_module.return_minimap(4,2),
"An impressive castle surrounds you. There might be " \
"a princess in one of these towers.",
border=None)
north.db.desc.reformat_column(1, width=70)

# [...]

# The eastern cottage.
east.db.desc = evtable.EvTable(map_module.return_minimap(6,5),
"A cosy cottage nestled among mountains stretching " \
"east as far as the eye can see.",
border=None)
east.db.desc.reformat_column(1, width=70)

# [...]

# The southern camp.
south.db.desc = evtable.EvTable(map_module.return_minimap(4,7),
"Surrounding a clearing are a number of tribal tents " \
"and at their centre a roaring fire.",
border=None)
south.db.desc.reformat_column(1, width=70)

# [...]

# The western coast.
west.db.desc = evtable.EvTable(map_module.return_minimap(2,5),
"The dark forest halts to a sandy beach. The sound of " \
"crashing waves calms the soul.",
border=None)
west.db.desc.reformat_column(1, width=70)

动态地图

我们在此节将会介绍,通过 房间 间的关联关系来动态的建立地图。

房间格子

要想此例子工作,至少有两个需求要达到

  1. 游戏的结构要跟随一个逻辑布局。我们在此例子中建设,只使用基本方向(N,E,S,W)
  2. 房间必须正确的连接。

概念

原理是,一只 蠕虫(Warm) 从当前位置,选取一个方向进行移动,并标记出位置。当走到一个预定义的距离后就停止,然后从另外一个方向进行移动。要注意的是,我们想要的是个很容易调用而不要太复杂。因此我们会将这些内容封装到一个 Python Class 中。

当我们执行 look 命令的时候,将会看到如下类似的东西:

Hallway

[.] [.]
[@][.][.][.][.]
[.] [.] [.]

The distant echoes of the forgotten
wail throughout the empty halls.

Exits: North, East, South

我们有 [@] 来定义当前的位置,[.] 来定义其他房间。

设置地图显示

我们首先要定义不同的房间如何进行显示。以便我们的 “Worm”知道如何根据 Room 中的 sector_type 属性来绘制图样。例子中我们定义两个符号——一个普通房间,及一个我们已经在其中的房间。我们也定义了一个如果房间没有 setor_type 属性的房间。

# in mygame/world/map.py

# the symbol is identified with a key "sector_type" on the
# Room. Keys None and "you" must always exist.
SYMBOLS = { None : ' . ', # for rooms without sector_type Attribute
'you' : '[@]',
'SECT_INSIDE': '[.]' }

试图访问一个不存在的属性将会返回 None,这就是说没有 setor_type 的房间将会显示为 .。现在点开始构造 Map 类.

class Map(object):

def __init__(self, caller, max_width=9, max_length=9):
self.caller = caller
self.max_width = max_width
self.max_length = max_length
self.worm_has_mapped = {} "
self.curX = None
self.curY = None
  • self.caller 通常是角色自己,调用地图的对象。
  • self.max_width/length 将要产生地图的最大宽度和长度。设置为 奇数来让我们的地图有一个中心点。
  • self.worm_has_mapped。 Worm 已经标记出的位置。
  • self.curX/Y Worm 当前位置

在实际进行任何类型的地图之前,我们需要创建一个空的显示区域,并使用以下方法对其进行完整性检查

def create_grid(self):
# This method simply creates an empty grid/display area
# with the specified variables from __init__(self):
board = []
for row in range(self.max_width):
board.append([])
for column in range(self.max_length):
board[row].append(' ')
return board

def check_grid(self):
# this method simply checks the grid to make sure
# that both max_l and max_w are odd numbers.
return True if self.max_length % 2 != 0 or self.max_width % 2 != 0\
else False

def draw_room_on_map(self, room, max_distance):
self.draw(room)

if max_distance == 0:
return

for exit in room.exits:
if exit.name not in ("north", "east", "west", "south"):
# we only map in the cardinal directions. Mapping up/down would be
# an interesting learning project for someone who wanted to try it.
continue
if self.has_drawn(exit.destination):
# we've been to the destination already, skip ahead.
continue

self.update_pos(room, exit.name.lower())
self.draw_room_on_map(exit.destination, max_distance - 1)

Mapper

  • self.draw(self, room) - 将房间绘制到表格
  • self.has_drawn(self, room) - 是否已经绘制
  • self.median(self, number) - 计算 0,n 中间的中位数。
  • self.update_pos(self, room, exit_name) - 重置 worm 的位置。
  • self.start_loc_on_grid(self) - 标记我们的当前位置
  • self.show_map -绘制完毕显示地图
  • self.draw_room_on_map(self, room, max_distance) - 主要方法函数。

Map

init

#mygame/world/map.py

class Map(object):

def __init__(self, caller, max_width=9, max_length=9):
self.caller = caller
self.max_width = max_width
self.max_length = max_length
self.worm_has_mapped = {}
self.curX = None
self.curY = None

if self.check_grid():
# we have to store the grid into a variable
self.grid = self.create_grid()
# we use the algebraic relationship
self.draw_room_on_map(caller.location,
((min(max_width, max_length) -1 ) / 2)

draw_room_on_map

# in mygame/world/map.py, in the Map class

def draw_room_on_map(self, room, max_distance):
self.draw(room)

if max_distance == 0:
return

for exit in room.exits:
if exit.name not in ("north", "east", "west", "south"):
# we only map in the cardinal directions. Mapping up/down would be
# an interesting learning project for someone who wanted to try it.
continue
if self.has_drawn(exit.destination):
# we've been to the destination already, skip ahead.
continue

self.update_pos(room, exit.name.lower())
self.draw_room_on_map(exit.destination, max_distance - 1)

draw

def draw(self, room):
# draw initial ch location on map first!
if room == self.caller.location:
self.start_loc_on_grid()
self.worm_has_mapped[room] = [self.curX, self.curY]
else:
# map all other rooms
self.worm_has_mapped[room] = [self.curX, self.curY]
# this will use the sector_type Attribute or None if not set.
self.grid[self.curX][self.curY] = SYMBOLS[room.db.sector_type]

start_loc_on_grid

def median(self, num):
lst = sorted(range(0, num))
n = len(lst)
m = n -1
return (lst[n//2] + lst[m//2]) / 2.0

def start_loc_on_grid(self):
x = self.median(self.max_width)
y = self.median(self.max_length)
# x and y are floats by default, can't index lists with float types
x, y = int(x), int(y)

self.grid[x][y] = SYMBOLS['you']
self.curX, self.curY = x, y # updating worms current location

has_drawn

def has_drawn(self, room):
return True if room in self.worm_has_mapped.keys() else False

update_pos

def update_pos(self, room, exit_name):
# this ensures the coordinates stays up to date
# to where the worm is currently at.
self.curX, self.curY = \
self.worm_has_mapped[room][0], self.worm_has_mapped[room][1]

# now we have to actually move the pointer
# variables depending on which 'exit' it found
if exit_name == 'east':
self.curY += 1
elif exit_name == 'west':
self.curY -= 1
elif exit_name == 'north':
self.curX -= 1
elif exit_name == 'south':
self.curX += 1

show_map

def show_map(self):
map_string = ""
for row in self.grid:
map_string += " ".join(row)
map_string += "\n"

return map_string

使用

我们将地图放在 return_appearance 方法中.

# in mygame/typeclasses/rooms.py

from evennia import DefaultRoom
from world.map import Map

class Room(DefaultRoom):

def return_appearance(self, looker):
# [...]
string = "%s\n" % Map(looker).show_map()
# Add all the normal stuff like room description,
# contents, exits etc.
string += "\n" + super().return_appearance(looker)
return string