第十二章:L2JMobius学习 – NPC孵化和怪物巡逻

在上一个章节中,我们已经介绍了玩家角色在游戏世界的移动。移动完毕之后,会显示周围的游戏对象,其中就包括NPC怪物。当然,玩家“孵化”自己(调用spawnMe方法)的时候,也会显示周围的游戏对象。首先,我们要介绍的时候NPC的实例化。在我们GameServer启动的时候,会执行入下代码:

// NPC数据读取(npc数据表)
if (!NpcTable.getInstance().isInitialized())

// NPC孵化(载入出生点并实例化)
SpawnTable.getInstance();

第一句代码NpcTable用来读取npc数据表,里面的纪录了所有NPC的模板数据,根据这些模板数据就可以实例化多个npc对象。该表中有一个“type”字段,字段值包括:Monster(怪物), Teleporter(传送师), Merchant(商人), VillageMaster(大师/宗师)等等。它代表的就是NPC真正的实例化的对象类。我们以怪物Monster类来说明,首先看它的继承情况。

Monster -> Attackable -> Npc -> Creature -> WorldObject

父类WorldObject和Creature就不用说了,我们之前介绍角色的时候,就已经讲过了,玩家角色Player类是从他们继承下来的,这里我们的npc也是如此。当然,上面介绍的其他NPC的类型也都是由“Npc -> Creature -> WorldObject”继承下来的。接下来,我们继续回到NpcTable类中,该类中有一个Map<Integer, NpcTemplate> _npcs 集合,里面缓存了所有从数据表npc中读取出来的纪录,每一条纪录都会实例化成一个NpcTemplate数据对象,集合中每个NpcTemplate数据对象对应的key值就是数据主键ID。我们之间也介绍过,很多读取数据的单例类,都会将自己读取的数据保存到一个集合中,方便后期使用。这个NpcTemplate数据对象存储了npc数据表里面的字段数据。

接下来就是SpawnTable单例类,它读取spawnlist数据表。这个表里面有有“npc_templateid”,“locx”,“locy”,“locz”等字段。从字段名称来看,他们对应的就是模板数据npc的主键ID,以及出生点xyz坐标。有了模板数据npc的主键ID,就可以从上面的NpcTable类中的集合中获取npc模板数据对象NpcTemplate了。在这个SpawnTable类中,同样有一个Map<Integer, Spawn> _spawntable集合类,里面缓存的是Spawn类。这类就是用来实例化NPC对象的。我们查看它的代码,如下所示

// 获取npc数据表中的模板数据对象 NpcTemplate
template1 = NpcTable.getInstance().getTemplate(rset.getInt("npc_templateid"));

// 根据 NpcTemplate 实例化 Spawn 类
spawnDat = new Spawn(template1);
// 设置npc的出生点
spawnDat.setId(rset.getInt("id"));
spawnDat.setAmount(rset.getInt("count"));
spawnDat.setX(rset.getInt("locx"));
spawnDat.setY(rset.getInt("locy"));
spawnDat.setZ(rset.getInt("locz"));
spawnDat.setHeading(rset.getInt("heading"));

// 执行初始化操作,实例化NPC对象
_npcSpawnCount += spawnDat.init();

// 缓存到集合里面
_spawntable.put(spawnDat.getId(), spawnDat);

上面的代码非常简单,重点我们介绍spawnDat.init(); 方法,也就是孵化器的初始化工作。我们上面已经讲过,不同的npc的实例类是不一样的,虽然他们都继承npc父类。我们需要根据npc数据表中的“type”字段来实例化实际的类对象。这就需要使用Java反射技术。关于这门技术,我们不过多介绍,不熟悉的可以自行学习一下。我们先看一下Spawn的构造方法,代码如下

// npc实际对应的类(Monster, Teleporter, Merchant, VillageMaster等等)
String implementationName = _template.getType();

// 加载 npc实际对应的类
final Class<?>[] parameters = { int.class, NpcTemplate.class };
_constructor = Class.forName("org.l2jmobius.gameserver.model.actor.instance." + implementationName).getConstructor(parameters);

上面的代码就是根据type值来加载对应的类。加载之后才可以使用Java反射实例化。接下来,我们就来看看Spawn类的init方法。

public int init()
{
		while (_currentCount < _maximumCount)
		{
			doSpawn();
		}
		_doRespawn = true;
		return _currentCount;
}

这里使用一个循环,这个_maximumCount就是每一种怪物需要实例化的个数,也就是对应实例化类(Monster, Teleporter, Merchant, VillageMaster等等)的数量。例如,怪物“哥布林”的模板数据存储在NpcTemplate对象中,而spawnlist数据表中记录了怪物“哥布林”的很多的出生点数据。每一个出生点都会实例化出来一个怪物“哥布林”Monster类。也就是说,同一种怪物会被实例化出来多个,这个数量就是上面代码:spawnDat.setAmount(rset.getInt("count")); 中设置进来的。接下来,我们到doSpawn方法中。

// 使用父类Npc来声明类实例对象
Npc npc = null;

// 实例化对象类的构造方法的参数
final Object[] parameters ={IdManager.getInstance().getNextId(),_template};

// Java反射进行实例化
final Object tmp = _constructor.newInstance(parameters);

// 强制类型转化成npc父类的类型
npc = (Npc) tmp;

// 继续初始化npc
return initializeNpc(npc);

上面的代码就是使用Java反射实例化NPC的子类(Monster, Teleporter, Merchant, VillageMaster等等)。由于我们不知道具体的NPC子类是哪个,所以不能提前声明子类的类型。因此,我们声明的是NPC父类的实例对象,然后将其实例化它的具体的子类,这个就是Java的多态。我们在执行npc父类方法的时候,实际就是执行子类的重写方法(这个不能忽略)。接下来,就是initializeNpc(npc); 代码的执行,继续初始化npc子类。这个方法比较简单,重点在于

// 设置npc的HP和MP
npc.setCurrentHpMp(npc.getMaxHp(), npc.getMaxMp());

// 调用npc子类的spawnMe孵化方法,这个很重要
npc.spawnMe(newlocx, newlocy, newlocz);

重点就是会调用NPC的子类(Monster, Teleporter, Merchant, VillageMaster等等)的spawnMe方法。这个方法,我们之前遇到过,就是玩家角色Player被实例化的时候就调用了。请大家注意的是,这个spawnMe方法是在WorldObject类中的。这个WorldObject类是所有游戏对象的父类,不管是玩家,还是npc,都继承这个父类。也就是说,npc的孵化和玩家的孵化,都是同一个spawnMe方法。我们回顾一下这个方法吧。

// 设置自己的位置坐标和瓦片地图
_location.setXYZ(spawnX, spawnY, z);
setWorldRegion(World.getInstance().getRegion(getLocation()));

// 将当前游戏对象放入到游戏世界World的_allobjects集合中
World.getInstance().storeObject(this);

// 将当前游戏对象放入到瓦片地图WorldRegion的_visibleObjects 集合中
final WorldRegion region = getWorldRegion();

// 查询当前角色周围的游戏对象,放入到 _knownList/_knownObjects 集合中
World.getInstance().addVisibleObject(this, region, null);

// 调用Creature类的onSpawn方法
onSpawn();

上面的代码就不再详细解释了。问题在于,npc的孵化和玩家的孵化真的一模一样码?肯定不一样的,例如,怪物在游戏世界中被孵化出来之后,要进行巡逻的。这个在哪里实现的呢?我们看看这个代码吧。

// 查询当前角色周围的游戏对象,放入到 _knownList/_knownObjects 集合中
World.getInstance().addVisibleObject(this, region, null);

我们去游戏世界类Wrold里面的查看一下这个addVisibleObject方法。

// 从地图上查找附近的游戏对象
final List<WorldObject> visibleObjects = getVisibleObjects(object, 2000);
for (int i = 0; i < visibleObjects.size(); i++)
{
    final WorldObject wo = visibleObjects.get(i);
	if (wo == null){continue;}
	
    // 周围的对象把 当前角色"我" 加入到 _knownObjects 列表中
	wo.getKnownList().addKnownObject(object, dropper);
	
	// 当前角色"我" 把 周围对象加入到 _knownObjects 列表中
	object.getKnownList().addKnownObject(wo, dropper);
}

上面的代码,整体思路非常的清晰,就是查询附近的游戏对象,然后添加到自己的_knownObjects 列表中。重点在于当前角色“我”的getKnownList方法是不一样的。我们之前讲解过附近游戏对象类WorldObjectKnownList,它只是一个父类而已,具体还要子类来实现不同的功能。例如,玩家角色Player对象的就是PlayerKnownList类,而怪物Monster对应的则是MonsterKnownList类。也就是说,虽然都是object.getKnownList()方法调用。但是,如果这个object是Player实例的话,它返回的是PlayerKnownList类;如果这个object是Monster实例的话,它返回的就是MonsterKnownList类。这个与AI类是一样的道理。接下来,我们就看看MonsterKnownList类是如何处理的?我们来查看它的addKnownObject方法。

// 调用父类的同名方法
if (!super.addKnownObject(object, dropper))


// 设置当前状态为 AI_INTENTION_ACTIVE
getActiveChar().getAI().setIntention(CtrlIntention.AI_INTENTION_ACTIVE, null);

在我们原来的addKnownObject方法中,就是把周围游戏对象添加到_knownObjects 列表中,并没有其他的操作。这里,我们的MonsterKnownList重写了addKnownObject方法,增加了额外的代码,就是让当前怪物的状态变成CtrlIntention.AI_INTENTION_ACTIVE ,我们可以理解这个状态就是怪物的巡逻状态。这个状态怎么执行的呢?首先还是查看AI类。对于怪物Monster类而言,它的AI类是AttackableAI类,它会继承父类CreatureAI。但是,对于setIntention方法来讲,它是位于更高的父类AbstractAI中。我们之前讲解玩家角色移动的时候,就讲解过这个类。

case AI_INTENTION_ACTIVE:
{
    onIntentionActive();
    break;
}

继续调用onIntentionActive 方法,这个方法主要在CreatureAI 类中

// 修改状态为 AI_INTENTION_ACTIVE
changeIntention(AI_INTENTION_ACTIVE, null, null);

// 调用 onEvtThink 方法
onEvtThink();

继续调用changeIntention方法和onEvtThink方法,这两个方法是在AttackableAI 类中的。在AttackableAI 类在changeIntention方法中有一个非常重要的方法:

// 启动AI任务,自动巡逻或自动攻击
startAITask();

这个AI任务就是定时执行ai.onEvtThink(); 方法(也就是说,程序会不停的调用onEvtThink)。我们上面代码中,执行完changeIntention方法之后,也调用了onEvtThink方法。只是时间前后问题而已。我们上面已经说到了,这个onEvtThink方法就在AttackableAI类中。

// Manage AI thinks of a Attackable
if (getIntention() == AI_INTENTION_ACTIVE)
{
    thinkActive();
}
else if (getIntention() == AI_INTENTION_ATTACK)
{
    thinkAttack();
}

如果是巡逻状态,就执行thinkActive方法;如果是攻击状态,就执行thinkAttack方法。我们来看看thinkActive方法,该方法也位于当前AttackableAI 类中。

// 怪物移动目标点
int x1;
int y1;
int z1;

// 根据出生点来随机一个位置
final int[] p = TerritoryTable.getInstance().getRandomPoint(npc.getSpawn().getLocation());
x1 = p[0];
y1 = p[1];
z1 = p[2];

// 移动到目标点
moveTo(x1, y1, z1);

看到moveTo方法,大家应该想起玩家角色Player的移动逻辑了吧。这个moveTo方法位于AbstractAI类中,我们复习一下。

// 调用 Creature 类的 moveToLocation 方法
_accessor.moveTo(x, y, z);

// 调用 Creature 类的 broadcastMoveToLocation 方法
_actor.broadcastMoveToLocation();

以后的代码逻辑,就跟玩家角色的移动共享了。在Creature 类的 moveToLocation 方法中,我计算移动速度,路程等数据,然后放入到移动任务管理器MovementTaskManager中。这个管理器会定时执行Creature 类的updatePosition方法来更新角色的位置。剩下的就是怪物定时同步服务器端的位置了。这个逻辑与玩家角色移动是一样的。只不过,玩家角色是手动鼠标控制的,而怪物移动则是通过startAITask(); 任务定时控制的。

最后,我们总结一下npc的刷新

第一,NpcTable用来读取npc数据表,获取npc模板数据

第二,SpawnTable读取spawnlist数据表,获取npc出生点数据

第三,根据npc模板数据和npc出生点数据实例化Spawn类

第四,调用Spawn类的init方法进行初始化

第五,调用Spawn类的doSpawn方法进行孵化操作

第六,使用Java反射实例化npc实例子类,

第七,调用npc实例子类的spawnMe方法

第八,调用World类的addVisibleObject方法添加附近游戏对象

第九,如果是MonsterKnownList类的话,会设置怪物为AI_INTENTION_ACTIVE状态

第十,执行AbstractAI类中的 setIntention 方法

第十一,执行CreatureAI 类中的 onIntentionActive 方法

第十二,执行AttackableAI 类中的 changeIntention方法

第十三,执行AttackableAI 类中的 startAITask方法,启动巡逻任务

第十四,执行AttackableAI 类中的 onEvtThink方法,定期执行哦

第十五,执行AttackableAI 类中的 thinkActive 方法,生成随机目标点

第十六,执行AbstractAI类中的 moveTo移动到目标点

.....后续代码就不详细介绍了。

关于NPC的刷新以及怪物的随机巡逻,我们就介绍到这里。Npc如何发送给客户端,我们下一章介绍。本章节涉及的内容均已上传百度网盘:

https://pan.baidu.com/s/1XdlcCFPvXnzfwFoVK7Sn7Q?pwd=avd4

欢迎加企鹅交流裙:874700842(裙文件里面也可以下载所有内容)。

猜你喜欢

转载自blog.csdn.net/konkon2012/article/details/131699877
今日推荐