顯示房間的動態地圖¶
MUD 中經常需要的功能是顯示遊戲內地圖以幫助導航。
Forest path
[.] [.]
[.][.][@][.][.][.]
[.] [.][.][.]
The trees are looming over the narrow forest path.
Exits: East, West
房間網格¶
本教學的執行至少需要滿足兩個要求。
泥漿的結構必須遵循邏輯佈局。 Evennia 支援你的世界的佈局在「邏輯上」是不可能的,房間迴圈到自己或出口通往地圖的另一邊。出口也可以被命名為任何名稱,從「跳出窗外」到「進入第五次元」。本教學假設您只能沿著基本方向(N、E、S 和 W)移動。
房間必須連線並連結在一起才能正確產生地圖。 Vanilla Evennia 附帶一個管理指令 tunnel,允許使用者在基本方向上建立房間,但需要額外的工作來確保房間已連線。例如,如果您
tunnel east,然後立即執行tunnel west,您會發現您已經建立了兩個完全獨立的房間。因此,如果您想建立“邏輯”佈局,則需要小心。在本教學中,我們假設您有這樣一個房間網格,我們可以從中產生地圖。
概念¶
在進入程式碼之前,理解和概念化它是如何運作的是有益的。這個想法類似於從您目前位置開始的蠕蟲。它選擇一個方向並從該方向向外“行走”,並在行走過程中繪製出其路線。一旦它行駛了預設的距離,它就會停下來並從另一個方向重新開始。需要注意的是,我們想要一個易於呼叫且不太複雜的系統。因此,我們將把整個程式碼包裝到一個自訂 Python 類別中(不是 typeclass,因為它不使用 evennia 本身的任何核心物件)。 我們將建立當您輸入“look”時顯示如下的內容:
Hallway
[.] [.]
[@][.][.][.][.]
[.] [.] [.]
The distant echoes of the forgotten
wail throughout the empty halls.
Exits: North, East, South
您目前的位置由 [@] 定義,而 [.]s 是「蠕蟲」見過的其他房間
自從離開您所在的位置後。
設定地圖顯示¶
首先我們必須定義用於顯示地圖的元件。為了讓「蠕蟲」知道在地圖上繪製什麼符號,我們將讓它檢查它訪問的名為 sector_type 的房間上的 Attribute。在本教學中,我們瞭解兩個符號 - 一個普通的房間和我們所在的房間。我們也為沒有說 Attribute 的房間定義了一個後備符號 - 這樣即使我們沒有正確準備房間,地圖仍然可以工作。假設您的遊戲資料夾名為mygame,我們在mygame/world/map.py.中建立此程式碼
# 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': '[.]' }
由於嘗試造訪未設定的 Attribute 返回 None,這意味著沒有 sector_type 的房間
屬性將顯示為. 。接下來我們開始建立自訂類別Map。它將容納所有
我們需要的方法。
# in 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
self.caller通常是您的角色物件,也就是使用地圖的角色物件。self.max_width/length確定將產生的地圖的最大寬度和長度。請注意,將這些變數設為“奇數”非常重要,以確保顯示區域具有中心點。self.worm_has_mapped是基於上面的蠕蟲類比建構的。該字典將儲存「蠕蟲」對映的所有房間及其在網格內的相對位置。這是最重要的變數,因為它充當“檢查器”和“地址簿”,能夠告訴我們蠕蟲病毒去過哪裡以及到目前為止它所對映的內容。self.curX/Y是代表蠕蟲在網格上目前位置的座標。
在實際完成任何型別的對應之前,我們需要建立一個空的顯示區域,並使用以下方法對其進行一些健全性檢查。
# in mygame/world/map.py
class Map(object):
# [... continued]
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
在我們讓蠕蟲病毒繼續傳播之前,我們需要了解這一切背後的一些電腦科學,即「圖遍歷」。在偽程式碼中,我們試圖完成的是:
# pseudo code
def draw_room_on_map(room, max_distance):
self.draw(room)
if max_distance == 0:
return
for exit in room.exits:
if self.has_drawn(exit.destination):
# skip drawing if we already visited the destination
continue
else:
# first time here!
self.draw_room_on_map(exit.destination, max_distance - 1)
Python 的美妙之處在於我們執行此操作的實際程式碼與此沒有太大區別 偽程式碼範例。
max_distance是一個變數,向我們的蠕蟲指示它將對映距離您當前位置有多少個房間 AWAY。顯然,如果您當前位置周圍有很多房間,則數字越大,所需的時間就越長。
這裡的第一個障礙是「max_distance」使用什麼值。蠕蟲沒有理由傳播得比實際顯示給您的距離更遠。例如,如果您的目前位置位於大小為max_length = max_width = 9的顯示區域的中心,那麼蠕蟲只需要
向任一方向走 4 空格:
[.][.][.][.][@][.][.][.][.]
4 3 2 1 0 1 2 3 4
max_distance可以根據顯示區域的大小動態設定。當寬度/長度改變時,它會變成簡單的代數線性關係,即 max_distance = (min(max_width, max_length) -1) / 2。
建構對映器¶
現在我們可以開始用一些方法填入我們的 Map 物件。我們還缺少一些非常重要的方法:
self.draw(self, room)- 負責實際繪製房間到網格。self.has_drawn(self, room)- 檢查房間是否已被對映並且蠕蟲是否已經在這裡。self.median(self, number)- 一種簡單的實用方法,可從0, n中找到中位數(中點)self.update_pos(self, room, exit_name)- 透過相應地重新分配self.curX/Y來更新蠕蟲的物理位置。self.start_loc_on_grid(self)- 網格上的第一個初始繪製代表您在網格中間的位置。self.show_map- 一切完成後將地圖轉換為可讀字串self.draw_room_on_map(self, room, max_distance)- 將它們聯絡在一起的主要方法。
現在我們知道我們需要哪些方法,讓我們改進最初的 __init__(self) 以傳遞一些
條件語句並設定它以開始建立顯示。
#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)
在這裡,我們檢查網格引數是否正確,然後建立一個空畫布並將我們的初始位置對應為第一個房間!
如上所述,self.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)
「蠕蟲」做的第一件事就是在self.draw中繪製你目前的位置。讓我們定義一下…
#in mygame/word/map.py, in the Map class
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]
在self.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
系統繪製完目前地圖後,會檢查 max_distance 是否為 0(因為此
是初始開始階段,但不是)。現在,一旦我們有每個單獨的出口,我們就處理迭代
在房間裡。它做的第一件事是檢查蠕蟲所在的房間是否已經被對映。
讓我們定義…
def has_drawn(self, room):
return True if room in self.worm_has_mapped.keys() else False
如果has_drawn回傳False,這表示蠕蟲已經找到了一個尚未對映的房間。它
然後會「移動」到那裡。 self.curX/Y 有點滯後,所以我們必須確保跟蹤
蠕蟲的位置;我們在下面的 self.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
一旦系統更新了蠕蟲的位置,它就會將新房間回饋給原來的房間
draw_room_on_map() 並重新開始該過程…
這基本上就是整件事情了。最後的方法是將它們組合在一起並製作一個漂亮的
使用 self.show_map() 方法從中提取表示性字串。
def show_map(self):
map_string = ""
for row in self.grid:
map_string += " ".join(row)
map_string += "\n"
return map_string
使用地圖¶
為了觸發地圖,我們將其儲存在房間 typeclass 上。如果我們把它放進去
return_appearance我們每次看房間都會拿回地圖。
return_appearance是所有物件上可用的預設 Evennia 掛鉤;它被稱為e.g。由look指令取得某事物的描述(本例中為房間)。
# in mygame/typeclasses/rooms.py
from evennia import DefaultRoom
from world.map import Map
class Room(DefaultRoom):
def return_appearance(self, looker):
# [...]
string = f"{Map(looker).show_map()}\n"
# Add all the normal stuff like room description,
# contents, exits etc.
string += "\n" + super().return_appearance(looker)
return string
顯然,這種生成地圖的方法沒有考慮任何隱藏的門或出口…等等,但希望它可以作為一個良好的基礎。如前所述,在實施此操作之前,為房間打下堅實的基礎非常重要。您可以透過使用 @tunnel 在原版 evennia 上嘗試此操作,本質上您可以建立一個長直/前衛的非迴圈房間,該房間將顯示在您的遊戲地圖上。
上面的範例將在房間描述上方顯示地圖。您也可以使用 EvTable 將描述和地圖放在一起。您可以做的其他一些事情是讓 Command 以更大的半徑顯示,也許帶有圖例和其他功能。
以下是全部map.py供您參考。您需要更新您的Room typeclass(請參閱上文)才能實際呼叫它。請記住,要檢視某個位置的不同符號,您還需要將房間上的 sector_type Attribute 設定為 SYMBOLS 字典中的按鍵之一。因此,在此範例中,要使房間對應為 [.],您需要將該房間的 sector_type 設為 "SECT_INSIDE"。用@set here/sector_type = "SECT_INSIDE"試試。如果您希望所有新房間都有給定的扇區符號,您可以更改下面 SYMBOLS 字典中的預設值,或者您可以在房間的 at_object_creation 方法中新增 Attribute。
# mygame/world/map.py
# These are keys set with the Attribute sector_type on the room.
# The keys None and "you" must always exist.
SYMBOLS = { None : ' . ', # for rooms without a sector_type attr
'you' : '[@]',
'SECT_INSIDE': '[.]' }
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 actually have to store the grid into a variable
self.grid = self.create_grid()
self.draw_room_on_map(caller.location,
((min(max_width, max_length) -1 ) / 2))
def update_pos(self, room, exit_name):
# this ensures the pointer variables always
# 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
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)
def draw(self, room):
# draw initial caller 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]
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
def has_drawn(self, room):
return True if room in self.worm_has_mapped.keys() else False
def create_grid(self):
# This method simply creates an empty grid
# 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
# 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 show_map(self):
map_string = ""
for row in self.grid:
map_string += " ".join(row)
map_string += "\n"
return map_string
最終意見¶
動態地圖可以透過更多功能進行擴充。例如,它可以標記退出或 也允許 NE、SE 等方向。它可以有適合不同地形型別的顏色。一個可以 也要研究上/下方向並找出如何以良好的方式顯示它。