撰写了文章 发布于 2019-06-19 11:27:01
Roguelike大全,part5
准备战斗
让地下城热闹起来
不知道你有没有感觉到,现在地下城成还是空荡荡的,我们接下来将完成如何生成怪物以及战斗的代码。这部分代码会比较困难,但是当你能够理解,后面就都是小意思了
首先,我们要实现怪物的放置虽然这个部分会看起来有点无聊。是的,这个确实很简单。但这是由于我们已经实现了基类object。于是,我们要做的无非是在每一个房间里都随机的生产景怪物,于是,我们写了一个简单的函数来让房间里住进一些住户
def place_objects(room): #choose random number of monsters num_monsters = libtcod.random_get_int(0, 0, MAX_ROOM_MONSTERS) for i in range(num_monsters): #choose random spot for this monster x = libtcod.random_get_int(0, room.x1, room.x2) y = libtcod.random_get_int(0, room.y1, room.y2) if libtcod.random_get_int(0, 0, 100) < 80: #80% chance of getting an orc #create an orc monster = Object(x, y, 'o', libtcod.desaturated_green) else: #create a troll monster = Object(x, y, 'T', libtcod.darker_green) objects.append(monster)
我决定创造半兽人和侏儒,而你可以选择任何你想要的怪物。事实上,这个函数你爱怎么改都可以。这也许是一个最简单的函数了,如果你想生成多个怪物,那就在这个函数后面增加新的判断和新的随机数。
#chances: 20% monster A, 40% monster B, 10% monster C, 30% monster D:choice = libtcod.random_get_int(0, 0, 100)if choice < 20: #create monster Aelif choice < 20+40: #create monster Belif choice < 20+40+10: #create monster Celse: #create monster D
另一种方案是制造一些事先设计好的组合,然后随机的选择其中一个组合
每一个组合都是一些怪物的小组,例如,一个侏儒和一些半兽人或者是50%的半兽人和50%的哥布林弓箭手
天空才是你的极限,你也可以用同样的方法来生成物品,但是我们等会再来做这件事情
现在地下城已经可以为每个房间生成怪物了在在姜房间增加到列表之前执行这个函数
在make_map函数里
#add some contents to this room, such as monsters place_objects(new_room)
我们将NPC的生成移出了列表他们再也没有必要了
物体阻塞
在这里,我们还要增加一些小功能。首先就是物体阻塞检测,我们不希望在同一个tile里有超过两个怪物。因为这样的话,就只有一个怪,会不会显示出来。而其他的则会被遮住。
有些物体尤其是物品不进行阻塞检测,如果你不能站在一瓶恢复药水的旁边,这就太傻了。为每个物体增加一个新的block属性
我们再为物体增加一个有用的属性name,这个属性在游戏中和人机界面上很有用,在初始化函数里增加这两个属性
def __init__(self, x, y, char, name, color, blocks=False): self.name = name self.blocks = blocks
现在我们写一个函数来检测一个tile上的物体有没有发生碰撞。就非常简单,但是非常的有用,并且可以让你未来不用为此头疼
def is_blocked(x, y): #first test the map tile if map[x][y].blocked: return True #now check for any blocking objects for object in objects: if object.blocks and object.x == x and object.y == y: return True return False
现在是使用这个函数的时候了在一个物体运动之后调用它
if not is_blocked(self.x + dx, self.y + dy):
现在每一个人,包括玩家都不可以跨过一个已经阻塞的tile!在place_object函数里,我们要看一看这个地块有没有阻塞,
#only place it if the tile is not blocked if not is_blocked(x, y):
不要忘记缩进。这保证了怪物们不会重叠!由于现在物体拥有了超过两个的属性,我们需要在创造它们的时候定义它们,就像我们生成玩家物体时一样,用下面的代码替换之前的初始化代码。
#create object representing the playerplayer = Object(SCREEN_WIDTH/2, SCREEN_HEIGHT/2, '@', libtcod.white) #create an NPCnpc = Object(SCREEN_WIDTH/2 - 5, SCREEN_HEIGHT/2, '@', libtcod.yellow) #the list of objects with those twoobjects = [npc, player]
#create object representing the playerplayer = Object(0, 0, '@', 'player', libtcod.white, blocks=True) #the list of objects starting with the playerobjects = [player]
if libtcod.random_get_int(0, 0, 100) < 80: #80% chance of getting an orc #create an orc monster = Object(x, y, 'o', 'orc', libtcod.desaturated_green, blocks=True) else: #create a troll monster = Object(x, y, 'T', 'troll', libtcod.darker_green, blocks=True)
并更新创造怪物的代码。
如果你在之前的章节里增加了可选的房间标号,你要更新代码,为其加上name属性 room_no = Object(new_x, new_y, chr(65+num_rooms), 'room number', libtcod.white)
游戏状态
在我们获得真正的战斗系统之前,我们还有一件事要做。我们的输入系统有一个致命的缺陷:玩家的行动(移动,战斗)和其它按钮(全屏,其它选项)处理方式是一样的。我们需要将它们分开。这样,当一个玩家暂停或者死亡,他就无法移动或战斗了,但是可以点击其它按钮。我们还希望知道玩家的一个操作有没有结束一个回合。切换成全屏可不应该导致一个回合的流逝。虽然这都是一些细枝末节,但是却是不可或缺的。我们需要两个全局变量,游戏状态和玩家的最后一个操作。(在主循环之前)
game_state = 'playing'player_action = None
在handle_keys函数里,移动和战斗按键只有在游戏状态是'playing"的时候才响应
if game_state == 'playing': #movement keys
我们还要改造函数让它最后返回“exit”
return 'exit' #exit game
测试所有的移动按钮,如果玩家不点击任何一个按键,游戏就不进入下一回合,而是返回'didnt-take-turn'
else: return 'didnt-take-turn'
在主循环运行完handle_keys之后,我们检测系统状态,当检测到'exit'之后退出游戏。
player_action = handle_keys() if player_action == 'exit': break
战斗顺序
在战斗系统的最开始,我们要组织玩家和怪物的战斗顺序。我们这里使用最简单的实现方式,玩家首先出手,在点击了按键之后,如果玩家执行完这一回合,其它的怪物依次执行它们的。就在handle_keys函数之后执行。
#let monsters take their turn if game_state == 'playing' and player_action != 'didnt-take-turn': for object in objects: if object != player: print 'The ' + object.name + ' growls!'
下面的是debug信息,在下一节我们将运行一个AI函数来进行移动和攻击。现在我们只关注玩家的输入。由于我们现在不仅仅可以移动,还可以执行攻击,所以我们写一个新的函数来代替之前的handle_keys函数。
player.move(0, -1) fov_recompute = True
...we'll make a function called player_move_or_attack and replace all those 4 blocks with calls like this:
这个函数叫做player_move_or_attack
player_move_or_attack(0, -1)
这个函数看上去挺长的,其实很简单,无非是判断目标tile可不可以移动,如果不可移动,可不可以攻击,如果可以攻击,则执行攻击指令,否则执行移动指令等等
def player_move_or_attack(dx, dy): global fov_recompute #the coordinates the player is moving to/attacking x = player.x + dx y = player.y + dy #try to find an attackable object there target = None for object in objects: if object.x == x and object.y == y: target = object break #attack if target found, move otherwise if target is not None: print 'The ' + target.name + ' laughs at your puny efforts to attack him!' else: player.move(dx, dy) fov_recompute = True
好了,虽然这里我们还没有涉及到伤害的代码,但是我们已经看到了双方各自执行自己的行动轮了,而系统按钮也不会导致一轮过去,我们现在来测试一下吧。
代码如链接所示
The whole code so far is available here.
----------------------------------------------------------------------------------
Preparing for combat
准备战斗
Populating the dungeon
让地下城热闹起来
Can you feel that? It's the sense of anticipation in the air! That's right, from now on we won't rest until our game lets us smite some pitiful minions of evil, for great justice! It'll be a long journey, and the code will become more complicated, but there's no point in tip-toeing around it any more; it's a game and we want to play it. Some sections will be a bit of a drag, but if you survive that, the next part will be much more fun and rewarding.
不知道你有没有感觉到,现在地下城成还是空荡荡的,我们接下来将完成如何生成怪物以及战斗的代码。这部分代码会比较困难,但是当你能够理解,后面就都是小意思了
First, we must handle monster placement. While this may seem daunting, it's actually pretty simple, thanks to our generic object system! It only requires us to create a new object and append it to the objects list. Therefore, all we need to do is, for each room, create a few monsters in random positions. So we'll create a simple function to populate a room:
首先,我们要实现怪物的放置虽然这个部分会看起来有点无聊。是的,这个确实很简单。但这是由于我们已经实现了基类object。于是,我们要做的无非是在每一个房间里都随机的生产景怪物,于是,我们写了一个简单的函数来让房间里住进一些住户
def place_objects(room): #choose random number of monsters num_monsters = libtcod.random_get_int(0, 0, MAX_ROOM_MONSTERS) for i in range(num_monsters): #choose random spot for this monster x = libtcod.random_get_int(0, room.x1, room.x2) y = libtcod.random_get_int(0, room.y1, room.y2) if libtcod.random_get_int(0, 0, 100) < 80: #80% chance of getting an orc #create an orc monster = Object(x, y, 'o', libtcod.desaturated_green) else: #create a troll monster = Object(x, y, 'T', libtcod.darker_green) objects.append(monster)
The constant MAX_ROOM_MONSTERS = 3 will be defined with the other constants so that it can be easily tweaked.
变量MAX_ROOM_MONSTERS =3和其他变量一样被定义出来,这样它们修改起来会比较容易
I decided to create orcs and trolls, but you can choose anything else. In fact, you should change this function as much as you want! This is probably the simplest method. If you want to add more monsters, you'll need to keep the random value in a variable and compare it multiple times, using this pattern:
我决定创造半兽人和侏儒,而你可以选择任何你想要的怪物。事实上,这个函数你爱怎么改都可以。这也许是一个最简单的函数了,如果你想生成多个怪物,那就在这个函数后面增加新的判断和新的随机数。
#chances: 20% monster A, 40% monster B, 10% monster C, 30% monster D:choice = libtcod.random_get_int(0, 0, 100)if choice < 20: #create monster Aelif choice < 20+40: #create monster Belif choice < 20+40+10: #create monster Celse: #create monster D
As an alternative, you could define a number of pre-set squads and choose one of them randomly, each squad being a combination of some monsters (for example, one troll and a few orcs, or 50% orcs and 50% goblin archers).
另一种方案是制造一些事先设计好的组合,然后随机的选择其中一个组合
每一个组合都是一些怪物的小组,例如,一个侏儒和一些半兽人或者是50%的半兽人和50%的哥布林弓箭手
The sky is the limit! You can also place items in the same manner, but we'll get to that later.
天空才是你的极限,你也可以用同样的方法来生成物品,但是我们等会再来做这件事情
Now, for the dungeon generator to place monsters in each room, call this function right before adding the new room to the list, inside make_map:
现在地下城已经可以为每个房间生成怪物了在在姜房间增加到列表之前执行这个函数
在make_map函数里
#add some contents to this room, such as monsters place_objects(new_room)
There! I also removed the dummy NPC from the initial objects list (before the main loop): it won't be needed any more.
我们将NPC的生成移出了列表他们再也没有必要了
Blocking objects
物体阻塞
Here, we'll add a few bits that are necessary before we can move on. First, blocking objects. We don't want more than one monster standing in the same tile, because only one will show up and the rest will be hidden!
在这里,我们还要增加一些小功能。首先就是物体阻塞检测,我们不希望在同一个tile里有超过两个怪物。因为这样的话,就只有一个怪,会不会显示出来。而其他的则会被遮住。
Some objects, especially items, don't block (it would be silly if you couldn't stand right next to a healing potion!), so each object will have an extra "blocks" property.
有些物体尤其是物品不进行阻塞检测,如果你不能站在一瓶恢复药水的旁边,这就太傻了。为每个物体增加一个新的block属性
We'll also take the opportunity to allow each object to have a name, which will be useful for game messages and the Graphical User Interface (GUI), which we'll go over later. Just add these two properties to the beginning of the Object 's __init__ method:
我们再为物体增加一个有用的属性name,这个属性在游戏中和人机界面上很有用,在初始化函数里增加这两个属性
def __init__(self, x, y, char, name, color, blocks=False): self.name = name self.blocks = blocks
Now, we'll create a function that tests if a tile is blocked, whether due to a wall or an object blocking it. It's very simple, but it will be useful in a lot of places, and will save you a lot of headaches further down the line.
现在我们写一个函数来检测一个tile上的物体有没有发生碰撞。就非常简单,但是非常的有用,并且可以让你未来不用为此头疼
def is_blocked(x, y): #first test the map tile if map[x][y].blocked: return True #now check for any blocking objects for object in objects: if object.blocks and object.x == x and object.y == y: return True return False
OK, now it's time to give it some use! First of all, in the Object 's move method, change the if condition to:
现在是使用这个函数的时候了在一个物体运动之后调用它
if not is_blocked(self.x + dx, self.y + dy):
Anyone, including the player, can't move over a blocking object now! Next, in the place_objects function, we'll see if the tile is unblocked before placing a new monster:
现在每一个人,包括玩家都不可以跨过一个已经阻塞的tile!在place_object函数里,我们要看一看这个地块有没有阻塞,
#only place it if the tile is not blocked if not is_blocked(x, y):
Don't forget to indent the lines after that. This guarantees that monsters don't overlap! And since objects have two more properties, we need to define them whenever we create one, such as the line that creates the player object. Replace the old object initialization code:
不要忘记缩进。这保证了怪物们不会重叠!由于现在物体拥有了超过两个的属性,我们需要在创造它们的时候定义它们,就像我们生成玩家物体时一样,用下面的代码替换之前的初始化代码。
#create object representing the playerplayer = Object(SCREEN_WIDTH/2, SCREEN_HEIGHT/2, '@', libtcod.white) #create an NPCnpc = Object(SCREEN_WIDTH/2 - 5, SCREEN_HEIGHT/2, '@', libtcod.yellow) #the list of objects with those twoobjects = [npc, player]
With this:
#create object representing the playerplayer = Object(0, 0, '@', 'player', libtcod.white, blocks=True) #the list of objects starting with the playerobjects = [player]
And update the code that creates the monsters:
if libtcod.random_get_int(0, 0, 100) < 80: #80% chance of getting an orc #create an orc monster = Object(x, y, 'o', 'orc', libtcod.desaturated_green, blocks=True) else: #create a troll monster = Object(x, y, 'T', 'troll', libtcod.darker_green, blocks=True)
并更新创造怪物的代码。
If you added the optional room "numbers" in part 3, you'll need to update the code to include a name.
如果你在之前的章节里增加了可选的房间标号,你要更新代码,为其加上name属性 room_no = Object(new_x, new_y, chr(65+num_rooms), 'room number', libtcod.white)
Game states
Last stop before we get to the actual combat system! Our input system has a fatal flaw: player actions (movement, combat) and other keys (fullscreen, other options) are handled the same way. We need to separate them. This way, if the player pauses or dies he can't move or fight, but can press other keys. We also want to know if the player's input means he finished his turn or not; changing to fullscreen shouldn't count as a turn. I know they're just simple details - but the game would be incredibly annoying without them! We only need two global variables, the game state and the player's last action (set before the main loop).
游戏状态
在我们获得真正的战斗系统之前,我们还有一件事要做。我们的输入系统有一个致命的缺陷:玩家的行动(移动,战斗)和其它按钮(全屏,其它选项)处理方式是一样的。我们需要将它们分开。这样,当一个玩家暂停或者死亡,他就无法移动或战斗了,但是可以点击其它按钮。我们还希望知道玩家的一个操作有没有结束一个回合。切换成全屏可不应该导致一个回合的流逝。虽然这都是一些细枝末节,但是却是不可或缺的。我们需要两个全局变量,游戏状态和玩家的最后一个操作。(在主循环之前)
game_state = 'playing'player_action = None
Inside handle_keys, the movement/combat keys can only be used if the game state is "playing":
在handle_keys函数里,移动和战斗按键只有在游戏状态是'playing"的时候才响应
if game_state == 'playing': #movement keys
We'll also change the same function so it returns a string with the type of player action. Instead of returning True to exit the game, return a special string:
我们还要改造函数让它最后返回“exit”
return 'exit' #exit game
And testing for all the movement keys, if the player didn't press any, then he didn't take a turn, so return a special string in that case:
测试所有的移动按钮,如果玩家不点击任何一个按键,游戏就不进入下一回合,而是返回'didnt-take-turn'
else: return 'didnt-take-turn'
After the call to handle_keys in the main loop, we can check for the special string 'exit' to exit the game. Later we'll do other stuff according to the player_action string.
在主循环运行完handle_keys之后,我们检测系统状态,当检测到'exit'之后退出游戏。
player_action = handle_keys() if player_action == 'exit': break
Fighting orderly
For the first part of the combat system we have to manage the player and monsters taking combat turns, as well as the player making an attack. To make it simple, the player takes his turn first, in handle_keys. If he took a turn, all the monsters take theirs. This goes after the handle_keys block in the main loop:
战斗顺序
在战斗系统的最开始,我们要组织玩家和怪物的战斗顺序。我们这里使用最简单的实现方式,玩家首先出手,在点击了按键之后,如果玩家执行完这一回合,其它的怪物依次执行它们的。就在handle_keys函数之后执行。
#let monsters take their turn if game_state == 'playing' and player_action != 'didnt-take-turn': for object in objects: if object != player: print 'The ' + object.name + ' growls!'
That's just a debug message, in the next part we'll call an AI routine to move and attack. Now we'll take care of the player input. Since he can attack now, instead of calling move (inside handle_keys) like this:
下面的是debug信息,在下一节我们将运行一个AI函数来进行移动和攻击。现在我们只关注玩家的输入。由于我们现在不仅仅可以移动,还可以执行攻击,所以我们写一个新的函数来代替之前的handle_keys函数。
player.move(0, -1) fov_recompute = True
...we'll make a function called player_move_or_attack and replace all those 4 blocks with calls like this:
这个函数叫做player_move_or_attack
player_move_or_attack(0, -1)
The function itself has a few lines of code but doesn't do anything extraordinary. It has to check if there's an object in the direction the player wants to move. If so, a debug message will be printed (this will later be replaced by an actual attack) If not, the player will just move there.
这个函数看上去挺长的,其实很简单,无非是判断目标tile可不可以移动,如果不可移动,可不可以攻击,如果可以攻击,则执行攻击指令,否则执行移动指令等等
def player_move_or_attack(dx, dy): global fov_recompute #the coordinates the player is moving to/attacking x = player.x + dx y = player.y + dy #try to find an attackable object there target = None for object in objects: if object.x == x and object.y == y: target = object break #attack if target found, move otherwise if target is not None: print 'The ' + target.name + ' laughs at your puny efforts to attack him!' else: player.move(dx, dy) fov_recompute = True
Alright, the code is ready to test! No damage is done yet but you can see the monsters taking their turns after you (notice when you switch to fullscreen it doesn't count as a turn, yay!), and you can bump into them to heroically but unsuccessfully try to destroy them!
好了,虽然这里我们还没有涉及到伤害的代码,但是我们已经看到了双方各自执行自己的行动轮了,而系统按钮也不会导致一轮过去,我们现在来测试一下吧。
代码如链接所示
The whole code so far is available here.
Guess what's next?