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

控制游戏流程

Implementing the GameController component that manages switching between the game states depending on the occurrence of certain events: all enemies are killed, player gets killed or time runs out.实现GameController组件,用于根据特定事件的发生(如所有敌人被消灭、玩家死亡或时间耗尽)在游戏状态之间切换。

The game should have different states depending on the occurrence of certain events. For example, you can add tracking the list of enemies, and if the list is empty the player has won. The game will end in defeat if the player is killed.游戏应根据特定事件的发生拥有不同的状态。例如,可以添加对敌人列表的跟踪,如果该列表为空,则表示玩家获胜。如果玩家被击杀,则游戏以失败告终。

To switch between Gameplay and Win/Lose states, we have the GameController component.为了在GameplayWin/Lose状态之间切换,我们使用GameController组件。

Create the GameController component and copy the following code into it:创建GameController组件,并将以下代码复制到该组件中:

GameController.h

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

#include <UnigineGame.h>

// 游戏状态
enum GameState
{
	Gameplay,
	Win,
	Lose,
};

class GameController :
	public Unigine::ComponentBase
{
public:
	// 声明 GameController 类的构造函数和析构函数,以及与组件关联的属性名称(property)
	COMPONENT_DEFINE(GameController, Unigine::ComponentBase);

	// 当前游戏流程状态
	GameState state;
	// 游戏结束时的摄像机(仅支持 Player 类型)
	PROP_PARAM(Node, EndCamera, nullptr);

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

private:
	// 是否显示结束画面的指示标志
	bool end_screen = false;

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

GameController.cpp

源代码 (C++)
#include "GameController.h"
#include "HUD.h"
#include "EnemyLogic.h"

REGISTER_COMPONENT(GameController);

using namespace Unigine;
using namespace Math;
void GameController::init()
{
	// 检查是否分配了游戏结束摄像机并且其类型为 Player
	if(EndCamera && !EndCamera->isPlayer())
	{
		Log::error("GameController 错误:%s 不是一个 Player 节点,无法用作 EndCamera。\n", EndCamera->getName());
		EndCamera = nullptr;
	}
	// 设置游戏的初始状态
	state = GameState::Gameplay;
}

void GameController::update()
{
	// 如果游戏已经结束
	if (state != GameState::Gameplay)
	{
		if (!end_screen && EndCamera)
		{
			// 切换到游戏结束摄像机视角
			Game::setPlayer(checked_ptr_cast<Player>(EndCamera.get()));
			// 在 HUD 中显示游戏结束的提示信息
			ComponentSystem::get()->getComponentInWorld<HUD>()->displayStateMessage(state);
			end_screen = true;
		}
	}
	else
	{
		// 如果所有敌人都被消灭,则切换到“胜利”状态
		if (!ComponentSystem::get()->getComponentInWorld<EnemyLogic>() || !ComponentSystem::get()->getComponentInWorld<EnemyLogic>()->isEnabled())
			state = GameState::Win;
	}
}

So let's add the displayStateMessage() method to the HUD component to display the game result:所以我们来给HUD组件添加displayStateMessage()方法,用于显示游戏结果:

HUD

源代码 (C++)
// 显示游戏结果信息
void HUD::displayStateMessage(GameState state)
{
	// 添加 WidgetLabel 用于显示最终结果消息,设置字体大小和颜色
	WidgetLabelPtr end_message = WidgetLabel::create(screenGui, (state == GameState::Win) ? "胜利!" : "你失败了!");
	end_message->setFontSize(100);
	end_message->setFontColor(vec4_red);
	screenGui->addChild(end_message, Gui::ALIGN_CENTER | Gui::ALIGN_OVERLAP);
	// 将控件生命周期绑定到世界
	end_message->setLifetime(Widget::LIFETIME_WORLD);

	// 终止流程
	ComponentSystem::get()->getComponentInWorld<GameController>()->setEnabled(false);
}

Next, modify code in the EnemyLogic and PlayerLogic components to use the logic of GameController:接下来,修改EnemyLogicPlayerLogic组件中的代码,以使用GameController的逻辑:

EnemyLogic.h

源代码 (C++)
#pragma once
#include <UnigineComponentSystem.h>
#include <UniginePathFinding.h>
#include <UnigineVisualizer.h>
#include "Health.h"
${#HL}$
#include "GameController.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;
	Health *health = nullptr;
${#HL}$
	GameController *gameController = 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);

	// 获取 Health 组件
	health = ComponentSystem::get()->getComponentInChildren<Health>(node);

${#HL}$
	// 获取对游戏管理器(GameController)的引用
	gameController = ComponentSystem::get()->getComponentInWorld<GameController>(); ${HL#}$

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

void EnemyLogic::update()
{
${#HL}$
	// 检查当前游戏状态,如果不是 Gameplay 则敌人不执行任何行为
	if (gameController->state != GameState::Gameplay)
		return; ${HL#}$
	// 检查敌人的生命值
	if (health && health->isDead())
		//  如果生命值为零,则删除敌人
		node.deleteLater();
	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);
}

PlayerLogic.h

源代码 (C++)
#pragma once
#include <UnigineComponentSystem.h>
#include "Health.h"
${#HL}$
#include "GameController.h" ${HL#}$
class PlayerLogic :
	public Unigine::ComponentBase
{
public:
	COMPONENT_DEFINE(PlayerLogic, Unigine::ComponentBase);

	// 声明将在执行序列相应阶段调用的方法
	COMPONENT_INIT(init,2);
	COMPONENT_UPDATE(update);

private:
	// 玩家生命值
	Health *health = nullptr;
${#HL}$
	// 游戏管理器的引用
	GameController* gameController = nullptr; ${HL#}$
	// 重写世界主循环的方法
	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);

${#HL}$
	// 获取对游戏管理器(GameController)的引用
	gameController = ComponentSystem::get()->getComponentInWorld<GameController>(); ${HL#}$
}

void PlayerLogic::update()
{
${#HL}$
	// 检查玩家生命值,如果被击杀,则删除玩家并将游戏切换为“失败”状态
	if (health && health->isDead())
	{
		// 删除玩家
		node.deleteLater();

		// 将游戏状态切换为失败
		gameController->state = GameState::Lose;
	}
	// 检查游戏状态,如果已结束,则删除玩家
	else if (gameController->state != GameState::Gameplay)
		node.deleteLater(); ${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.保存我们修改过的所有文件,然后按下Ctrl + F5来构建并运行应用程序,这将使组件系统更新用于将组件分配到节点的属性。运行完毕后关闭程序,切换回 UnigineEditor。

  1. Create a NodeDummy, name it gameplay_systems, and assign the GameController component to it.创建一个NodeDummy,将其命名为gameplay_systems,并为其分配GameController组件。

  2. For the game ending, let's create a separate camera that will look at the scene from above. Choose Create -> Camera -> Player Dummy in the menu. Rename the camera to end_camera. Switch to this camera in the Editor and control the camera to select the desired scene view.为了处理游戏结束的情况,我们创建一个独立的摄像机,从上方俯瞰场景。在菜单中选择Create -> Camera -> Player Dummy。将该摄像机重命名为end_camera。在编辑器中切换到该摄像机,并调整视角以选择所需的场景视图。
  3. Drag the end_camera node to the End Camera field of the GameController component assigned to the gameplay_systems node.end_camera节点拖放到分配给gameplay_systems节点的GameController组件的End Camera字段中。

Now your can add more enemies and test the game.现在你可以添加更多敌人并测试游戏了。

  1. To generate an arbitrary number of enemies, add a few lines to the GameController component:为了生成任意数量的敌人,在GameController组件中添加几行代码:

    GameController.h

    源代码 (C++)
    #pragma once
    #include <UnigineComponentSystem.h>
    
    #include <UnigineGame.h>
    
    // 游戏状态
    enum GameState
    {
    	Gameplay,
    	Win,
    	Lose,
    };
    
    class GameController :
    	public Unigine::ComponentBase
    {
    public:
    	COMPONENT_DEFINE(GameController, Unigine::ComponentBase);
    
    	// 当前游戏流程状态
    	GameState state;
    	// 游戏结束时的摄像机(仅支持 Player 类型)
    	PROP_PARAM(Node, EndCamera, nullptr);
    
    ${#HL}$
    	// 敌人生成位置
    	PROP_PARAM(Node, spawnPoint, nullptr);
    	// 带有敌人模板的 .node 资源
    	PROP_PARAM(File, enemyPrefab, nullptr);
    	// 要生成的敌人数
    	PROP_PARAM(Int, numEnemies, 1);
    	// 每个敌人之间的生成间隔(秒)
    	PROP_PARAM(Float, spawnInterval, 2.0f);
    
    	int spawned_enemy_counter = 0;
    	float currentTime = 0.0f; ${HL#}$
    
    	// 声明将在执行序列相应阶段调用的方法
    	COMPONENT_INIT(init);
    	COMPONENT_UPDATE(update);
    
    private:
    	// 是否显示结束画面的指示标志
    	bool end_screen = false;
    
    	// 重写世界主循环的方法
    	void init();
    	void update();
    };

    GameController.cpp

    源代码 (C++)
    #include "GameController.h"
    #include "HUD.h"
    #include "EnemyLogic.h"
    
    REGISTER_COMPONENT(GameController);
    
    using namespace Unigine;
    using namespace Math;
    void GameController::init()
    {
    	// 检查是否分配了游戏结束摄像机并且其类型为 Player
    	if(EndCamera && !EndCamera->isPlayer())
    	{
    		Log::error("GameController 错误:%s 不是一个 Player 节点,无法用作 EndCamera。\n", EndCamera->getName());
    		EndCamera = nullptr;
    	}
    	// 设置游戏的初始状态
    	state = GameState::Gameplay;
    }
    
    void GameController::update()
    {
    	// 如果游戏已经结束
    	if (state != GameState::Gameplay)
    	{
    		if (!end_screen && EndCamera)
    		{
    			// 切换到游戏结束摄像机视角
    			Game::setPlayer(checked_ptr_cast<Player>(EndCamera.get()));
    			// 在 HUD 中显示游戏结束的提示信息
    			ComponentSystem::get()->getComponentInWorld<HUD>()->displayStateMessage(state);
    			end_screen = true;
    		}
    	}
    	else
    	{
    		// 如果所有敌人都被消灭,则切换到“胜利”状态
    ${#HL}$		if ((!ComponentSystem::get()->getComponentInWorld<EnemyLogic>() || !ComponentSystem::get()->getComponentInWorld<EnemyLogic>()->isEnabled()) && spawned_enemy_counter == numEnemies) ${HL#}$
    			state = GameState::Win;
    ${#HL}$
    		// 以设定的时间间隔(spawnInterval)在指定位置(SpawnPoint)生成新的敌人(enemyPrefab)
    		if (spawned_enemy_counter < numEnemies && !enemyPrefab.nullCheck())
    		{
    			currentTime += Game::getIFps();
    
    			if (currentTime > spawnInterval)
    			{
    				currentTime -= spawnInterval;
    				spawned_enemy_counter++;
    				World::loadNode(Unigine::FileSystem::guidToPath(FileSystem::getGUID(enemyPrefab.getRaw())))->setTransform(spawnPoint->getWorldTransform());
    			}
    		} ${HL#}$
    	}
    }
  2. Create the Node Dummy node and place it to the point where new enemies will appear and name it spawn_point.创建一个Node Dummy节点,将其放置在新敌人出现的位置,并命名为spawn_point
  3. Drag the spawn_point node to the Spawn Point field, and the robot_enemy.node asset – to the Enemy Prefab field, and set the number of enemies and their spawn interval in seconds.spawn_point节点拖放到Spawn Point字段中,将robot_enemy.node资源拖放到Enemy Prefab字段中,并设置敌人的数量以及生成间隔时间(秒)。

Now, let's get down to business!好了,现在正式开始游戏吧!

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

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