前面的教程我们创建了一个坦克角色,并通过场景编辑器STAGE,将它加载到场景中,并添加了自带的地形,并调整坦克坐标使贴近地面。
本节将设计让坦克响应Space按下事件来启动引擎,并按 ↑ 让坦克移动起来,即当我们按下Space键的时候发送一个消息,然后让坦克接受消息并根据消息控制引擎的开启和关闭,然后按下 ↑ 键,并在tick事件中让坦克向前贴地移动。
游戏事件
1、简介
Delta3D事件本身只是简单的字符串标识,当某个事件发生后以消息的形式发出,每一个游戏事件都代表一个单独的行为,如发现了苹果,解救了人质等,我们常常混淆了游戏事件和游戏消息。
- 游戏事件是一个简单的GameEvent数据结构
- 游戏消息是用来承载GameEvent,然后通过消息发送器发送出去的
Delta3D提供了一个简单的事件管理层,是一个单例类:dtCore::GameEventManager,通过它,可在任何地方查询任何游戏事件。
2、发送事件的过程:
- 创建事件并将其注册到事件管理器
m_toggleEngineEvent = new dtCore::GameEvent("ToggleEngine");
dtCore::GameEventManager::GetInstance().AddEvent(*m_toggleEngineEvent);
此时已经创建了游戏事件,然后再通过游戏消息进行承载后发送给其他角色或组件。
- 创建游戏消息并发送
游戏消息是一种只有一个游戏事件作为参数的简单的消息类型。发送消息如下:
dtCore::RefPtr<dtGame::GameEventMessage> eventMsg;
this->GetGameManager()->GetMessageFactory().CreateMessage(dtGame::MessageType::INFO_GAME_EVENT, eventMsg);
eventMsg->SetGameEvent(*m_toggleEngineEvent);
this->GetGameManager()->SendMessage(*eventMsg);
以上代码通过游戏管理器的消息工厂创建了一个游戏消息,然后向该消息绑定事件,并通过游戏管理器发送该消息。
可激活体
1、简介
可以把可激活体想象成事件回调,它可以像属性一样被处理,即可激活体和方法调用的概念就像属性和数据成员的概念。
可激活体在角色代理被创建的时候创建,有名称,通过名称可对其进行访问。以下是创建可激活体的示例:
// 这是系统默认的可激活体,即父类定义的虚函数,子类继承可重写实现,用于系统消息的处理
void TankActor::ProcessMessage(const dtGame::Message& message)
{
const dtGame::GameEventMessage& eventMsg = static_cast<const dtGame::GameEventMessage&>(message);
if (eventMsg.GetGameEvent() != nullptr)
{
auto eventName = eventMsg.GetGameEvent()->GetName();
if (eventName == "ToggleEngine") // 启动引擎的消息
{
m_isEngineRunning = !m_isEngineRunning;
m_dust->SetEnabled(m_isEngineRunning); // m_dust在头文件中定义的粒子系统,dtCore::RefPtr<dtCore::ParticleSystem>,在OnEnteredWorld中创建
/*
m_dust = new dtCore::ParticleSystem;
m_dust->LoadFile("resources/Particles/dust.osg");
m_dust->SetEnabled(false);
AddChild(m_dust);
*/
}
else if (eventName == "SpeedBoost")
{
setVelocity(m_velocity + -5);
}
}
}
2、创建可激活体
为了创建自定义的可激活体,我们要创建自己的消息处理函数并把它封装到一个可激活体中,这个工作可以在游戏角色代理GameActorProxy的BuildInvokables()函数中完
成,BuildInvokables在游戏角色被创建时调用。
2.1 创建格式
void TankActorProxy::BuildInvokables()
{
dtActors::GameMeshActor::BuildInvokables();
TankActor* actor = this->GetDrawable<TankActor>();
// 创建自定义的可激活体
this->AddInvokable(*new dtGame::Invokable("MyInvokableName",
dtUtil::MakeFunctor(&TankActor::MyInvokableFunc, actor))); // MyInvokableFunc为自定义响应函数,原型为:void MyInvokable(const dtGame::Message& message);
}
2.2 注册可激活体
我们需要将可激活体注册到游戏管理器GameManager,一般在Proxy的OnEnterWorld()中完成这项工作,该函数会在角色被添加到游戏管理器中的时候被调用,创建和注册自定义的可激活体需要使用相同的名称,如下:
// TankActorProxy.cpp
void TankActorProxy::OnEnteredWorld()
{
// 注册自定义的可激活体
RegisterForMessages(MyGameMessageType::MyInvokableMessageType, "MyInvokableName");
// 注册消息有两种方式
/*
RegisterForMessageAboutOtherActor() 和 RegisterForMessageAboutSelf()
第二种可以只处理自己Actor发送的消息,即别的Actor关闭了引擎,不会影响自己
需要在创建事件时设置角色Id,eventMsg->setAboutActorId();
*/
// 以下为系统默认的可激活体注册方式
// 注册游戏消息,会自动发送到ProcessMessage可激活体
RegisterForMessages(dtGame::MessageType::INFO_GAME_EVENT);
// 注册TickLocal、TickRemote,只能选其中之一
if (!IsRemote())
{
RegisterForMessages(dtGame::MessageType::TICK_LOCAL, dtGame::GameActorProxy::TICK_LOCAL_INVOKABLE);
}
else
{
RegisterForMessages(dtGame::MessageType::TICK_REMOTE, dtGame::GameActorProxy::TICK_REMOTE_INVOKABLE);
}
}
注,自定义的可激活体的消息类型如果是系统内置的类型,查询dtGame::MessageType类中的定义即可,如果要自定义消息类型,则继承dtGame::MessageType并实现,dtGame::MessageType提供了方便的宏定义来创建自定义消息类型,如下:
// MyGameMessageType.h
#ifndef MyGameMessageType_h__
#define MyGameMessageType_h__
#include <dtGame/messagetype.h>
DT_DECLARE_MESSAGE_TYPE_CLASS_BEGIN(MyGameMessageType, DELTA3D_EXPORT)
static const MyGameMessageType MyInvokableMessageType;
DT_DECLARE_MESSAGE_TYPE_CLASS_END()
#endif // MyGameMessageType_h__
// MyGameMessageType.cpp
#include "MyGameMessageType.h"
DT_IMPLEMENT_MESSAGE_TYPE_CLASS(MyGameMessageType)
// 10000为自定义的消息类型标识,需要区分系统内置的消息类型标识
const MyGameMessageType MyGameMessageType::MyInvokableMessageType("InvokableMessageType","InvokableMessageType","InvokableMessageType Test.",10000, (dtGame::GameEventMessage*)(NULL));
只要在适当的地方,通过上述游戏事件中讲的事件发送后,就会触发绑定的MyInvokableFunc方法。
3、添加输入组件
现在我们已经有了一些行为来处理游戏事件消息,但编译后,并不会做任何事情,因为,并没有任何地方发送这些事件。我们需要响应我们的按键,并做出响应。
这里就需要一个组件,并且游戏管理器最主要是和消息、角色及组件打交道的。
我们定义一个自定义的捕获键盘和鼠标事件的组件,它继承自dtGame::BaseInputComponent,它本身知道如何捕获键盘和鼠标事件,我们要做的就是重载鼠标键盘消息响应函数并添加自定义的处理即可:
// MyInputComponent.h
#ifndef MyInputComponent_h__
#define MyInputComponent_h__
#include "delta3d.h"
class MyInputComponent : public dtGame::BaseInputComponent
{
public:
// 传递name参数,在任何地方可通过dtGame::GameManager::GetInstance(name)->GetComponentByName获取组件
MyInputComponent(const std::string& name);
// 处理键盘事件
virtual bool HandleKeyPressed(const dtCore::Keyboard* keyboard, int key) override;
protected:
~MyInputComponent();
private:
dtCore::RefPtr<dtCore::GameEvent> m_toggleEngineEvent;
dtCore::RefPtr<dtCore::GameEvent> m_speedBoost;
dtCore::RefPtr<dtCore::GameEvent> m_testInvokable;
void fireGameEvent(const dtCore::GameEvent& event, const dtGame::MessageType& messageType);
};
#endif // MyInputComponent_h__
// MyInputComponent.cpp
#include "MyInputComponent.h"
MyInputComponent::MyInputComponent(const std::string& name)
: dtGame::BaseInputComponent(name)
{
// 创建事件
m_toggleEngineEvent = new dtCore::GameEvent("TogglerEngine");
dtCore::GameEventManager::GetInstance().AddEvent(*m_toggleEngineEvent);
m_speedBoost = new dtCore::GameEvent("SpeedBoost");
dtCore::GameEventManager::GetInstance().AddEvent(*m_speedBoost);
m_testInvokable = new dtCore::GameEvent("MyInvokableName");
dtCore::GameEventManager::GetInstance().AddEvent(*m_testInvokable);
}
MyInputComponent::~MyInputComponent()
{
}
void MyInputComponent::fireGameEvent(const dtCore::GameEvent& event, const dtGame::MessageType& messageType)
{
// 创建事件消息对象
dtCore::RefPtr<dtGame::GameEventMessage> eventMsg;
// 创建事件消息
this->GetGameManager()->GetMessageFactory().CreateMessage(messageType, eventMsg);
// 设置事件到消息对象
eventMsg->SetGameEvent(event);
// 发送消息,编译发现winuser.h中有#define SendMessage,造成这里报错,如果你报错
/* 建议使用取消winuser.h的宏定义
#ifdef SendMessage
#undef SendMessage
#endif
*/
this->GetGameManager()->SendMessage(*eventMsg);
}
// 处理键盘按下事件
bool MyInputComponent::HandleKeyPressed(const dtCore::Keyboard* keyboard, int key)
{
bool handle = true; // false,表示事件继续传递
switch(key)
{
// 这里使用的是底层3rd osg库的枚举,建议复制到dtCore命名空间中,便于记忆
case osgGA::GUIEventAdapter::KEY_Return:
// 回车键加速
fireGameEvent(*m_speedBoost);
break;
case dtCore::KEY_Space:
// 空格启动或关闭引擎
fireGameEvent(*m_toggleEngineEvent);
break;
case dtCore::KEY_F9:
fireGameEvent(*m_testInvokable);
break;
default:
handle = false;
break;
}
if (!handle)
{
return BaseInputComponent::HandleKeyPressed(keyboard, key);
}
return handle;
}
为了处理键盘事件,需要将自定义的组件添加到游戏管理器中,一般在GameEntryPoint类中的OnStartup函数中添加,即本示例第8讲中的BlogTutorialGameEntryPoint类中添加以下代码:
// 添加输入处理组件
MyInputComponent* inputComponent = new MyInputComponent("MyInputComponent");
gamemanager.AddComponent(*inputComponent);
编译后,按下Space键,在TankActor的ProcessMessage中即可触发断点,并可看到界面上坦克下方出现灰尘效果,再按下Space后,灰尘消失。
按下自定义的F9按钮,也会触发到我们自定义的可激活体:
4、处理tick事件
下面来处理按下 ↑ 键时,让坦克向前移动。
这里先讲一下游戏循环,游戏循环是一个可以让游戏所有部分工作起来的无限循环,每一次单步循环,游戏的所有部分都要做某些工作,这里的一次循环就是一次tick。只要游戏在运行,游戏管理器就会发送TICK_LOCAL和TICK_REMOTE消息,对应需要tick的角色需要进行注册,这样TickLocal和TickRemote才会被调用。
一般在角色代理的OnEnterWorld()中对需要的消息进行注册。
// 注册TickLocal、TickRemote,只能选其中之一
if (!IsRemote())
{
RegisterForMessages(dtGame::MessageType::TICK_LOCAL, dtGame::GameActorProxy::TICK_LOCAL_INVOKABLE);
}
else
{
RegisterForMessages(dtGame::MessageType::TICK_REMOTE, dtGame::GameActorProxy::TICK_REMOTE_INVOKABLE);
}
然后再角色类中重载TickLocal和TickReomte方法来实现tick消息。
void HoverTankActor::OnTickLocal(const dtGame::TickMessage& tickMessage)
{
// 获取仿真时间,即距离上一次tick经过的时间
float deltaSimTime = tickMessage.GetDeltaSimTime();
// 计算速度和转向
computeVelocityAndTurn(deltaSimTime);
// 移动坦克
moveTank(deltaSimTime);
}
void HoverTankActor::OnTickRemote(const dtGame::TickMessage& tickMessage)
{
float deltaSimTime = tickMessage.GetDeltaSimTime();
// 不能计算速度和转向,我们不拥有远程对象
moveTank(deltaSimTime);
}
如果我们的帧率是10,仿真速度系数是1,deltaSimTime的值就接近0.1,如果帧率是300,仿真速度系数是1.5,deltaSimTime的值就接近0.00499,时间变化相差很大,但游戏不用关心,只要是基于deltaSimTime计算的,就不需要知道在不同帧率或仿真速率下的区别。
这里只处理了本地行为,本地行为就是我们自己拥有这个对象,在一个单人游戏中,通常你自己会在你的机器上拥有所有的对象,所以所有对象都是本地的,而在网络游戏中,你只是拥有你自己的对象,如玩家本身,因为你不拥有他们,你就不能对他们进行仿真驱动。
其他函数,如计算速度和转向率,移动坦克,较复杂,请阅读代码即可。
void HoverTankActor::moveTank(float deltaSimTime)
{
dtCore::Transform tx;
osg::Matrix mat;
osg::Quat q;
osg::Vec3 viewDir;
GetTransform(tx);
tx.GetRotation(mat);
mat.get(q);
viewDir = q * osg::Vec3(0,-1,0);
// translate the player along its current view direction based on current velocity
osg::Vec3 pos;
tx.GetTranslation(pos);
pos = pos + (viewDir*(m_velocity*deltaSimTime));
//particle fun
if (m_dust.valid() && m_isEngineRunning && m_velocity != 0)
{
// Get the layer we want
dtCore::ParticleLayer& pLayerToSet = *m_dust->GetSingleLayer("Layer 0");
// make a temp var for changing particle default template.
osgParticle::Particle& defaultParticle = pLayerToSet.GetParticleSystem().getDefaultParticleTemplate();
// do our funky changes
float lifetime = dtUtil::Max(2.0f, dtUtil::Abs(m_velocity+1) * 0.4f);
defaultParticle.setLifeTime(lifetime);
}
// attempt to ground clamp the actor so that he doesn't go through mountains.
osg::Vec3 intersection;
m_isector->Reset(); // dtCore::RefPtr<dtCore::Isector> m_isector = new dtCore::Isector();
m_isector->SetStartPosition(osg::Vec3(pos.x(),pos.y(),-10000));
m_isector->SetDirection(osg::Vec3(0,0,1));
if (m_isector->Update())
{
const dtCore::DeltaDrawable* hitActor = m_isector->GetClosestDeltaDrawable();
if (hitActor != this)
{
const osg::Vec3 p = m_isector->GetHitList()[0].getWorldIntersectPoint();
// make it hover
pos.z() = p.z() + 2.0f;
}
}
osg::Vec3 xyz = GetGameActorProxy().GetRotation();
xyz[2] += 360.0f * m_turnRate * deltaSimTime; // float m_turnRate = 0;
tx.SetTranslation(pos);
SetTransform(tx);
GetGameActorProxy().SetRotation(xyz);
}
void HoverTankActor::computeVelocityAndTurn(float deltaSimTime)
{
osg::Vec3 turnTurret;
// calculate current velocity
float decelDirection = (m_velocity >= 0.0) ? -1.0f : 1.0f;
float accelDirection = 0.0f;
float acceleration = 0.0;
dtCore::Keyboard* keyboard = GetGameActorProxy().GetGameManager()->GetApplication().GetKeyboard();
// which way is the user trying to go?
if (keyboard->GetKeyState('i'))
{
accelDirection = -1.0f;
}
else if (keyboard->GetKeyState('k'))
{
accelDirection = 1.0f;
}
// speed up based on user and current speed (ie, too fast)
if (m_isEngineRunning && accelDirection != 0.0f)
{
// boosted too fast, slow down
if ((accelDirection > 0 && m_velocity > MAXTANKVELOCITY) || // const float MAXTANKVELOCITY = 15.0f;
(accelDirection < 0 && m_velocity < -MAXTANKVELOCITY))
{
acceleration = deltaSimTime*(MAXTANKVELOCITY/3.0f)*decelDirection;
}
// hold speed
else if (m_velocity == accelDirection * MAXTANKVELOCITY)
{
acceleration = 0;
}
// speed up normally - woot!
else
{
acceleration = accelDirection*deltaSimTime*(MAXTANKVELOCITY/2.0f);
}
}
else if (m_velocity > -0.1 && m_velocity < 0.1)
{
acceleration = -m_velocity; // close enough to 0, so just stop
}
else // coast to stop
{
acceleration = deltaSimTime*(MAXTANKVELOCITY/6.0f)*decelDirection;
}
//std::cerr << "Ticking - deltaTime[" << deltaSimTime << "], acceleration [" << acceleration << "]" << std::endl;
setVelocity(m_velocity + acceleration);
if (m_isEngineRunning && keyboard->GetKeyState('l'))
{
setTurnRate(-0.1f);
}
else if (m_velocity && keyboard->GetKeyState('j'))
{
setTurnRate(0.1f);
}
else
{
setTurnRate(0.0f);
}
}
具体细节不再讨论,我们要注意的是,我们在每次滴答中直接访问了键盘事件,我们调用 keyboard.GetKeyState() 判断改变速度的按键 (‘I’ and ‘K’)和改变方向的按键(‘J’ and ‘L’) 是否被按下了,据此我们调用SetTurnRate()和 SetVelocity()设置旋转和速度. 很有意思,我们在输入组件MyInputComponent 中处理过键盘消息,这里我们又看到了另外一种处理键盘消息的方式,它可以直接作用到一个角色上,而不是组件。