前面的教程我们创建了一个坦克角色,并通过场景编辑器STAGE,将它加载到场景中,并添加了自带的地形,并调整坦克坐标使贴近地面。
本节将设计让坦克响应Space按下事件来启动引擎,并按 ↑ 让坦克移动起来,即当我们按下Space键的时候发送一个消息,然后让坦克接受消息并根据消息控制引擎的开启和关闭,然后按下 ↑ 键,并在tick事件中让坦克向前贴地移动。

游戏事件

1、简介


Delta3D事件本身只是简单的字符串标识,当某个事件发生后以消息的形式发出,每一个游戏事件都代表一个单独的行为,如发现了苹果,解救了人质等,我们常常混淆了游戏事件和游戏消息。

  • 游戏事件是一个简单的GameEvent数据结构
  • 游戏消息是用来承载GameEvent,然后通过消息发送器发送出去的

Delta3D提供了一个简单的事件管理层,是一个单例类:dtCore::GameEventManager,通过它,可在任何地方查询任何游戏事件。

2、发送事件的过程:


  1. 创建事件并将其注册到事件管理器
m_toggleEngineEvent = new dtCore::GameEvent("ToggleEngine");
dtCore::GameEventManager::GetInstance().AddEvent(*m_toggleEngineEvent);

此时已经创建了游戏事件,然后再通过游戏消息进行承载后发送给其他角色或组件。

  1. 创建游戏消息并发送
    游戏消息是一种只有一个游戏事件作为参数的简单的消息类型。发送消息如下:
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 velocityosg::Vec3 pos;tx.GetTranslation(pos);pos = pos + (viewDir*(m_velocity*deltaSimTime));//particle funif (m_dust.valid() && m_isEngineRunning && m_velocity != 0){// Get the layer we wantdtCore::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 changesfloat 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 hoverpos.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 velocityfloat 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 downif ((accelDirection > 0 && m_velocity > MAXTANKVELOCITY) ||  // const float MAXTANKVELOCITY = 15.0f;(accelDirection < 0 && m_velocity < -MAXTANKVELOCITY)){acceleration = deltaSimTime*(MAXTANKVELOCITY/3.0f)*decelDirection;}// hold speedelse 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 中处理过键盘消息,这里我们又看到了另外一种处理键盘消息的方式,它可以直接作用到一个角色上,而不是组件。

Delta3D(9)教程:添加消息发送和可激活体相关推荐

  1. ActiveMQ添加商品发送消息

    添加商品 需要同步索引库,同步缓存,生成静态页面等等 很多地方,都需要监听添加商品的事件,所以,这里使用Topic模式 方案选择 方案一:直接写业务逻辑 在商品服务模块,添加商品的业务逻辑中,添加同步 ...

  2. 最近发现系统rabbitmq丢消息比较严重,于是想了些方案来查找原因,给将消息发送方式添加确认机制。 我们在本地模拟了wms发送打标消息的场景. 1. 有事务 2. 先发点对点队列, 再发订

    最近发现系统rabbitmq丢消息比较严重,于是想了些方案来查找原因,给将消息发送方式添加确认机制. 我们在本地模拟了wms发送打标消息的场景. 1. 有事务 2. 先发点对点队列, 再发订阅队列 3 ...

  3. Kafka入门教程 Golang实现Kafka消息发送、接收

    一:核心概念 kafka是消息中间件的一种,是一种分布式流平台,是用于构建实时数据管道和流应用程序.具有横向扩展,容错,wicked fast(变态快)等优点. kafka中涉及的名词: 消息记录(r ...

  4. 传感器信号 如何发送到服务器,传感器如何将消息发送给云服务器

    传感器如何将消息发送给云服务器 内容精选 换一换 消息发送和消费的可靠性必须由DMS服务和生产者以及消费者协同工作才能保证.同时开发者需要尽量合理使用DMS消息队列,以提高消息发送和消息消费的效率与准 ...

  5. Springboot RabbitMQ 基础使用、消息发送确认、签收

    概述 rabbitMQ 会做一个系列,包括:安装.基础使用.高级队列.集群. 使用环境: jdk 8 .springboot 2.4.10 常见概念: AMQP:高级消息队列协议,这是一个消息应用的规 ...

  6. go 实现 kafka 消息发送、接收

    引言 网络上关于 go 实现 kafka 消息发送和接收的文章很多,但是实际操作起来又不是很清楚,本文在网络资源的基础上,结合自己搭建过程中遇到的问题进行了总结. 本文的实验主机:Mac笔记本. 一. ...

  7. RabbitMQ消息发送和接收

    1.RabbitMQ的消息发送和接受机制 所有 MQ 产品从模型抽象上来说都是一样的过程: 消费者(consumer)订阅某个队列.生产者(producer)创建消息,然后发布到队列(queue)中, ...

  8. 计算器初步添加消息响应

    文章目录 1 计算器初步添加消息响应 1 计算器初步添加消息响应 只需要按照信号与槽的要求进行修改即可,修改后的代码如下(main.cpp未修改): QCalculatorUI.h: #ifndef ...

  9. 如何设计一门语言(五)——面向对象和消息发送

    面向对象这个抽象的特例总是有说不完的话题,更糟糕的是很多语言都错误地实现了面向对象--class居然可以当一个变量类型什么的这只是让人们写代码写的更糟糕而已.当然这个话题第三篇文章已经说过了,现在来谈 ...

最新文章

  1. 正则表达式中的\.表示什么意思
  2. 多行文本框拖动问题解决
  3. 文件上传服务器jvm调优,JVM性能调优解决方案(12页)-原创力文档
  4. Linux常用命令(echo、date、ls、cd、history、cat)
  5. 电脑屏保海底世界_一款电脑桌面锁屏软件:梦幻水族馆(好多鱼)
  6. 手把手教你安卓入门(一)
  7. linux sed在容器中怎么用,概述LFCS 基础之sed用法
  8. Android下载文件
  9. Extjs中给同一个GridPanel中的事件添加参数的方法
  10. 通信要学很多计算机课吗,辽宁科技学院通信工程专业要学哪些课程,好学吗?...
  11. 机器人学中的状态估计 中文版_《机器人学中的状态估计》-05偏差,匹配和外点...
  12. 如何过上简单的生活(转)
  13. 现网问题排查实战:Jstat,Jstack,Jmap
  14. win7免费升级win10官方工具
  15. STM32 PWM呼吸灯程序
  16. 【校招实习面试实战,身临其境】华为软件开发工程师面试复盘总结
  17. 8、某网络拓扑如图所示,路由器R1通过接口E1、E2分别连接局域网1、局域网2,通过接口L0连接路由器R2,并通过路由器R2连接域名服务器与互联网。R1的L0接口的IP地址是202.118.2.1/2
  18. 三位数除以两位数竖式计算没有余数_三位数除以两位数有余数竖式运算300题
  19. 罗斯蒙特214CRTSMB1S3M0420SLM2AR1C1B1TBM2温度传感器
  20. Oracle数据仓库参考架构

热门文章

  1. php 变量的md5加密,利用PHP脚本在Linux下用md5函数加密字符串的方法
  2. 秒懂Java之方法引用(method reference)详解
  3. access 英语什么意思_access是什么意思中文翻译
  4. 7-100 简单求阶乘问题 (10 分)本题要求编写程序,计算N的阶乘。输入格式:输入在一行中给出一个不超过12的正整数N。输出格式:在一行中输出阶乘的值。输入样例:4结尾无空行
  5. 华为机试2——比赛顺序
  6. 安徽二级计算机考试准考证打印入口
  7. matlab mrg32k3a,求教 – MATLAB中文论坛
  8. 恋爱指南----《强迫的爱》
  9. 关于qt.qpa.plugin: Could not load the Qt platform plugin “xcb“的问题
  10. AMiner学术搜索服务全新上线 | “一键”帮你搞定论文综述,带你研读AI相关Topic百篇经典论文