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

健康值与受击伤害

The player and enemies should have a health level that will decrease each time they are hit by a bullet. We'll store the health information in the Health component.玩家和敌人都应有一个生命值,每当他们被子弹击中时,该值会减少。我们将在组件 Health 中存储生命值信息。

  1. Create the Health component and copy the following code into it:创建一个名为 Health 的组件,并复制以下代码:

    Health.h

    源代码 (C++)
    #pragma once
    #include <UnigineComponentSystem.h>
    class Health :
    	public Unigine::ComponentBase
    {
    public:
    	COMPONENT_DEFINE(Health, Unigine::ComponentBase);
    
    	// 初始生命值
    	PROP_PARAM(Int, health, 5);
    
    	// 判断当前生命值是否为0
    	bool isDead() { return health <= 0; }
    
    	// 执行伤害逻辑
    	void takeDamage(int damage);
    };

    Health.cpp

    源代码 (C++)
    #include "Health.h"
    REGISTER_COMPONENT(Health);
    using namespace Unigine;
    using namespace Math;
    
    void Health::takeDamage(int damage)
    {
    	// 应用伤害
    	health = max(health - damage, 0);
    }

    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.保存所有已修改的文件,然后在 IDE 中按 Ctrl + F5 编译并启动应用程序,以便组件系统生成用于将组件绑定到节点的属性。启动后关闭应用并返回 UnigineEditor。

  2. Add it to the visuals node of the robot_enemy node.将组件添加至robot_enemy节点的visuals节点。

  3. Add it to the player_hit_box node of the player node.将组件添加至player节点的player_hit_box节点。
  4. In order to use the Health component logic, we need to modify number of components.为了使用Health组件的逻辑,我们需要修改若干组件。

    In WeaponController code, add the following several lines to the part detecting that the player has hit an object:WeaponController代码中,在检测玩家击中某个对象的部分添加以下几行代码:

    WeaponController.h

    源代码 (C++)
    #pragma once
    #include <UnigineComponentSystem.h>
    #include <UnigineVisualizer.h>
    #include "ShootInput.h"
    
    ${#HL}$ #include "VFXController.h" ${HL#}$
    class WeaponController :
    	public Unigine::ComponentBase
    {
    public:
    	COMPONENT_DEFINE(WeaponController, Unigine::ComponentBase);
    
    	PROP_PARAM(Node, shooting_camera, nullptr);
    	PROP_PARAM(Node, shoot_input_node, nullptr);
    
    	PROP_PARAM(Node, weapon_muzzle, nullptr);
    	PROP_PARAM(Node, vfx_node, nullptr);
    
    	VFXController* vfx;
    
    	Unigine::PlayerDummyPtr shootingCamera = nullptr;
    	ShootInput *shootInput = nullptr;
    ${#HL}$	int damage = 1; ${HL#}$
    
    	// 用于定义子弹可影响对象的Intersection掩码
    	int mask = ~0;
    
    	// 声明在执行序列相应阶段调用的方法
    	COMPONENT_INIT(init);
    	COMPONENT_UPDATE(update);
    
    	void shoot();
    
    protected:
    	// 重写世界主循环的方法
    	void init();
    	void update();
    };

    WeaponController.cpp

    源代码 (C++)
    #include "WeaponController.h"
    ${#HL}$
    #include "Health.h" ${HL#}$
    REGISTER_COMPONENT(WeaponController);
    using namespace Unigine;
    using namespace Math;
    
    void WeaponController::shoot()
    {
    	if (weapon_muzzle)
    		vfx->onShoot(weapon_muzzle->getWorldTransform());
    
    	// 将线段的起点 (p0) 设置为摄像机的位置,终点 (p1) 设置为摄像机视角方向上 100 个单位远的位置
    	Vec3 p0 = shootingCamera->getWorldPosition();
    	Vec3 p1 = shootingCamera->getWorldPosition() + (Vec3)shootingCamera->getWorldDirection() * 100;
    
    	// 创建一个用于存储交叉点法线的对象
    	WorldIntersectionNormalPtr hitInfo = WorldIntersectionNormal::create();
    	// 获取射线(p0, p1)与第一个对象的交点
    	Unigine::ObjectPtr hitObject = World::getIntersection(p0, p1, mask, hitInfo);
    	// 如果找到了交点
    	if (hitObject)
    	{
    		// 使用 Visualizer 在交点处渲染法线向量
    		Visualizer::renderVector(hitInfo->getPoint(), hitInfo->getPoint() + (Vec3)hitInfo->getNormal(), vec4_red, 0.25f, false, 2.0f);
    
    		// 在交点处渲染击中效果
    		vfx->onHit(hitInfo->getPoint(), hitInfo->getNormal(), hitObject);
    ${#HL}$
    		// 应用伤害
    		Health *health = ComponentSystem::get()->getComponent<Health>(hitObject);
    		if (health)
    			health->takeDamage(damage); ${HL#}$
    	}
    }
    
    void WeaponController::init()
    {
    	// 获取分配了 ShootInput 组件的摄像机
    	shootingCamera = checked_ptr_cast<Unigine::PlayerDummy>(shooting_camera.get());
    
    	// 从 'shoot_input_node' 节点获取 ShootInput 组件
    	shootInput = ComponentSystem::get()->getComponent<ShootInput>(shoot_input_node.get());
    
    	// 从 'vfx_node' 节点获取 VFXController 组件
    	vfx = Unigine::ComponentSystem::get()->getComponent<VFXController>(vfx_node.get());
    }
    
    void WeaponController::update()
    {
    	// 处理用户输入:检查是否按下了“开火”按钮
    	if (shootInput->isShooting())
    		shoot();
    }

    In Bullet.cpp, let's add several lines after detecting that the player has been hit and just before removing the bullet in order to apply damage to the character and update the health information:Bullet.cpp中,在检测到玩家被击中并即将删除子弹之前,添加几行代码以对角色造成伤害并更新生命值信息:

    Bullet.cpp

    源代码 (C++)
    #include "Bullet.h"
    #include "PlayerLogic.h"
    ${#HL}$
    #include "Health.h"
    #include "HUD.h" ${HL#}$
    #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);
    
    ${#HL}$
    	检查被击中对象是否为玩家且是否带有 Health 组件
    	Health *health = ComponentSystem::get()->getComponent<Health>(hitObject);
    	if (health && ComponentSystem::get()->getComponentInParent<PlayerLogic>(hitObject))
    	{
    		// 应用子弹伤害
    		health->takeDamage(damage);
    
    		//  在 HUD 中更新玩家生命值信息
    		ComponentSystem::get()->getComponentInWorld<HUD>()->updateHealthInfo(health->health);
    	} ${HL#}$
    
    	// 删除子弹
    	node.deleteLater();
    }

The robots that have zero health should be deleted from the scene. The Health component has the isDead() flag which is checked by the EnemyLogic component of the robot. If the flag is set to true, the node of that robot will be deleted. To do this, we will add the health property to the EnemyLogic component, initialize it in init() and then we'll check the health level of the enemy robot every frame in update() and remove it if necessary:生命值为零的机器人应从场景中删除。Health组件具有一个isDead()标志,该标志由机器人的EnemyLogic组件进行检查。如果该标志被设置为true,则会删除该机器人的节点。为实现此目的,我们将在EnemyLogic组件中添加health属性,在init()中进行初始化,并在update()中每帧检查敌人机器人的生命值,并在必要时将其移除:

EnemyLogic.h

源代码 (C++)
#pragma once
#include <UnigineComponentSystem.h>
#include <UniginePathFinding.h>
#include <UnigineVisualizer.h>
${#HL}$
#include "Health.h" ${HL#}$
#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;
${#HL}$
	Health *health = nullptr; ${HL#}$
	// 创建路径点队列
	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()
{
	// 如果目标可见(玩家) -- 从 Idle 切换到 Chase
	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;
		}
	}

	// 如果目标不可见 -- 从 Chase 切换到 Idle
	if (!targetIsVisible)
		currentState = EnemyLogicState::Idle;

	// 如果距离小于攻击半径 -- 从 Chase 切换到 Attack
	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()
{
	// 如果目标不可见或超出攻击范围 -- 从 Attack 切换到 Chase
	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);

${#HL}$
	// 获取 Health 组件
	health = ComponentSystem::get()->getComponentInChildren<Health>(node); ${HL#}$

	shouldUpdateRoute = true;
	lastCalculationTime = Game::getTime();
}

void EnemyLogic::update()
{
${#HL}$
	// 检查敌人的生命值
	if (health && health->isDead())
		//  如果生命值归零,删除敌人
		node.deleteLater(); ${HL#}$
	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);
}

We need to add the same check for the player, only instead of deleting it (in this case we'll just delete the main camera and see nothing else) we'll just make it immovable by disabling several components.我们还需要为玩家添加相同的检查。但不是删除玩家(因为这将导致主摄像机被删除而什么都看不到),而是通过禁用几个组件来让其无法移动。

  1. Create the PlayerLogic component and add the following code into it:创建PlayerLogic组件,并添加以下代码:

    PlayerLogic.h

    源代码 (C++)
    #pragma once
    #include <UnigineComponentSystem.h>
    #include "Health.h"
    class PlayerLogic :
    	public Unigine::ComponentBase
    {
    public:
    	COMPONENT_DEFINE(PlayerLogic, Unigine::ComponentBase);
    
    	// 声明在执行序列相应阶段调用的方法
    	COMPONENT_INIT(init,2);
    	COMPONENT_UPDATE(update);
    
    private:
    	// 玩家生命值
    	Health *health = nullptr;
    	// 重写世界主循环的方法
    	void init();
    	void update();
    };

    PlayerLogic.cpp

    源代码 (C++)
    #include "PlayerLogic.h"
    #include "FirstPersonController.h"
    #include "HUD.h"
    #include "WeaponController.h"
    #include "ShootInput.h"
    REGISTER_COMPONENT(PlayerLogic);
    using namespace Unigine;
    using namespace Math;
    
    void PlayerLogic::init()
    {
    	// 从节点获取 Health 组件
    	health = ComponentSystem::get()->getComponentInChildren<Health>(node);
    	// 更新初始玩家生命值信息
    	ComponentSystem::get()->getComponentInWorld<HUD>()->updateHealthInfo(health->health);
    }
    
    void PlayerLogic::update()
    {
    	// 检查 IsDead 标志是否被设置
    	if (health && health->isDead())
    	{
    		// 通过禁用组件使玩家无法移动
    		ComponentSystem::get()->getComponent<FirstPersonController>(node)->setEnabled(false);
    		ComponentSystem::get()->getComponent<WeaponController>(node)->setEnabled(false);
    		ComponentSystem::get()->getComponent<ShootInput>(node)->setEnabled(false);
    	}
    }

    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. Add the PlayerLogic component to the player node.PlayerLogic组件添加到player节点。

Let's also add displaying of player's health information in the HUD. To do this, we will add a few lines to the init() method and add the updateHealthInfo() method to update the value in the GUI widget in the HUD component's code:我们还将添加在HUD中显示玩家生命值信息的功能。为此,我们将在init()方法中添加几行,并添加updateHealthInfo()方法,用于更新HUD组件代码中的GUI控件数值:

HUD.h

源代码 (C++)
#pragma once
#include <UnigineComponentSystem.h>
#include <UnigineGui.h>
class HUD :
	public Unigine::ComponentBase
{
public:
	COMPONENT_DEFINE(HUD, Unigine::ComponentBase);
	// 准星参数
	PROP_PARAM(File, crosshairImage, "");
	PROP_PARAM(Int, crosshairSize, 16);

	// 声明在执行序列相应阶段调用的方法
	COMPONENT_INIT(init);
	COMPONENT_UPDATE(update);
	
	// 连接到屏幕 GUI
	Unigine::GuiPtr screenGui = nullptr;
${#HL}$
	void updateHealthInfo(int health); ${HL#}$
protected:
	Unigine::WidgetSpritePtr sprite = nullptr;
${#HL}$
	// 用于显示玩家生命值的标签
	Unigine::WidgetLabelPtr label = nullptr; ${HL#}$
	Unigine::Math::ivec2 prev_size;
	// 重写世界主循环的方法
	void init();
	void update();
};

HUD.cpp

源代码 (C++)
#include "HUD.h"
#include <UnigineGame.h>
REGISTER_COMPONENT(HUD);
using namespace Unigine;
using namespace Math;

void HUD::init()
{
	// 获取当前屏幕的 GUI
	screenGui = Gui::getCurrent();

	// 添加用于准星的 WidgetSprite
	if (crosshairImage != "")
		sprite = WidgetSprite::create(screenGui, Unigine::FileSystem::guidToPath(FileSystem::getGUID(crosshairImage.getRaw())));
	// 设置准星图像大小
	sprite->setWidth(crosshairSize);
	sprite->setHeight(crosshairSize);
	// 将图像添加到 GUI,使其始终位于屏幕中心并覆盖其他控件
	screenGui->addChild(sprite, Gui::ALIGN_CENTER | Gui::ALIGN_OVERLAP);
	// 将控件的生命周期绑定到世界
	sprite->setLifetime(Widget::LIFETIME_WORLD);

${#HL}$
	// 添加一个 WidgetLabel 用于显示玩家生命值并设置字体大小
	label = WidgetLabel::create(screenGui, "");
	label->setFontSize(50);
	label->setPosition(10, 10);
	label->setLifetime(Widget::LIFETIME_WORLD);

	// 将控件添加到 GUI
	screenGui->addChild(label, Gui::ALIGN_TOP | Gui::ALIGN_LEFT | Gui::ALIGN_OVERLAP); ${HL#}$
}

void HUD::update()
{
	ivec2 new_size = screenGui->getSize();
	if (prev_size != new_size)
	{
		screenGui->removeChild(sprite);
		screenGui->addChild(sprite, Gui::ALIGN_CENTER | Gui::ALIGN_OVERLAP);
	}
	prev_size = new_size;
}

${#HL}$
// 更新玩家当前的生命值
void HUD::updateHealthInfo(int health)
{
	label->setText(Unigine::String::format("Health: %d", health));
} ${HL#}$

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.保存所有已修改的文件,然后在 IDE 中按 Ctrl + F5 编译并启动应用程序,以便组件系统生成用于将组件绑定到节点的属性。启动后关闭应用并返回 UnigineEditor。

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

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