This page has been translated automatically.
UNIGINE 基础课程
1. 简介
2. 虚拟世界管理
3. 3D模型准备
4. 材质
5. 摄像机和光照系统
6. 实现应用程序逻辑
7. 制作过场动画与动画序列
8. 准备发布项目
9. 物理系统
10. 优化基础
12. 项目3:第三人称越野街机赛车游戏。简介
13. 项目4:带有简单交互的 VR 应用程序

添加具有AI的敌人

Enemies are the important part of any shooter. We are going to create an enemy that moves around the scene chasing the player, starts firing at a certain distance from the player, and gets killed (deleted) if hit by the player's bullets.敌人是任何射击游戏的重要组成部分。我们将创建一个在场景中移动追逐玩家的敌人,在距离玩家一定距离时开始射击,如果被玩家子弹击中则会被消灭(删除)。

Before adding an enemy model, you should create it in a 3D modeling software.在添加敌人模型前,你需要使用3D建模软件创建它。

Find our ready-to-use robot_enemy.node enemy template in the data/fps/robot folder and place it in the scene.在我们的data/fps/robot文件夹中找到现成的robot_enemy.node敌人模板,并将其放入场景中。

Applying a Finite-State Machine for AI
应用有限状态机实现AI#

To be a strong opponent, your enemy must have a certain level of intelligence. A simple AI can be implemented using a Finite-State Machine — a concept allowing you to describe the logic in terms of states and transitions between them.要成为强大的对手,你的敌人需要具备一定程度的智能。可以使用有限状态机实现简单的AI——这个概念允许你用状态和状态之间的转换来描述逻辑。

For simplicity, consider three states: Idle, Chase, and Attack/Fire.为简化起见,考虑三种状态:空闲(Idle)、追逐(Chase)和攻击/开火(Attack/Fire)。

The following diagram describes what the enemy should do in each state, and how it will switch different states. The typical transitions would be from Idle to Chase, from Chase to Attack, and vice versa.以下图表描述了敌人在每个状态下的行为,以及它如何在不同状态间切换。典型的转换包括从空闲到追逐、从追逐到攻击,以及相反的转换。

Raycasts to Determine Visibility
使用射线检测确定可见性#

How will the enemy "see" us? This can be implemented with the help of raycast (Intersections), which we have already used to determine the bullet hits. The algorithm is simple: we shoot a ray from the enemy's location in the direction he is looking at, detect the first object intersected by the ray and check if it is the player. All this can be described using the following function:敌人如何"看到"我们?这可以通过我们已经用于确定子弹命中的射线检测(相交检测,Intersections)来实现。算法很简单:从敌人的位置向其视线方向发射一条射线,检测射线相交的第一个对象,并检查是否是玩家。所有这些可以用以下函数描述:

源代码 (C++)
bool isTargetVisible()
{
	Vec3 direction = (player->getWorldPosition() - intersectionSocket->getWorldPosition());
	Vec3 p0 = intersectionSocket->getWorldPosition();
	Vec3 p1 = p0 + direction;

	Unigine::ObjectPtr hitObject = World::getIntersection(p0, p1, playerIntersectionMask.get(), hitExcludes, hitInfo);
	if (!hitObject)
		return false;

	return player->getID() == hitObject->getID();
}

To implement transition between states in each frame, we are going to do the following:要在每一帧实现状态间的转换,我们将执行以下操作:

源代码 (C++)
void EnemyLogic::update()
{
	// 更新目标信息、路径和朝向
	updateTargetState();
	updateOrientation();
	updateRoute();

	// 敌人状态切换
	switch (currentState)
	{
		case EnemyLogicState::Idle: processIdleState(); break;
		case EnemyLogicState::Chase: processChaseState(); break;
		case EnemyLogicState::Attack: processAttackState(); break;
	}
}

void EnemyLogic::processIdleState()
{
	// 如果目标(玩家)可见,从空闲状态转换到追逐状态
	if (targetIsVisible)
		currentState = EnemyLogicState::Chase;
}

void EnemyLogic::processChaseState()
{
	// 重新计算方向和加速度坐标
  
	//  如果目标(玩家)不可见,从追逐状态转换到空闲状态
	if (!targetIsVisible)
		currentState = EnemyLogicState::Idle;

	// 检查距离,从追逐状态转换到攻击状态
	else if (lastSeenDistanceSqr < attackInnerRadius * attackInnerRadius)
	{
		currentState = EnemyLogicState::Attack;
		// 开始射击
	}

	// 接近目标
}

void EnemyLogic::processAttackState()
{
	// 检查距离,从攻击状态转换到追逐状态
	if (!targetIsVisible || lastSeenDistanceSqr > attackOuterRadius * attackOuterRadius)
	{
		currentState = EnemyLogicState::Chase;
		
		// 停止射击
	}
}

Using Navigation
使用导航系统#

Just seeing the object is not enough, one must also get within shooting distance (within the attack radius). The enemy should be able to chase the player correctly — build a route from its current position to the player's current position, without walking through obstacles or getting stuck halfway. To give the enemy additional knowledge about how it can navigate through the level, you can use navigation. The PathRoute class in the UNIGINE API is responsible for finding path on the plane or in three-dimensional space. Pathfinding is performed only within the Navigation Area, which can be either of the following two types:仅仅看到目标是不够的,敌人还需要进入射击距离(攻击半径范围内)。敌人应该能够正确地追逐玩家——从当前位置到玩家当前位置构建路线,避免穿过障碍物或卡在半路。要为敌人提供关于如何在关卡中导航的额外知识,可以使用导航系统。UNIGINE API中的PathRoute类负责在平面或三维空间中寻找路径。寻路仅在导航区域(Navigation Area)内执行,导航区域可以是以下两种类型之一:

  • Navigation Sector is used to search for a path both in three-dimensional space (a multi-story house, for example) and on the plane — in the sector projection area (in this case the Z coordinate is ignored). Sectors can be combined to build complex areas — a set of intersecting sectors forms a single navigation area.Navigation Sector(导航扇区):用于在三维空间(例如多层房屋)和平面(在此情况下忽略Z坐标)中搜索路径——在扇区投影区域内。扇区可以组合以构建复杂区域,一组相交扇区形成单个导航区域
  • Navigation Mesh is used for pathfinding only on the plane at a specified height above the mesh polygons — i.e. polygons in this case show where you can walk. Unlike sectors, Navigation Mesh is always on its own, i.e. you cannot create areas by combining several meshes or a mesh and sectors.Navigation Mesh(导航网格)仅用于在网格多边形指定高度平面上进行路径查找,即多边形在这种情况下显示可以行走的位置。与扇区不同,导航网格始终是独立的,即你不能通过组合多个网格或网格与扇区来创建区域

In our case, since our characters move in a relatively simple environment, we will use Navigation Mesh to define the navigation area.在我们的案例中,由于我们的角色在相对简单的环境中移动,我们将使用导航网格来定义Navigation Mesh

Such a mesh can be generated based on the FBX model of the scene using special tools, for example, RecastBlenderAddon.这样的网格可以基于场景的FBX模型使用特殊工具生成,例如RecastBlenderAddonRecastBlenderAddon

To place the mesh in the scene, click Create -> Navigation -> NavigationMesh in the Menu Bar and select the core/meshes/plain.mesh file. Align the mesh with the area to cover all areas where walking is allowed.要将网格放置在场景中,请点击菜单栏中的Create -> Navigation -> NavigationMesh(创建→导航→导航网格),然后选择core/meshes/plain.mesh文件。将网格与区域对齐以覆盖所有允许行走的区域。

In the Parameters window, set the Height of the navigation mesh to 3 for proper route calculation.Parameters(参数)窗口中,将导航网格的Height(高度)设置为3以进行正确的路线计算。

Now that we have a navigation area, we can start pathfinding. In the Chase state, our enemy, instead of rushing to the last visible position of the player along a straight line, will follow the path using the Navigation Mesh we added. The path consists of a queue of route points calculated using the functionality of the PathRoute class. It looks something like this:现在我们有了导航区域,可以开始路径查找。在追逐状态下,我们的敌人将使用我们添加的导航网格沿着路径前进,而不是沿着直线冲向玩家最后可见的位置。路径由使用PathRoute类功能计算的路线点队列组成。它看起来像这样:

源代码 (C++)
void EnemyLogic::updateRoute()
{
	if (Game::getTime() - lastCalculationTime < routeRecalculationInterval)
		return;

	if (shouldUpdateRoute)
	{
		// 计算到玩家的路径
		route->create2D(node->getWorldPosition(), lastSeenPosition, 1);
		
		shouldUpdateRoute = false;
	}

	// 如果路线已计算
	if (route->isReady())
	{
		// 检查是否到达路线的目标点
		if (route->isReached())
		{
			// 清除路线点队列
			calculatedRoute.clear();

			// 将所有根点添加到队列中
			for (int i = 1; i < route->getNumPoints(); ++i)
				calculatedRoute.append(route->getPoint(i));

			shouldUpdateRoute = true;
			lastCalculationTime = Game::getTime();
		}
		else
			// 如果未到达目标点,则重新计算路线
			shouldUpdateRoute = true;
	}
}

Teaching the Enemy to Shoot
让敌人学会射击#

After teaching the enemy to chase the player, we need to teach it to shoot. You don't want to strangle the player, do you?在教会敌人追逐玩家后,我们需要教会它射击。你不想让玩家轻易逃脱,对吧?

To implement the shooting ability, we need a bullet NodeReference that will be created at the moment of shooting when the robot is in the Attack state.要实现射击能力,我们需要一个子弹节点引用(NodeReference),当机器人在攻击状态时会创建它。

Let's add the shooting logic in the EnemyFireController component to make the robot shoot alternately from the left and right muzzle. The positions of their muzzles where bullets will be spawned are defined by the positions of two Dummy Nodes that are assigned to the Left Muzzle and Right Muzzle fields of the component.让我们在EnemyFireController组件中添加射击逻辑,使机器人从左、右枪管交替射击。子弹生成位置的枪管位置由两个虚拟节点(Dummy Node)定义,它们被分配给组件的Left MuzzleRight Muzzle字段。

  1. Create the EnemyFireController component and paste the following code into it:创建EnemyFireController组件并粘贴以下代码:

    EnemyFireController.h

    源代码 (C++)
    #pragma once
    #include <UnigineComponentSystem.h>
    #include <UnigineGame.h>
    #include <UnigineWorld.h>
    class EnemyFireController :
    	public Unigine::ComponentBase
    {
    public:
    	COMPONENT_DEFINE(EnemyFireController, Unigine::ComponentBase);
    	PROP_PARAM(Node, leftMuzzle, nullptr);
    	PROP_PARAM(Node, rightMuzzle, nullptr);
    
    	// 准星参数
    	PROP_PARAM(File, bulletPrefab, "");
    	PROP_PARAM(Float, shootInterval, 1.0f);
    
    	// 声明在程序执行序列各阶段调用的方法
    	COMPONENT_INIT(init);
    	COMPONENT_UPDATE(update);
    	void startFiring();
    	void stopFiring();
    
    protected:
    	float currentTime = 0.0f;
    	bool isLeft = false;
    	bool isFiring = false;
    	// 世界主循环重写方法
    	void init();
    	void update();
    };

    EnemyFireController.cpp

    源代码 (C++)
    #include "EnemyFireController.h"
    REGISTER_COMPONENT(EnemyFireController);
    using namespace Unigine;
    using namespace Math;
    
    void EnemyFireController::startFiring()
    {
    	isFiring = true;
    }
    
    void EnemyFireController::stopFiring()
    {
    	isFiring = false;
    }
    
    void EnemyFireController::init()
    {
    	// 重置计时器
    	currentTime = 0.0f;
    	// 切换为右枪管射击
    	isLeft = false;
    }
    
    void EnemyFireController::update()
    {
    	// 如果机器人不处于攻击状态(空闲或追逐),则不执行任何操作
    	if (!isFiring || bulletPrefab.nullCheck())
    		return;
    
    	// 更新计时器
    	currentTime += Game::getIFps();
    
    	// 检查射击间隔
    	if (currentTime > shootInterval)
    	{
    		// 重置计时器
    		currentTime -= shootInterval;
    		// 从bulletPrefab指定的资源创建子弹
    		NodePtr bullet = World::loadNode(Unigine::FileSystem::guidToPath(FileSystem::getGUID(bulletPrefab.getRaw())));
    
    		// 根据射击侧设置子弹位置
    		bullet->setWorldTransform((isLeft) ? leftMuzzle->getWorldTransform() : rightMuzzle->getWorldTransform());
    		// 切换枪管用于下次射击
    		isLeft = !isLeft;
    	}
    }

    Save all the files that we modified and then build and run the application by hitting Ctrl + F5 to make the Component System update properties used to assign the components to nodes. Close the application after running it and switch to UnigineEditor.保存文件,然后按Ctrl + F5构建并运行应用程序,使组件系统生成用于将组件分配给节点的属性文件。运行应用程序后将其关闭,然后切换到UnigineEditor。

  2. If necessary, enable editing of the robot_enemy node and assign the EnemyFireController component to the robot_root Dummy Object.如需编辑robot_enemy节点,将EnemyFireController组件分配给robot_root (Dummy Object)
  3. Drag and drop the LeftGunMuzzle and RightGunMuzzle Dummy Nodes to the corresponding fields of the EnemyFireController component.LeftGunMuzzleRightGunMuzzle虚拟节点拖放到EnemyFireController组件的对应字段。

  4. Drag and drop data/fps/bullet/bullet.node to the Bullet Prefab field.data/fps/bullet/bullet.node拖放到Bullet Prefab字段。

The right gun muzzle is selected已选择右枪管作为初始射击点

After spawning, the bullet should move in the appropriate direction changing its position in the world. If the bullet intersects with an object, a hit effect should be spawned at the point of impact. And if this object can take damage (i.e., it has a Health component, we'll do that a bit later), its health should be decreased by a certain value. Also, you can make the bullet apply an impulse to physical objects.子弹生成后,应沿正确方向移动并改变其在世界中的位置。如果子弹与物体相交,应在碰撞点生成命中效果。如果该物体可以受到伤害(即有Health组件,这个我们稍后会实现),则应减少其一定数值的生命值。此外,你还可以让子弹对物理对象施加冲量。

  1. Add the data/fps/bullet/bullet.node asset to the scene.data/fps/bullet/bullet.node资源添加到场景。
  2. Create the Bullet component and copy the following code:创建Bullet组件并复制以下代码:

    Bullet.h

    源代码 (C++)
    #pragma once
    #include <UnigineComponentSystem.h>
    #include <UnigineWorld.h>
    class Bullet :
    	public Unigine::ComponentBase
    {
    public:
    	COMPONENT_DEFINE(Bullet, Unigine::ComponentBase);
    
    	PROP_PARAM(File, hitPrefab, "");
    	PROP_PARAM(Float, speed, 10.0f);
    	PROP_PARAM(Int, damage, 1);
    
    
    	PROP_PARAM(Mask, intersectionMask, ~0);
    
    	// 声明在程序执行序列各阶段调用的方法
    	COMPONENT_UPDATE(update);
    
    protected:
    	Unigine::WorldIntersectionNormalPtr hitInfo = Unigine::WorldIntersectionNormal::create();
    	// 世界主循环重写方法
    	void update();
    };

    Bullet.cpp

    源代码 (C++)
    #include "Bullet.h"
    #include "PlayerLogic.h"
    #include <UnigineGame.h>
    REGISTER_COMPONENT(Bullet);
    using namespace Unigine;
    using namespace Math;
    
    void Bullet::update()
    {
    	// 设置子弹当前位置
    	Vec3 currentPosition = node->getWorldPosition();
    	// 设置子弹沿Y轴移动的方向
    	vec3 currentDirection = node->getWorldDirection(Math::AXIS_Y);
    
    	// 根据设定速度沿弹道更新子弹位置
    	node->setWorldPosition(node->getWorldPosition() + currentDirection * speed * Game::getIFps());
    
    	// 检测子弹弹道与物体的碰撞
    	Unigine::ObjectPtr hitObject = World::getIntersection(currentPosition, node->getWorldPosition(), intersectionMask, hitInfo);
    
    	// 如果未检测到碰撞,不执行任何操作
    	if (!hitObject)
    		return;
    
    	// 否则加载命中效果的NodeReference
    	NodePtr hitEffect = World::loadNode(Unigine::FileSystem::guidToPath(FileSystem::getGUID(hitPrefab.getRaw())));
    	// 将NodeReference设置到碰撞点并根据碰撞法线设置方向
    	hitEffect->setParent(hitObject);
    	hitEffect->setWorldPosition(hitInfo->getPoint());
    	hitEffect->setWorldDirection(hitInfo->getNormal(), vec3_up, Math::AXIS_Y);
    
    	// 删除子弹对象
    	node.deleteLater();
    }

    Save all the files that we modified and then build and run the application by hitting Ctrl + F5 to make the Component System update properties used to assign the components to nodes. Close the application after running it and switch to UnigineEditor.保存文件,然后按Ctrl + F5构建并运行应用程序,使组件系统生成用于将组件分配给节点的属性文件。运行应用程序后将其关闭,然后切换到UnigineEditor。

  3. Enable editing of the bullet node and assign the bullet component to its Static Mesh node.启用bullet节点的编辑功能,并将bullet组件分配至其静态网格(Static Mesh)节点。
  4. Drag data/fps/bullet/bullet_hit.node to the Hit Prefab field.data/fps/bullet/bullet_hit.node拖拽至Hit Prefab字段。

  5. Assign the LifeTime component to the bullet (Static Mesh) node and set its Life Time value to 5 seconds.bullet静态网格(Static Mesh)添加LifeTime组件,并将其Life Time值设为5秒。
  6. Select the bullet Node Reference and click Apply to save changes and remove the bullet node from the scene.选中bullet节点引用(Node Reference),点击Apply保存更改并将bullet节点移出场景。

Putting All Together
整体整合#

Now summarizing the above, let's create the EnemyLogic component with the following code:综合上述内容,现在创建EnemyLogic组件并填入以下代码:

EnemyLogic.h

源代码 (C++)
#pragma once
#include <UnigineComponentSystem.h>
#include <UniginePathFinding.h>
#include <UnigineVisualizer.h>
#include "EnemyFireController.h"

// 声明敌人状态
 
class EnemyLogic :
	public Unigine::ComponentBase
{
public:
	COMPONENT_DEFINE(EnemyLogic, Unigine::ComponentBase);

	enum EnemyLogicState
	{
		Idle,
		Chase,
		Attack,
	};

	PROP_PARAM(Node, player, nullptr);
	PROP_PARAM(Node, intersectionSocket, nullptr);
	PROP_PARAM(Mask, playerIntersectionMask, ~0);

	PROP_PARAM(File, hitPrefab, "");
	PROP_PARAM(Float, reachRadius, 0.5);
	PROP_PARAM(Float, attackInnerRadius, 5.0f);
	PROP_PARAM(Float, attackOuterRadius, 7.0f);
	PROP_PARAM(Float, speed, 1.0f);
	PROP_PARAM(Float, rotationStiffness, 8);
	PROP_PARAM(Float, routeRecalculationInterval, 3.0f);

	PROP_PARAM(Int, damage, 1);

	PROP_PARAM(Mask, intersectionMask, ~0);

	// 声明在程序执行序列各阶段调用的方法
	COMPONENT_INIT(init);
	COMPONENT_UPDATE(update);

protected:
	// 世界主循环重写方法
	void init();
	void update();

	bool isTargetVisible();
	void updateRoute();
	void updateTargetState();
	void updateOrientation();
	void processIdleState();
	void processChaseState();
	void processAttackState();
private:
	// 初始化敌人状态
	EnemyLogicState currentState = EnemyLogicState::Idle;

	bool targetIsVisible;
	Unigine::Math::Vec3 lastSeenPosition;
	Unigine::Math::vec3 lastSeenDirection;
	float lastSeenDistanceSqr;

	Unigine::BodyRigidPtr bodyRigid = nullptr;
	Unigine::WorldIntersectionPtr hitInfo = Unigine::WorldIntersection::create();
	Unigine::Vector<Unigine::NodePtr> hitExcludes;

	EnemyFireController *fireController = nullptr;
	// 创建路径点队列
	Unigine::Vector<Unigine::Math::Vec3> calculatedRoute;
	Unigine::PathRoutePtr route = Unigine::PathRoute::create();
	bool shouldUpdateRoute = true;
	float lastCalculationTime = 0.0f;
};

EnemyLogic.cpp

源代码 (C++)
#include "EnemyLogic.h"
REGISTER_COMPONENT(EnemyLogic);
using namespace Unigine;
using namespace Math;


bool EnemyLogic::isTargetVisible()
{
	Vec3 direction = (player->getWorldPosition() - intersectionSocket->getWorldPosition());
	Vec3 p0 = intersectionSocket->getWorldPosition();
	Vec3 p1 = p0 + direction;
	
	Unigine::ObjectPtr hitObject = World::getIntersection(p0, p1, playerIntersectionMask.get(), hitExcludes, hitInfo);
	if (!hitObject)
		return false;

	return player->getID() == hitObject->getID();
}

void EnemyLogic::updateRoute()
{
	if (Game::getTime() - lastCalculationTime < routeRecalculationInterval)
		return;

	if (shouldUpdateRoute)
	{
		// 计算到玩家的路径
		route->create2D(node->getWorldPosition(), lastSeenPosition, 1);
		
		shouldUpdateRoute = false;
	}

	// 如果路线已计算
	if (route->isReady())
	{
		// 检查是否到达路线的目标点
		if (route->isReached())
		{
			// 清除路线点队列
			calculatedRoute.clear();

			// 将所有根点添加到队列中
			for (int i = 1; i < route->getNumPoints(); ++i)
				calculatedRoute.append(route->getPoint(i));

			shouldUpdateRoute = true;
			lastCalculationTime = Game::getTime();
		}
		else
			// 如果未到达目标点,则重新计算路线
			shouldUpdateRoute = true;
	}
}

void EnemyLogic::updateTargetState()
{
	// 刷新当前可见状态
	targetIsVisible = isTargetVisible();

	// 如果玩家可见,记住最后记录的位置
	if (targetIsVisible)
		lastSeenPosition = player->getWorldPosition();

	lastSeenDirection = (vec3)(lastSeenPosition - node->getWorldPosition());
		lastSeenDistanceSqr = lastSeenDirection.length2();
	lastSeenDirection.normalize();
}

void EnemyLogic::updateOrientation()
{
	vec3 direction = lastSeenDirection;
	direction.z = 0.0f;

	quat targetRotation = quat(Math::setTo(vec3_zero, direction.normalize(), vec3_up, Math::AXIS_Y));
	quat currentRotation = node->getWorldRotation();

	currentRotation = Math::slerp(currentRotation, targetRotation, Game::getIFps() * rotationStiffness);
	node->setWorldRotation(currentRotation);
}

void EnemyLogic::processIdleState()
{
	// 如果目标(玩家)可见,从空闲状态转换到追逐状态
	if (targetIsVisible)
		currentState = EnemyLogicState::Chase;
}

void EnemyLogic::processChaseState()
{
	vec3 currentVelocity = bodyRigid->getLinearVelocity();
	currentVelocity.x = 0.0f;
	currentVelocity.y = 0.0f;
	if (calculatedRoute.size() > 0)
	{
		float distanceToTargetSqr = (calculatedRoute.first() - node->getWorldPosition()).length2();

		bool targetReached = (distanceToTargetSqr < reachRadius* reachRadius);
		if (targetReached)
			calculatedRoute.removeFirst();

		if (calculatedRoute.size() > 0)
		{
			vec3 direction = calculatedRoute.first() - node->getWorldPosition();
			direction.z = 0.0f;
			direction.normalize();
			currentVelocity.x = direction.x * speed;
			currentVelocity.y = direction.y * speed;
		}
	}

	// 如果目标(玩家)不可见,从追逐状态转换到空闲状态
	if (!targetIsVisible)
		currentState = EnemyLogicState::Idle;

	// 检查距离,从追逐状态转换到攻击状态
	else if (lastSeenDistanceSqr < attackInnerRadius * attackInnerRadius)
	{
		currentState = EnemyLogicState::Attack;
		currentVelocity.x = 0.0f;
		currentVelocity.y = 0.0f;
		// 开始射击
		if (fireController)
			fireController->startFiring();
	}

	bodyRigid->setLinearVelocity(currentVelocity);
}

void EnemyLogic::processAttackState()
{
	// 检查距离,从攻击状态转换到追逐状态
	if (!targetIsVisible || lastSeenDistanceSqr > attackOuterRadius * attackOuterRadius)
	{
		currentState = EnemyLogicState::Chase;
		// 停止射击
		if (fireController)
			fireController->stopFiring();
	}
}


void EnemyLogic::init()
{
	// 初始化导航网格内沿路径移动点的参数
	route->setRadius(0.0f);
	route->setHeight(1.0f);
	route->setMaxAngle(0.5f);

	bodyRigid = node->getObjectBodyRigid();
	hitExcludes.append(node);
	hitExcludes.append(node->getChild(0));

	targetIsVisible = false;
	// 获取EnemyFireController组件
	fireController = ComponentSystem::get()->getComponent<EnemyFireController>(node);
	
	shouldUpdateRoute = true;
	lastCalculationTime = Game::getTime();
}

void EnemyLogic::update()
{
	updateTargetState();
	updateOrientation();
	updateRoute();

	// 敌人状态切换
	switch (currentState)
	{
		case EnemyLogicState::Idle: processIdleState(); break;
		case EnemyLogicState::Chase: processChaseState(); break;
		case EnemyLogicState::Attack: processAttackState(); break;
	}

	// 根据当前状态改变颜色
	vec4 color = vec4_black;
	switch (currentState)
	{
		case EnemyLogicState::Idle: color = vec4_blue; break;
		case EnemyLogicState::Chase: color = vec4(1.0f, 1.0f, 0.0f, 1.0f); break;
		case EnemyLogicState::Attack: color = vec4_red; break;
	}

	// 可视化敌人状态
	Visualizer::renderPoint3D(node->getWorldPosition() + vec3_up * 2.0f, 0.25f, color);
	Visualizer::renderPoint3D(node->getWorldPosition() + vec3_up * 3.0f, 0.25f, isTargetVisible() ? vec4_green : vec4_red);
	Visualizer::renderPoint3D(lastSeenPosition, 0.1f, vec4(1.0f, 0.0f, 1.0f, 1.0f));


	// 可视化攻击半径
	Visualizer::renderSphere(attackInnerRadius, node->getWorldTransform(), vec4_red);
	Visualizer::renderSphere(attackOuterRadius, node->getWorldTransform(), vec4_red);

	// 可视化路径点
	for(Vec3 route_point: calculatedRoute)
		Visualizer::renderPoint3D(route_point + vec3_up, 0.25f, vec4_black);
}
  1. Save all the files that we modified and then build and run the application by hitting Ctrl + F5 to make the Component System update properties used to assign the components to nodes. Close the application after running it and switch to UnigineEditor.保存文件,然后按Ctrl + F5构建并运行应用程序,使组件系统生成用于将组件分配给节点的属性文件。运行应用程序后将其关闭,然后切换到UnigineEditor。

  2. Enable editing of the robot_enemy node and assign the new component to the robot_root Dummy Node in the Parameters window.启用robot_enemy节点的编辑功能,在参数窗口中将新组件分配给robot_root Dummy Node(虚拟节点)。

  3. Drag and drop the player_hit_box node to the Player field of the EnemyLogic component. This node imitates the player body and is used in calculations. Make sure that the Intersection option is checked for player_hit_box.player_hit_box节点拖放到EnemyLogic组件的Player字段。该节点模拟玩家身体并用于计算。确保player_hit_box已勾选Intersection选项。
  4. Drag and drop the robot_intersection_socket node of the robot_enemy node to Intersection Socket field. This is the node from which the robot will do intersection checks.robot_enemy节点下的robot_intersection_socket节点拖放到Intersection Socket字段。这是机器人用于进行相交检测的节点。

For debugging, you can enable Visualizer that will display the inner and outer attack radius, as well as the colored squares above the robot indicating:调试时,你可以启用Visualizer,它将显示内攻击半径和外攻击半径,以及机器人上方的彩色方块指示:

  • The state of the robot: Idle — BLUE, Chase — YELLOW, Attack — RED.机器人状态:空闲--蓝色,追逐--黄色,攻击--红色
  • If the target is visible: Yes — GREEN, No — RED.目标是否可见:是--绿色,否--红色

And the points of the calculated path:以及计算路径的路径点:

本页面上的信息适用于 UNIGINE 2.20 SDK.

最新更新: 2025-06-20
Build: ()