RPG游戏制作-05-世界多姿多彩 NPC的诞生

NPC(Non-Player Character)在游戏中广泛存在。本游戏是c++为主,lua为辅,然后在脚本中来控制游戏流程,以后的NPC应用广泛,可以是普通的人物,还可以是宝箱,传送阵,乃至是田地,即角色扮演+模拟经营。当然,前提是我们得在c++做好足够的工作才行。

首先,添加一个枚举体

//游戏状态
enum class GameState
{
	Normal,/*正常状态,即行走状态*/
	Fighting,/*战斗状态*/
	Script,/*脚本状态*/
};

GameState即为当前的游戏状态,同时GameScene中加入m_gameState。然后,主角只有在正常状态下才可以操作人物,故EventLayer需要过滤事件:

bool EventLayer::onTouchBegan(Touch* touch,SDL_Event* event)
{
	auto location = touch->getLocation();
	auto gameScene = GameScene::getInstance();
	auto gameState = gameScene->getGameState();
	
	if (gameState == GameState::Normal)
		gameScene->clickPath(location);

	return true;
}

void EventLayer::onTouchMoved(Touch* touch,SDL_Event* event)
{
}

void EventLayer::onTouchEnded(Touch* touch,SDL_Event* event)
{
}


即当游戏状态为GameState::Normal时,才进行事件转发。

然后,需要在GameScene中提供一些常用的脚本接口,以便于外界对lua的操作。

	//执行函数 参数和函数需要先入栈
	int executeFunction(int nargs, int nresults);
	//开始或者恢复协程 仅开始时需要函数入栈,其他恢复协程则只需要参数即可
	int resumeFunction(int nargs);
	
	int resumeLuaScript(WaitType waitType);
	//把对应类型的值放入栈顶,并返回其类型
	int getLuaGlobal(const char* name);
	//把t[k]放入栈顶,并返回其类型
	int getLuaField(int index, const char* key);

	lua_State* getLuaState() const { return m_pLuaState;}
	//参数入栈
	void pushInt(int intValue);
	void pushFloat(float floatValue);
	void pushBoolean(bool boolValue);
	void pushString(const char* stringValue);
	void pushNil();

	void pop(int n);

	bool toLuaBoolean(int index);
	const char* checkLuaString(int index);

上面的函数除了resumeLuaScript()外(此函数后面会提到),其余的都是GameScene类中简单调用了lua的函数(组合模式的弊端)。

int GameScene::executeFunction(int nargs, int nresults)
{
	int ret = LUA_ERRRUN;
	//检测-(nargs + 1)是否是函数
	if (lua_isfunction(m_pLuaState, -(nargs + 1)))
	{
		ret = lua_pcall(m_pLuaState, nargs, nresults,NULL);
	}
	else
	{
		lua_pushfstring(m_pLuaState, "the index isn't function %d", -(nargs + 1));
	}
	return ret;
}

int GameScene::resumeFunction(int nargs)
{
	int ret = LUA_ERRRUN;
	//检测是否存在足够的参数
	if (lua_gettop(m_pLuaState) >= nargs)
	{
		ret = lua_resume(m_pLuaState, nullptr, nargs);
		m_nCoroutineRet = ret;
	}
	else
	{
		lua_pushfstring(m_pLuaState, "the index isn't function %d", -(nargs + 1));
	}
	//当执行无误时应该返回LUA_OK或者是LUA_YIELD
	return ret;
}

上面两个函数的主要区别就是executeFunction()内部使用了lua_pcall(),而resumeFunction()内部则是使用了lua_resume()。lua_resume()类似于lua_pcall(),不过lua_resume()主要用于开始或者恢复协程,有关于lua的协程概念,详细的可以百度。推荐看下lua官方网站的wiki,好多疑问上面都可以找到解答。

int GameScene::getLuaGlobal(const char* name)
{
	return lua_getglobal(m_pLuaState, name);
}

int GameScene::getLuaField(int index, const char* key)
{
	return lua_getfield(m_pLuaState, index, key);
}

void GameScene::pushInt(int intValue)
{
	lua_pushinteger(m_pLuaState, (lua_Integer)intValue);
}

void GameScene::pushFloat(float floatValue)
{
	lua_pushnumber(m_pLuaState, (lua_Number)floatValue);
}

void GameScene::pushBoolean(bool boolValue)
{
	int value = boolValue ? 1 : 0;
	lua_pushboolean(m_pLuaState, value);
}

void GameScene::pushString(const char* stringValue)
{
	lua_pushstring(m_pLuaState, stringValue);
}

void GameScene::pushNil()
{
	lua_pushnil(m_pLuaState);
}

void GameScene::pop(int n)
{
	lua_pop(m_pLuaState, n);
}

bool GameScene::toLuaBoolean(int index)
{
	return lua_toboolean(m_pLuaState, index) == 1;
}

const char* GameScene::checkLuaString(int index)
{
	return luaL_checkstring(m_pLuaState, index);
}

上面的几个函数都是简单调用了lua提供的函数。

接着就是NPC。一般情况下,NPC是依附于地图的,即在地图创建的同时,也创建该地图对应的NPC;地图销毁则回收销毁对应的NPC。在本游戏中,每个地图文件(*.tmx)都是有着一个名为 "script layer"的对象层,脚本对象的属性如下:

chartlet 贴图名,当使用时需要在character.plist中注册,可为空
priority 优先级,同图块优先级
script 脚本文件路径,可为空
trigger 触发方式

优先级在StaticData.h中定义

/*优先级 */
#define PRIORITY_SAME 0 /*与人物能产生碰撞*/
#define PRIORITY_LOW  1 /*NPC显示在下*/
#define PRIORITY_HIGH 2 /*NPC显示在上*/

然后就是触发方式,这个在NonPlayerCharacter.h中有定义:

//脚本触发方式
enum class TriggerType
{
	None,
	Click,/*点击NPC*/
	Touch,/*触摸触发*/
	All,
};

Touch指的是当角色和NPC的碰撞面积交叉时,NPC会被触发,比如传送阵。

Click则指的是当玩家"点"到NPC时,之后主角移动到NPC面前,NPC会被触发,比如商人。

先看下NonPlayerCharacter.lua脚本:

NonPlayerCharacter = {};

function NonPlayerCharacter:new(o)
	o = o or {};

	setmetatable(o,self);
	self.__index = self;

	return o;
end
--初始化
function NonPlayerCharacter:initialize(id)
	self.id = id;
end
--playerId 触发的主角id
--返回值 为true则吞并事件
function NonPlayerCharacter:execute(playerId)
	return false;
end

function NonPlayerCharacter:update()
	-- body
end

function NonPlayerCharacter:clean()
	self = nil;
end

上面创建了一个包含了5个函数的table,也可以认为是类。它与c++中的NonPlayerCharacter类对应,每个NPC都有一个对应的table,当NPC对象创建时,对应的table也会创建,当NPC对象销毁时,对应的table也会销毁。

NonPlayerCharacter.h

#ifndef __NonPlayerCharacter_H__
#define __NonPlayerCharacter_H__
#include <string>
#include "lua/lua.h"
#include "Character.h"

using namespace std;
//脚本触发方式
enum class TriggerType
{
	None,
	Click,/*点击NPC*/
	Touch,/*触摸触发*/
	All,
};

class NonPlayerCharacter : public Character
{
	SDL_SYNTHESIZE(TriggerType, m_triggerType, TriggerType);
	SDL_BOOL_SYNTHESIZE(m_bDirty, Dirty);//是否是脏的,即是否要删除
	SDL_BOOL_SYNTHESIZE(m_bUpdate, Update);//是否调用update函数
	SDL_SYNTHESIZE_READONLY(int, m_nPriority, Priority);//优先级
private:
	string m_name;//脚本名称
	string m_filename;//脚本文件名
	Rect m_boundingBox;//包围盒
public:
	NonPlayerCharacter();
	~NonPlayerCharacter();

	static NonPlayerCharacter* create(const ValueMap& objects);

	bool init(const ValueMap& objects);

	//主角进行交互并执行对应函数 当返回值为true则吞并事件
	bool execute(int playerID);
	void update(float dt);
	void clean();
	//是否和对应矩形发生碰撞
	bool intersectRect(const Rect& rect);
	//是否能触发
	bool isTrigger(TriggerType triggerType);
	//是否能通过
	bool isPassing() const;
private:
	bool initialize();
	static TriggerType getTriggerTypeByString(const string& sType);
};
#endif

NPC继承自Character,即NPC可以有行走图。另外,initialize(),execute(),update(),clean()函数都是调用了对应table中的函数,把可扩展性留给脚本去实现。

bool NonPlayerCharacter::init(const ValueMap& objects)
{
	bool ret = false;

	float x = objects.at("x").asFloat();
	float y = objects.at("y").asFloat();
	float width = objects.at("width").asFloat();
	float height = objects.at("height").asFloat();
	//获取脚本名
	m_name = objects.at("name").asString();
	//获取对应属性
	const auto& properties = objects.at("properties").asValueMap();
	//获取文件名
	m_filename = properties.at("script").asString();
	m_nPriority = properties.at("priority").asInt();
	//动画名可能不存在
	if (properties.find("chartlet") != properties.end())
	{
		m_chartletName = properties.at("chartlet").asString();
	}
	if (properties.find("update") != properties.end())
	{
		m_bUpdate = properties.at("update").asBool();
	}
	auto triggerStr = properties.at("trigger").asString();
	m_triggerType = this->getTriggerTypeByString(triggerStr);
	//设置碰撞面积
	m_boundingBox.origin = Point(x,y);
	m_boundingBox.size = Size(width,height);

	int uniqueID = this->getUniqueID();
	//进行脚本的初始化
	ret |= this->initialize();
	auto chartletName = this->m_chartletName;
	//获取贴图
	auto size = m_boundingBox.size;
	auto origin = m_boundingBox.origin;
	//设置逻辑大小
	if (!chartletName.empty())
	{
		ret |= Character::init(chartletName);
	}
	else if (this->getSprite() == nullptr)
	{
		this->setContentSize(size);
	}
	this->setPosition(Point(origin.x + size.width/2,origin.y + size.height/2));

	return ret;
}

init函数中获取valueMap的相关内容,并且如果chartletName不空的话,还会接着调用Character的init函数。

bool NonPlayerCharacter::initialize()
{	
	auto gameScene = GameScene::getInstance();
	auto id = this->getUniqueID();
	//执行脚本
	int ret = LUA_OK; 
	if (!m_filename.empty())
	{
		gameScene->executeScriptFile(m_filename.c_str());
	}
	//脚本加载成功,调用对应的初始化函数
	if (ret == LUA_OK)
	{
		//获取函数
		gameScene->getLuaGlobal(m_name.c_str());
		gameScene->getLuaField(-1, "initialize");
		//放入参数
		gameScene->getLuaGlobal(m_name.c_str());
		gameScene->pushInt(id);
		//执行函数
		gameScene->executeFunction(2, 0);
		gameScene->pop(1);

		return true;
	}
	else
	{
		string errMsg = gameScene->checkLuaString(-1);
		gameScene->pop(1);

		printf("NonPlayerCharacter::init error:%s\n", errMsg.c_str());
	}

	return false;
}

initlize函数先尝试加载对应的脚本文件,默认加载成功,之后调用对应table的initialize函数。

bool NonPlayerCharacter::execute(int playerID)
{
	auto gameScene = GameScene::getInstance();

	bool ret = false;
	//获得函数
	gameScene->getLuaGlobal(m_name.c_str());
	gameScene->getLuaField(-1, "execute");
	//放入参数
	gameScene->getLuaGlobal(m_name.c_str());
	gameScene->pushInt(playerID);
	//执行函数
	gameScene->resumeFunction(2);
	//获取返回值
	ret = gameScene->toLuaBoolean(-1);
	gameScene->pop(2);

	return ret;
}

void NonPlayerCharacter::update(float dt)
{
	auto gameScene = GameScene::getInstance();
	//获得函数
	gameScene->getLuaGlobal(m_name.c_str());
	gameScene->getLuaField(-1, "update");
	//放入参数
	gameScene->getLuaGlobal(m_name.c_str());
	gameScene->pushFloat(dt);
	//执行函数
	gameScene->executeFunction(2,0);
	gameScene->pop(1);
}

void NonPlayerCharacter::clean()
{
	auto gameScene = GameScene::getInstance();

	if(gameScene != nullptr)
	{
		//获得函数
		gameScene->getLuaGlobal(m_name.c_str());
		gameScene->getLuaField(-1 ,"clean");
		//放入参数
		gameScene->getLuaGlobal(m_name.c_str());
		//执行函数
		gameScene->executeFunction(1,0);
		gameScene->pop(1);
	}
}

上面几个函数和initialize函数类似。另外,在使用lua函数的时候,应该注意对lua栈的维护,否则会导致栈满而报错。解决办法:要么每次使用lua函数时严格控制栈;要么在每帧调用的函数(如GameScene中的update)中进行调用lua_settop()进行清理(不推荐)。

bool NonPlayerCharacter::intersectRect(const Rect& rect)
{
	//查看对应table中是否存在字段range
	auto gameScene = GameScene::getInstance();

	gameScene->getLuaGlobal(m_name.c_str());
	int ret = gameScene->getLuaField(-1, "range");
	//不存在该字段,检测矩形碰撞
	if (ret == LUA_TNIL)
	{
		gameScene->pop(2);

		auto r = this->getBoundingBox();
		return r.intersectRect(rect);
	}
	//获取字段
	string range = gameScene->checkLuaString(-1);
	gameScene->pop(2);
	//全范围
	if (range == "all")
		return true;
	else
		return m_boundingBox.intersectRect(rect);
}

在检测矩形碰撞时做了一点点改动,这个函数会先检测对应table中是否有range这个字段,如果没有,则调用Rect的insectRect进行检测;如果有这个字段,则检测是否是"all",是则表示会发生碰撞,否则则会使用m_boundingBox进行碰撞检测,m_boundingBox中在init函数中进行赋值,其大小是tmx中的对应对象的大小。这个改动主要是为了以后的踩雷式遇敌做的准备。

bool NonPlayerCharacter::isTrigger(TriggerType triggerType)
{
	if (triggerType == TriggerType::All)
		return true;
	
	return this->m_triggerType == triggerType;
}

bool NonPlayerCharacter::isPassing() const
{
	return m_nPriority != PRIORITY_SAME;
}
TriggerType NonPlayerCharacter::getTriggerTypeByString(const string& sType)
{
	TriggerType type = TriggerType::None;

	if (sType == "Touch")
		type = TriggerType::Touch;
	else if (sType == "Click")
		type = TriggerType::Click;

	return type;
}

然后就是ScriptLayer类,ScriptLayer类似于PlayerLayer,在内部同样只是保存了对NPC的引用,NPC实际的"父亲"和PlayerLayer中的角色一样都是collisionLayer,这样NPC也能很方便地实现和主角同样的场景遮挡。

#ifndef __ScriptLayer_H__
#define __ScriptLayer_H__
#include <vector>
#include "SDL_Engine/SDL_Engine.h"
class NonPlayerCharacter;
enum class TriggerType;

using namespace std;
USING_NS_SDL;
/*脚本等待类型*/
enum class WaitType
{
	None,
	Time,
	Click,
	Button,
};

class ScriptLayer : public Layer
{
	SDL_SYNTHESIZE(WaitType, m_waitType, WaitType);//脚本等待类型
	SDL_SYNTHESIZE(float, m_waitTime, WaitTime);//等待时间
private:
	vector<NonPlayerCharacter*> m_npcList;
	//避免在脚本中进行地图切换时发生错误
	vector<NonPlayerCharacter*> m_npcListCopy;
public:
	ScriptLayer();
	~ScriptLayer();

	CREATE_FUNC(ScriptLayer);
	bool init();

	virtual void update(float dt);
	void updateWaitTime(float dt);
	/*触发事件,并返回是否可通过当前脚本事件*/
	bool checkAndTriggerScriptEvent(const Rect& boundingBox, int playerID, TriggerType type, int priority);
	//添加NPC
	void addNPC(NonPlayerCharacter* npc);
	//清除全部脚本事件
	void clear();
};
#endif

当添加NPC时,ScriptLayer对象会先引用该NPC,然后添加到容器m_npcListCopy中,之后在ScriptLayer::update()函数中把m_npcListCopy的项全部转移至m_npcList中。这样做的原因是为了避免直接添加从而迭代器失效导致运行错误(如checkAndTriggerScriptEvent中是在循环中进行检测碰撞和执行对应的脚本函数的)。

void ScriptLayer::update(float dt)
{
	for (auto it = m_npcListCopy.begin();it != m_npcListCopy.end();)
	{
		auto npc = *it;
		
		m_npcList.push_back(npc);

		it = m_npcListCopy.erase(it);
	}
	for (auto it = m_npcList.begin();it != m_npcList.end();)
	{
		auto script = *it;

		if (!script->isDirty())
		{
			//只有在应该更新的时候才会调用update函数
			if (script->isUpdate())
				script->update(dt);
			it++;
		}
		else//删除
		{
			it = m_npcList.erase(it);

			script->clean();
			script->removeFromParent();
			SDL_SAFE_RELEASE_NULL(script);
		}
	}
}

在update函数中会对npc进行转移,并且会检测更新以及删除操作。

void ScriptLayer::updateWaitTime(float dt)
{
	if (m_waitType == WaitType::Time)
	{
		m_waitTime -= dt;
		//回调函数
		if (m_waitTime <= 0.f)
		{
			m_waitTime = 0.f;
			GameScene::getInstance()->resumeLuaScript(m_waitType);
		}
	}
}

这个函数主要是为了实现脚本中的延时操作,配合lua中的协程进行实现,比如一个角色从(1,1)走到(3,3),此时的脚本因为有协程的存在而暂停,但是c++进行不可能暂停,又因为需要唤醒对应的协程,故需要一个计时器,除了WaitType::Time之外,还有Click和Button,Click对应的是点击操作,Button则是虚拟按钮。

int GameScene::resumeLuaScript(WaitType waitType)
{
	int nargs = 0;
	int ret = LUA_ERRRUN;

	if (waitType == WaitType::None)
		return ret;
	//需要提前添加参数入栈
	else if (waitType == WaitType::Button)
		nargs = 1;

	m_pScriptLayer->setWaitType(WaitType::None);
	//恢复协程
	ret = this->resumeFunction(nargs);

	return ret;
}

resumeLuaScript函数就是唤醒(resume 而不是start)协程,且WaitType::Button会传递一个参数,这个以后用到再详细说明。

bool ScriptLayer::checkAndTriggerScriptEvent(const Rect& boundingBox, int playerID, TriggerType type, int priority)
{
	bool bPassing = true;
	//是否吞并事件
	bool bSwallowed = false;

	for (auto it = m_npcList.begin();it != m_npcList.end();it++)
	{
		auto script = *it;

		//发生碰撞 脚本不同
		if (!script->isDirty() && script->getUniqueID() != playerID 
			&& script->intersectRect(boundingBox))
		{
			//触发条件相同 优先级相同 并且没有吞并事件 调用对应的脚本
			if (script->isTrigger(type) && (script->getPriority() & priority)
				&& !bSwallowed)
			{
				bSwallowed = script->execute(playerID);
			}
			//只有在触发条件和主角同级时才会判断是否会碰撞
			if (script->getPriority() == PRIORITY_SAME)
			{
				bPassing &= script->isPassing();
			}
		}
	}
	return bPassing;
}

检测并且触发脚本。有的脚本可能会吞并事件(避免同时有若干个NPC同时响应的bug),且会判断与角色是否发生碰撞。

void ScriptLayer::addNPC(NonPlayerCharacter* npc)
{
	SDL_SAFE_RETAIN(npc);
	m_npcListCopy.push_back(npc);
}

void ScriptLayer::clear()
{
	//滞后删除
	for (auto it = m_npcList.begin();it != m_npcList.end();it++)
	{
		auto script = *it;

		script->setDirty(true);
	}
}

在Game Scene类中添加

	ScriptLayer* m_pScriptLayer;

并在init函数中创建。

然后就是changeMap的改动。前面也说过NPC是依附于地图的,所以在切换地图时需要创建该地图对应的NPC。

void GameScene::changeMap(const string& mapName, const Point &tileCoodinate)
{
	//获取主角列表 并清除
	auto &characterList = m_pPlayerLayer->getCharacterList();

	for (auto character : characterList)
	{
		character->retain();
		character->removeFromParent();
	}
	//改变当前地图
	m_pMapLayer->clear();
	m_pMapLayer->init(mapName);
	//重新设置A星算法的大小
	StaticData::getInstance()->getAStar()->setMapSize(m_pMapLayer->getTiledMap()->getMapSize());
	//获取碰撞层
	auto collisionLayer = m_pMapLayer->getCollisionLayer();
	//设置主角位置
	auto tileSize = m_pMapLayer->getTiledMap()->getTileSize();
	auto pos = Point(tileSize.width * (tileCoodinate.x + 0.5f)
		,tileSize.height * (tileCoodinate.y + 0.5f));
	//添加主角
	for (unsigned int i = 0; i < characterList.size();i++)
	{
		auto character = characterList.at(i);
		//添加主角,并设置localZOrder
		collisionLayer->addChild(character,CHARACTER_LOCAL_Z_ORDER - i);

		character->setPosition(pos);
		character->release();
	}
	//改变当前中心点
	this->setViewpointCenter(pos);
	//删除脚本事件
	m_pScriptLayer->clear();
	//获取脚本对象
	auto scriptObjects = m_pMapLayer->getScriptObjectGroup();

	if (scriptObjects != nullptr)
	{
		ValueVector& vec = scriptObjects->getObjects();
		//生成npc
		for (const auto& object : vec)
		{
			const auto& valueMap = object.asValueMap();
			NonPlayerCharacter* npc = NonPlayerCharacter::create(valueMap);
			//添加到脚本事件层
			m_pScriptLayer->addNPC(npc);
			//遮挡处理
			int localZOrder = CHARACTER_LOCAL_Z_ORDER;

			if (npc->getPriority() == PRIORITY_LOW) localZOrder -= 1;
			else if (npc->getPriority() == PRIORITY_HIGH) localZOrder += 1;
			//添加到碰撞层
			collisionLayer->addChild(npc,localZOrder);
		}
	}
}	//删除脚本事件
	m_pScriptLayer->clear();
	//获取脚本对象
	auto scriptObjects = m_pMapLayer->getScriptObjectGroup();

	if (scriptObjects != nullptr)
	{
		ValueVector& vec = scriptObjects->getObjects();
		//生成npc
		for (const auto& object : vec)
		{
			const auto& valueMap = object.asValueMap();
			NonPlayerCharacter* npc = NonPlayerCharacter::create(valueMap);
			//添加到脚本事件层
			m_pScriptLayer->addNPC(npc);
			//遮挡处理
			int localZOrder = CHARACTER_LOCAL_Z_ORDER;

			if (npc->getPriority() == PRIORITY_LOW) localZOrder -= 1;
			else if (npc->getPriority() == PRIORITY_HIGH) localZOrder += 1;
			//添加到碰撞层
			collisionLayer->addChild(npc,localZOrder);
		}
	}
}

然后就是关于触发,目前先添加Touch的触发。

需要在initializeMapAndPlayer函数中添加:

	//添加角色移动结束触发器
	_eventDispatcher->addEventCustomListener(CHARACTER_MOVE_TO_TILE,
		SDL_CALLBACK_1(GameScene::moveToTile, this), this);

Character对象在每次到达一个图块中心时(A*算法得到的是一个路径数组,Character对象使用MoveTo+Callback来根据该数组行走,在Callback中会发送事件)都会发送一个用户事件,我们所做的就是捕获这个事件,然后在回调函数中进行对应的操作。

void GameScene::moveToTile(EventCustom* eventCustom)
{
	auto character = (Character*) eventCustom->getUserData();
	//获取主角
	auto player = m_pPlayerLayer->getPlayer();
	auto playerID = player->getUniqueID();
	//该角色不是主角,直接返回
	if (playerID != character->getUniqueID())
		return;

	auto rect = Rect(player->getPosition(),Size(1.f,1.f));
	//触发高低优先级的脚本事件
	m_pScriptLayer->checkAndTriggerScriptEvent(rect, playerID, TriggerType::Touch,
		PRIORITY_HIGH | PRIORITY_LOW);
}

getUniqueID()每个继承自Object的对象都有一个唯一的ID(cocos2d-x中没有该函数,但有_uniqueID,加上即可)。

moveToTile()会在任何角色移动到图块中心时回调,因此需要检测下是否是主角,然后再判断是否触发NPC。另外,只有PRIORITY_SAME才会检测碰撞,PRIORITY_LOW PRIORITY_HIGH则不会,它们影响的只是渲染顺序而已。

然后添加一个测试脚本:Map01_02.lua

require "NonPlayerCharacter"

--NPC 主角的妹妹
Map01_02_NPC01 = NonPlayerCharacter:new();

function Map01_02_NPC01:initialize(id)
	
	NonPlayerCharacter:initialize(id);
end

function Map01_02_NPC01:execute()
	-- 已经接受了任务
	--if LUA_isTaskAccepted("Map01_Task01_01") then
	--elseif LUA_isTaskSuccess("Map01_Task01_01") then
	--end
	return false;
end

--传送门 传送到1.tmx
Map01_02_Portal01 = NonPlayerCharacter:new();

function Map01_02_Portal01:execute(playerID)
	print("execute success");

	return false;
end

该脚本对应地图1_2.tmx。且该文件只会在Map01_02_Portal01对象加载一次。此时要注意tmx中创建对象的顺序,1_2.tmx的xml表示如下:

 <objectgroup name="script layer">
  <object id="1" name="Map01_02_Portal01" x="383" y="624" width="48" height="48">
   <properties>
    <property name="priority" type="int" value="1"/>
    <property name="script" value="script/Map01_02.lua"/>
    <property name="trigger" value="Touch"/>
   </properties>
  </object>
  <object id="4" name="Map01_02_NPC01" x="528" y="144" width="48" height="48">
   <properties>
    <property name="chartlet" value="moon"/>
    <property name="priority" type="int" value="0"/>
    <property name="script" value=""/>
    <property name="trigger" value="Click"/>
    <property name="update" type="bool" value="true"/>
   </properties>
  </object>
 </objectgroup>

目前共有两个对象,Map01_02_Protal01在前,故按照顺序,此对象是第一个加载的(可在GameScene中维护一个表来避免重复加载)。

---------------------------------------------------------分割线----------------------------------------------------------

先修改如下:

在Game Scene中添加成员:

	//保存已经执行过的文件
	vector<string> m_loaded;

然后就是:

//执行脚本文件 force是否强制执行该文件
	int executeScriptFile(const char* filename, bool force = false);

如果force为true,则无论m_loaded中是否存在,强制加载该文件。

int GameScene::executeScriptFile(const char* filename, bool force)
{
	bool bShouldLoad = false;
	auto it = find(m_loaded.begin(), m_loaded.end(), filename);
	//第一次,加载
	if (it == m_loaded.end())
	{
		bShouldLoad = true;
		m_loaded.push_back(filename);
	}
	else if (force)
		bShouldLoad = true;

	if (!bShouldLoad)
		return LUA_OK;

	FileUtils* utils = FileUtils::getInstance();
	string fullpath = utils->fullPathForFilename(filename);

	int ret = LUA_ERRRUN;
	unsigned int size = 0;
	unique_ptr<char> chunk = utils->getUniqueDataFromFile(fullpath, &size);
	//加载并执行
	if (chunk != nullptr)
	{
		const char* data = chunk.get();

		ret = luaL_loadbuffer(m_pLuaState, data, size, nullptr);
		if (ret == LUA_OK)
		{
			ret = lua_pcall(m_pLuaState, 0, 0, 0);
		}
		chunk.reset();
	}
	return ret;
}	bool bShouldLoad = false;
	auto it = find(m_loaded.begin(), m_loaded.end(), filename);
	//第一次,加载
	if (it == m_loaded.end())
	{
		bShouldLoad = true;
		m_loaded.push_back(filename);
	}
	else if (force)
		bShouldLoad = true;

	if (!bShouldLoad)
		return LUA_OK;

	FileUtils* utils = FileUtils::getInstance();
	string fullpath = utils->fullPathForFilename(filename);

	int ret = LUA_ERRRUN;
	unsigned int size = 0;
	unique_ptr<char> chunk = utils->getUniqueDataFromFile(fullpath, &size);
	//加载并执行
	if (chunk != nullptr)
	{
		const char* data = chunk.get();

		ret = luaL_loadbuffer(m_pLuaState, data, size, nullptr);
		if (ret == LUA_OK)
		{
			ret = lua_pcall(m_pLuaState, 0, 0, 0);
		}
		chunk.reset();
	}
	return ret;
}

简单的过滤,无法区别"Map01_02.lua"和"script/Map01_02.lua",不过目前倒是够用了。

猜你喜欢

转载自blog.csdn.net/bull521/article/details/79916599
今日推荐