This page has been translated automatically.
视频教程
界面
要领
高级
实用建议
基础
专业(SIM)
UnigineEditor
界面概述
资源工作流程
Version Control
设置和首选项
项目开发
调整节点参数
Setting Up Materials
设置属性
照明
Sandworm
使用编辑器工具执行特定任务
如何擴展編輯器功能
嵌入式节点类型
Nodes
Objects
Effects
Decals
光源
Geodetics
World Nodes
Sound Objects
Pathfinding Objects
Players
编程
基本原理
搭建开发环境
C++
C#
UnigineScript
统一的Unigine着色器语言 UUSL (Unified UNIGINE Shader Language)
Plugins
File Formats
材质和着色器
Rebuilding the Engine Tools
GUI
双精度坐标
应用程序接口
Animations-Related Classes
Containers
Common Functionality
Controls-Related Classes
Engine-Related Classes
Filesystem Functionality
GUI-Related Classes
Math Functionality
Node-Related Classes
Objects-Related Classes
Networking Functionality
Pathfinding-Related Classes
Physics-Related Classes
Plugins-Related Classes
IG Plugin
CIGIConnector Plugin
Rendering-Related Classes
VR-Related Classes
创建内容
内容优化
材质
Material Nodes Library
Miscellaneous
Input
Math
Matrix
Textures
Art Samples
Tutorials
注意! 这个版本的文档是过时的,因为它描述了一个较老的SDK版本!请切换到最新SDK版本的文档。
注意! 这个版本的文档描述了一个不再受支持的旧SDK版本!请升级到最新的SDK版本。

制作第一人称射击游戏(C#)

This tutorial shows how to create a First-Person Shooter.本教程演示如何创建第一人称射击游戏。

Using the C# component system, we will create a controllable player who can fire a gun with a crosshair in the center of the screen, and enemies pursuing the player and trying to shoot him.使用C#组件系统,我们将创建一个可控制的玩家,他可以在屏幕中央用十字准线发射枪,敌人追逐玩家并试图射击他。

Topics covered in this article include:本文涵盖的主题包括:

Creating a New Project and Downloading Assets
创建新项目并下载资源#

We are going to use the previously made assets which are available for downloading on Add-On Store.我们将使用以前制作的资源,这些资源可以通过SDK浏览器下载。

  1. Create a new empty C# project. Open the SDK Browser, go to the My Projects tab and click the Create New button.创建一个新的空C#项目。 打开SDK浏览器,转到My Projects选项卡,然后单击Create New按钮。
  2. In the window that opens, make sure to select C# (.NET) in the API + IDE list and click Create New Project.在打开的窗口中,确保在API + IDE列表中选择C# (.NET),然后单击Create New Project

  3. After the new project is created, it will appear in the My Projects tab. Click Open Editor under the created project to open it in the UnigineEditor.创建新项目后,它将出现在My Projects选项卡中。 单击已创建项目下的Open Editor以在UnigineEditor中打开它。

  4. Download the Docs Sample Content add-on from Add-On Store at https://store.unigine.com/ and add it to the project by dragging into the project data/ folder in the Asset Browser. In the Import Package window that opens, click the Import Package button and wait until the add-on contents are imported.从Add-On Store(网址:https://store.unigine.com/)下载Docs Sample Content插件,并通过拖拽到资源浏览器中的项目data/文件夹将其添加到项目中。在打开的Import Package窗口中,单击Import Package按钮,等待加载项内容导入完成。

Creating a Player
创建播放器#

To make a main character, we will need a controller node implementing basic player functionality (handling keyboard and mouse input, movement settings, etc.). Attached to this node, we will have a first-person view camera, hands with a gun, and a body imitation to check for collisions with other characters, bullets, and environment. Later we will assign logic components to the nodes to implement shooting, visual effects, etc.要制作一个主角,我们需要一个控制器节点来实现基本的播放器功能(处理键盘和鼠标输入,移动设置等)。). 附加到这个节点上,我们将有一个第一人称视角相机,手持枪和身体模仿,以检查与其他角色,子弹和环境的碰撞。 稍后我们将为节点分配逻辑组件以实现拍摄,视觉效果等。

Character Controller
字符控制器#

For the character controller, we will use the template first-person controller. It is included in the default scene of a new C# project as the first_person_controller node reference that stores a dummy object with the FirstPersonController component assigned.对于字符控制器,我们将使用模板第一人称控制器。 它作为first_person_controller节点引用包含在新C#项目的默认场景中,该引用存储了分配了FirstPersonController组件的虚拟对象

Let's rename it player: enable editing of the node reference and rename both nodes. It will represent our character.让我们重命名它player:启用节点引用的编辑并重命名两个节点。 它将代表我们的性格。

Arranging a First-Person Setup with Hands and Pistol
用手和手枪安排第一人称设置#

For a first-person setup you will need hands and weapon models and animations previously created in a 3D modeling software. Our ready-to-use assets are available in the data/fps folder.对于第一人称设置,您需要以前在3D建模软件中创建的手和武器模型和动画。 我们的即用型资源位于data/fps文件夹中。

Adding Hands
增加人手#

We start with adding hands, and then we will attach a pistol to them.我们从添加手开始,然后我们将为他们附加一把手枪。

In the Asset Browser, find the data/fps/hands/hands.fbx asset and add it to the scene.在资源浏览器中,找到data/fps/hands/hands.fbx资源并将其添加到场景中。

Player Body
球员身体#

To simulate a player's body that takes damage when hit by enemy bullets, you can use an additional node:要模拟玩家的身体在被敌人的子弹击中时受到伤害,您可以使用一个额外的节点:

  1. In the Menu bar, choose Create -> Primitive -> Box to create a box primitive of the (1,1,2) size, add it to the scene and rename player_hit_box.在菜单栏中,选择Create -> Primitive -> Box以创建(1,1,2)大小的box图元,将其添加到场景中并重命名player_hit_box

  2. Add it as a child to the hands dummy node and reset its position to the parent one.

    将其作为子项添加到hands虚拟节点并将其位置重置为父节点。

  3. Adjust the position of the player_hit_box so that it is placed below the hands.调整player_hit_box的位置,使其置于手的下方。
  4. And switch it to Dynamic mode.并将其切换到Dynamic模式。

  5. Make it invisible by clearing its Viewport mask in the Node tab of the Parameters window. Also clear the Shadow mask to disable shadows rendering.通过在Parameters窗口的Node选项卡中清除其Viewport掩码,使其不可见。 还要清除Shadow蒙版以禁用阴影渲染。

Later, we will assign a Health component to it.稍后,我们将为其分配一个Health组件。

Adding a Camera
添加相机#

To be able to see through the eyes of the character in the UnigineEditor, you can create a new camera (PlayerDummy). It will make it easier to test the first-person setup.为了能够在UnigineEditor中通过角色的眼睛看到,您可以创建一个新的相机(PlayerDummy)。 这将使测试第一人称设置更容易。

  1. Right-click the player dummy object and choose Create -> Camera -> Dummy. Place the created camera somewhere in the world. If necessary, enable editing of the player node reference first.右键单击player虚拟对象,然后选择Create -> Camera -> Dummy。 将创建的相机放置在世界的某个地方。 如有必要,请先启用player节点引用的编辑。

  2. In the Node tab of the Parameters window, reset the position of the camera to the player position. Then adjust the rotation so that the camera is directed forward: set the rotation around the X axis to 90.

    Node的标签Parameters窗口,将摄像机的位置重置为player位置。 然后调整旋转以使相机向前定向:将围绕X轴的旋转设置为90

  3. Add the hands dummy node as a child to the PlayerDummy node.hands虚拟节点作为子节点添加到PlayerDummy节点。

  4. Adjust the position of the hands so that you can see them through the PlayerDummy camera. Transformation of the player's body should also be changed.调整手的位置,以便您可以通过PlayerDummy相机看到它们。 玩家身体的改造也应该改变。
  5. In the Parameters window, change Near Clipping and FOV Degrees values of the PlayerDummy node: it will help you to get the required camera view.Parameters窗口中,更改PlayerDummy节点的Near ClippingFOV Degrees值:它将帮助您获得所需的相机视图。
  6. Check the Main Player box in the parameters of the PlayerDummy to make it the main camera.

    检查Main Player框中的参数PlayerDummy使其成为主摄像头。

  7. To avoid hands falling under gravity, adjust the position of the player dummy object's ShapeCapsule in the Physics tab of the Parameters window. It should almost coincide with the position of the player_hit_box node.为了避免手在重力作用下掉落,请在Parameters窗口的Physics选项卡中调整player虚拟对象的ShapeCapsule位置。 它应该几乎与player_hit_box节点的位置重合。
  8. Select the player dummy object and go to the Node tab of the Parameters window.选择player虚拟对象并转到Parameters窗口的Node选项卡。
  9. In the Node Components and Properties section, choose Camera -> Camera mode -> USE EXTERNAL.Node Components and Properties部分,选择Camera -> Camera mode -> USE EXTERNAL
  10. Drag and drop the PlayerDummy node to the Camera field.PlayerDummy节点拖放到Camera字段。

Now you can switch to the PlayerDummy camera in the Editor Viewport.现在您可以在Editor Viewport中切换到PlayerDummy相机。

Attaching a Weapon to the Hands
将武器附在手上#

Our FBX model of hands contains several bones. We can attach the pistol to a particular bone of the hands to make it follow the transformations of this bone. For this purpose, you should create a WorldTransformBone node.我们的FBX手模型包含多个骨骼。 我们可以将手枪固定在手的特定骨骼上,使其遵循此骨骼的转换。 为此,您应该创建一个WorldTransformBone节点。

  1. In the Asset Browser, find the data/fps/pistol/pistol.fbx asset and add it to the scene.Asset Browser中,找到data/fps/pistol/pistol.fbx资源并将其添加到场景中。
  2. In the Menu bar, choose Create -> Mesh -> SkinnedBone: a WorldTransformBone node will be created. Add it as a child to the hands skinned mesh (the one that is inherited from the hands dummy node).在菜单栏中,选择Create -> Mesh -> SkinnedBone:将创建一个WorldTransformBone节点。 将其作为子级添加到hands蒙皮网格(从hands虚拟节点继承的网格)。

  3. In the Bone drop-down list, select joint_hold. This will be the bone to which the pistol will be attached.骨骼下拉列表中,选择joint_hold。 这将是手枪将连接的骨头。

  4. Make the pistol a child of the WorldTransformBone node. Reset its relative position and rotation to zero if needed.使手枪成为WorldTransformBone节点的子节点。 如果需要,将其相对位置和旋转重置为零。

Testing Animations
测试动画#

There are also a number of animations to be used for the hands (idle, walking, shooting). You can check how a certain animation looks like, for example:还有一些动画可用于手(空闲,行走,射击)。 您可以检查某个动画的外观,例如:

  1. In the Asset Browser, find the fps/hands/hands_animations/handspistolidle.fbx/handspistolidle.anim file and drag it to the Preview Animation section of the hands skinned mesh parameters.Asset Browser中,找到fps/hands/hands_animations/handspistolidle.fbx/handspistolidle.anim文件并将其拖动到hands蒙皮网格参数的Preview Animation部分。
  2. Check the Loop option and click Play.选中循环选项,然后单击Play

Playing Animations via Code
通过代码播放动画#

When the character changes its states (shooting, walking forward/backward/left/right), the corresponding animations should change smoothly. We will implement a component for mixing our animations.当角色改变状态(射击,向前/向后/向左/向右行走)时,相应的动画应该平滑地改变。 我们将实现一个用于混合动画的组件。

Blending Animations
混合动画#

To ensure a seamless transition, we need to play two animations simultaneously and blend them. To do so, we will use multiple layers; then we will be able to assign different weights to these layers and achieve smooth blending.为了确保无缝过渡,我们需要同时播放两个动画并混合它们。 为此,我们将使用多个图层;然后我们将能够为这些图层分配不同的权重并实现平滑混合。

The following scheme shows the blend tree we are going to use:下面的方案显示了我们要使用的混合树:

  1. Create a new C# component HandAnimationController.cs: in the Asset Browser, right-click and choose Create Code -> C# Component in the drop-down list. Copy and paste the following code to the created component:创建一个新的C#组件HandAnimationController.cs:在Asset Browser中,右键单击并在下拉列表中选择Create Code -> C# Component。 将以下代码复制并粘贴到创建的组件中:

    HandAnimationController.cs

    源代码 (C#)
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    
    [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- this line is generated automatically for a new component
    public class HandAnimationController : Component
    {
    	// first person controller
    	public FirstPersonController fpsController = null;
    
    	public float moveAnimationSpeed = 30.0f;
    	public float shootAnimationSpeed = 30.0f;
    	public float idleWalkMixDamping = 5.0f;
    	public float walkDamping = 5.0f;
    	public float shootDamping = 1.0f;
    
    	// animation parameters
    	[ParameterFile(Filter = ".anim")]
    	public string idleAnimation = null;
    
    	[ParameterFile(Filter = ".anim")]
    	public string moveForwardAnimation = null;
    
    	[ParameterFile(Filter = ".anim")]
    	public string moveBackwardAnimation = null;
    
    	[ParameterFile(Filter = ".anim")]
    	public string moveRightAnimation = null;
    
    	[ParameterFile(Filter = ".anim")]
    	public string moveLeftAnimation = null;
    
    	[ParameterFile(Filter = ".anim")]
    	public string shootAnimation = null;
    
    	public vec2 LocalMovementVector
    	{
    		get
    		{
    			return new vec2(
    				MathLib.Dot(fpsController.SlopeAxisY, fpsController.HorizontalVelocity),
    				MathLib.Dot(fpsController.SlopeAxisX, fpsController.HorizontalVelocity)
    			);
    		}
    		set {}
    	}
    
    	private ObjectMeshSkinned meshSkinned = null;
    	private float currentIdleWalkMix = 0.0f; // 0 means idle animation, 1 means walk animation
    	private float currentShootMix = 0.0f; // 0 means idle/walk mix, 1 means shoot animation
    	private float currentWalkForward = 0.0f;
    	private float currentWalkBackward = 0.0f;
    	private float currentWalkRight = 0.0f;
    	private float currentWalkLeft = 0.0f;
    
    	private float currentWalkIdleMixFrame = 0.0f;
    	private float currentShootFrame = 0.0f;
    	private int numShootAnimationFrames = 0;
    
    	// animation layers number
    	private const int numLayers = 6;
    
    	private void Init()
    	{
    		// grab the node with the current component assigned
    		// and cast it to the ObjectMeshSkinned type
    		meshSkinned = node as ObjectMeshSkinned;
    
    		// set the number of animation layers for the node
    		meshSkinned.NumLayers = numLayers;
    
    		// set animation for each animation layer
    		meshSkinned.SetLayerAnimationFilePath(0, idleAnimation);
    		meshSkinned.SetLayerAnimationFilePath(1, moveForwardAnimation);
    		meshSkinned.SetLayerAnimationFilePath(2, moveBackwardAnimation);
    		meshSkinned.SetLayerAnimationFilePath(3, moveRightAnimation);
    		meshSkinned.SetLayerAnimationFilePath(4, moveLeftAnimation);
    		meshSkinned.SetLayerAnimationFilePath(5, shootAnimation);
    
    		numShootAnimationFrames = meshSkinned.GetLayerNumFrames(5);
    
    		// enable all animation layers
    		for (int i = 0; i < numLayers; ++i)
    			meshSkinned.SetLayerEnabled(i, true);
    	}
    ${#HL}$
    	public void Shoot()
    	{
    		// enable the shooting animation
    		currentShootMix = 1.0f;
    		// set the animation layer frame to 0
    		currentShootFrame = 0.0f;
    	}  ${HL#}$
    
    	private void Update()
    	{
    		vec2 movementVector = LocalMovementVector;
    	
    		// check if the character is moving
    		bool isMoving = movementVector.Length2 > MathLib.EPSILON;
    
    		// handle input: check if the fire button is pressed
    		bool isShooting = Input.IsMouseButtonDown(Input.MOUSE_BUTTON.LEFT);
    		if (isShooting)
    			Shoot();
    		// calculate the target values for the layer weights
    		float targetIdleWalkMix = (isMoving) ? 1.0f : 0.0f;
    		float targetWalkForward = (float) MathLib.Max(0.0f, movementVector.x);
    		float targetWalkBackward = (float) MathLib.Max(0.0f, -movementVector.x);
    		float targetWalkRight = (float) MathLib.Max(0.0f, movementVector.y);
    		float targetWalkLeft = (float) MathLib.Max(0.0f, -movementVector.y);
    
    		// apply the current layer weights
    		float idleWeight = 1.0f - currentIdleWalkMix;
    		float walkMixWeight = currentIdleWalkMix;
    		float shootWalkIdleMix = 1.0f - currentShootMix;
    
    		meshSkinned.SetLayerWeight(0, shootWalkIdleMix * idleWeight);
    		meshSkinned.SetLayerWeight(1, shootWalkIdleMix * walkMixWeight * currentWalkForward);
    		meshSkinned.SetLayerWeight(2, shootWalkIdleMix * walkMixWeight * currentWalkBackward);
    		meshSkinned.SetLayerWeight(3, shootWalkIdleMix * walkMixWeight * currentWalkRight);
    		meshSkinned.SetLayerWeight(4, shootWalkIdleMix * walkMixWeight * currentWalkLeft);
    		meshSkinned.SetLayerWeight(5, currentShootMix);
    
    		// update the animation frames: set the same frame for the animation layers to keep them in sync
    		meshSkinned.SetLayerFrame(0, currentWalkIdleMixFrame);
    		meshSkinned.SetLayerFrame(1, currentWalkIdleMixFrame);
    		meshSkinned.SetLayerFrame(2, currentWalkIdleMixFrame);
    		meshSkinned.SetLayerFrame(3, currentWalkIdleMixFrame);
    		meshSkinned.SetLayerFrame(4, currentWalkIdleMixFrame);
    		// set the shooting animation layer frame to 0 to start animation from the beginning
    		meshSkinned.SetLayerFrame(5, currentShootFrame);
    
    		currentWalkIdleMixFrame += moveAnimationSpeed * Game.IFps;
    		currentShootFrame = MathLib.Min(currentShootFrame + shootAnimationSpeed * Game.IFps, numShootAnimationFrames);
    
    		// smoothly update the current weight values
    		currentIdleWalkMix = MathLib.Lerp(currentIdleWalkMix, targetIdleWalkMix, idleWalkMixDamping * Game.IFps);
    
    		currentWalkForward = MathLib.Lerp(currentWalkForward, targetWalkForward, walkDamping * Game.IFps);
    		currentWalkBackward = MathLib.Lerp(currentWalkBackward, targetWalkBackward, walkDamping * Game.IFps);
    		currentWalkRight = MathLib.Lerp(currentWalkRight, targetWalkRight, walkDamping * Game.IFps);
    		currentWalkLeft = MathLib.Lerp(currentWalkLeft, targetWalkLeft, walkDamping * Game.IFps);
    
    		currentShootMix = MathLib.Lerp(currentShootMix, 0.0f, shootDamping * Game.IFps);
    	}
    }
  2. In the UnigineEditor, assign this component to the hands skinned mesh.在UnigineEditor中,将此组件分配给hands蒙皮网格。
  3. Remove the fps/hands/hands_animations/handspistolidle.fbx/handspistolidle.anim from the Preview Animation field of the Mesh Skinned section.Mesh Skinned部分的Preview Animation字段中移除fps/hands/hands_animations/handspistolidle.fbx/handspistolidle.anim
  4. Add animations stored in the fps/hands/hands_animations folder to the corresponding parameters.将存储在fps/hands/hands_animations文件夹中的动画添加到相应的参数中。

  5. Assign (drag and drop) the player dummy node to the Fps Controller field of the HandAnimationController component so that it could get required data from the player's first person controller to perform blending.

    分配(拖放)player虚拟节点到Fps Controller领域的HandAnimationController组件,以便它可以从玩家的first person controller来执行混合。

  6. Save all changes and run the application logic via the UnigineEditor to check the result.Save all changes and run the application logic via the UnigineEditor to check the result.

Implementing Player Shooting
实施球员射击#

Shooting Controls
拍摄控制#

Let's implement a new component for checking if the fire button is pressed. This is the preferred way as we are going to use this logic in the other components:让我们实现一个新的组件,用于检查是否按下了fire按钮。 这是首选的方式,因为我们将在其他组件中使用此逻辑:

  • In the HandAnimationController component to start the shooting animation.HandAnimationController组件中开始拍摄动画。
  • In the WeaponController component to start the shooting logic.WeaponController组件中启动拍摄逻辑。

In this component, you can also define a button that acts as a fire button.在该组件中,您还可以定义一个充当消防按钮的按钮。

To handle user input, use one of the Input class functions to check if the given button is pressed.要处理用户输入,请使用Input类函数之一来检查是否按下了给定的按钮。

  1. Create a ShootInput.cs component and copy the following code to it.创建一个ShootInput.cs组件并将以下代码复制到其中。

    ShootInput.cs

    源代码 (C#)
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    
    [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- this line is generated automatically for a new component
    public class ShootInput : Component
    {	
    	public bool IsShooting()
    	{
    		return Input.IsMouseButtonDown(Input.MOUSE_BUTTON.LEFT);
    	}
    }
  2. Add the ShootInput.cs component to the player dummy node.ShootInput.cs组件添加到player虚拟节点。

  3. Modify the HandAnimationController.cs component in order to use logic of the ShootInput.cs. Replace your current code with the following one:修改HandAnimationController.cs组件以便使用ShootInput.cs的逻辑。 将当前代码替换为以下代码:

    HandAnimationController.cs

    源代码 (C#)
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    
    [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- this line is generated automatically for a new component
    public class HandAnimationController : Component
    {
    	// first person controller
    	public FirstPersonController fpsController = null;
    
    ${#HL}$	public ShootInput shootInput = null;  ${HL#}$
    
    	public float moveAnimationSpeed = 30.0f;
    	public float shootAnimationSpeed = 30.0f;
    	public float idleWalkMixDamping = 5.0f;
    	public float walkDamping = 5.0f;
    	public float shootDamping = 1.0f;
    
    	// animation parameters
    	[ParameterFile(Filter = ".anim")]
    	public string idleAnimation = null;
    
    	[ParameterFile(Filter = ".anim")]
    	public string moveForwardAnimation = null;
    
    	[ParameterFile(Filter = ".anim")]
    	public string moveBackwardAnimation = null;
    
    	[ParameterFile(Filter = ".anim")]
    	public string moveRightAnimation = null;
    
    	[ParameterFile(Filter = ".anim")]
    	public string moveLeftAnimation = null;
    
    	[ParameterFile(Filter = ".anim")]
    	public string shootAnimation = null;
    
    	public vec2 LocalMovementVector
    	{
    		get
    		{
    			return new vec2(
    				MathLib.Dot(fpsController.SlopeAxisY, fpsController.HorizontalVelocity),
    				MathLib.Dot(fpsController.SlopeAxisX, fpsController.HorizontalVelocity)
    			);
    		}
    		set {}
    	}
    
    	private ObjectMeshSkinned meshSkinned = null;
    	private float currentIdleWalkMix = 0.0f; // 0 means idle animation, 1 means walk animation
    	private float currentShootMix = 0.0f; // 0 means idle/walk mix, 1 means shoot animation
    	private float currentWalkForward = 0.0f;
    	private float currentWalkBackward = 0.0f;
    	private float currentWalkRight = 0.0f;
    	private float currentWalkLeft = 0.0f;
    
    	private float currentWalkIdleMixFrame = 0.0f;
    	private float currentShootFrame = 0.0f;
    	private int numShootAnimationFrames = 0;
    
    	// animation layers number
    	private const int numLayers = 6;
    
    	private void Init()
    	{
    		// grab the node with the current component assigned
    		// and cast it to the ObjectMeshSkinned type
    		meshSkinned = node as ObjectMeshSkinned;
    
    		// set the number of animation layers for the node
    		meshSkinned.NumLayers = numLayers;
    
    		// set animation for each animation layer
    		meshSkinned.SetLayerAnimationFilePath(0, idleAnimation);
    		meshSkinned.SetLayerAnimationFilePath(1, moveForwardAnimation);
    		meshSkinned.SetLayerAnimationFilePath(2, moveBackwardAnimation);
    		meshSkinned.SetLayerAnimationFilePath(3, moveRightAnimation);
    		meshSkinned.SetLayerAnimationFilePath(4, moveLeftAnimation);
    		meshSkinned.SetLayerAnimationFilePath(5, shootAnimation);
    
    		numShootAnimationFrames = meshSkinned.GetLayerNumFrames(5);
    
    		// enable all animation layers
    		for (int i = 0; i < numLayers; ++i)
    			meshSkinned.SetLayerEnabled(i, true);
    	}
    ${#HL}$
    	public void Shoot()
    	{
    		// enable the shooting animation
    		currentShootMix = 1.0f;
    		// set the animation layer frame to 0
    		currentShootFrame = 0.0f;
    	}  ${HL#}$
    
    	private void Update()
    	{
    		vec2 movementVector = LocalMovementVector;
    	
    		// check if the character is moving
    		bool isMoving = movementVector.Length2 > MathLib.EPSILON;
    
    		// handle input: check if the fire button is pressed
    ${#HL}$
    		if (shootInput.IsShooting())
    			Shoot();  ${HL#}$
    		// calculate the target values for the layer weights
    		float targetIdleWalkMix = (isMoving) ? 1.0f : 0.0f;
    		float targetWalkForward = (float) MathLib.Max(0.0f, movementVector.x);
    		float targetWalkBackward = (float) MathLib.Max(0.0f, -movementVector.x);
    		float targetWalkRight = (float) MathLib.Max(0.0f, movementVector.y);
    		float targetWalkLeft = (float) MathLib.Max(0.0f, -movementVector.y);
    
    		// apply the current layer weights
    		float idleWeight = 1.0f - currentIdleWalkMix;
    		float walkMixWeight = currentIdleWalkMix;
    		float shootWalkIdleMix = 1.0f - currentShootMix;
    
    		meshSkinned.SetLayerWeight(0, shootWalkIdleMix * idleWeight);
    		meshSkinned.SetLayerWeight(1, shootWalkIdleMix * walkMixWeight * currentWalkForward);
    		meshSkinned.SetLayerWeight(2, shootWalkIdleMix * walkMixWeight * currentWalkBackward);
    		meshSkinned.SetLayerWeight(3, shootWalkIdleMix * walkMixWeight * currentWalkRight);
    		meshSkinned.SetLayerWeight(4, shootWalkIdleMix * walkMixWeight * currentWalkLeft);
    		meshSkinned.SetLayerWeight(5, currentShootMix);
    
    		// update the animation frames: set the same frame for the animation layers to keep them in sync
    		meshSkinned.SetLayerFrame(0, currentWalkIdleMixFrame);
    		meshSkinned.SetLayerFrame(1, currentWalkIdleMixFrame);
    		meshSkinned.SetLayerFrame(2, currentWalkIdleMixFrame);
    		meshSkinned.SetLayerFrame(3, currentWalkIdleMixFrame);
    		meshSkinned.SetLayerFrame(4, currentWalkIdleMixFrame);
    		// set the shooting animation layer frame to 0 to start animation from the beginning
    		meshSkinned.SetLayerFrame(5, currentShootFrame);
    
    		currentWalkIdleMixFrame += moveAnimationSpeed * Game.IFps;
    		currentShootFrame = MathLib.Min(currentShootFrame + shootAnimationSpeed * Game.IFps, numShootAnimationFrames);
    
    		// smoothly update the current weight values
    		currentIdleWalkMix = MathLib.Lerp(currentIdleWalkMix, targetIdleWalkMix, idleWalkMixDamping * Game.IFps);
    
    		currentWalkForward = MathLib.Lerp(currentWalkForward, targetWalkForward, walkDamping * Game.IFps);
    		currentWalkBackward = MathLib.Lerp(currentWalkBackward, targetWalkBackward, walkDamping * Game.IFps);
    		currentWalkRight = MathLib.Lerp(currentWalkRight, targetWalkRight, walkDamping * Game.IFps);
    		currentWalkLeft = MathLib.Lerp(currentWalkLeft, targetWalkLeft, walkDamping * Game.IFps);
    
    		currentShootMix = MathLib.Lerp(currentShootMix, 0.0f, shootDamping * Game.IFps);
    	}
    }
  4. Select the hands node, drag and drop the player dummy node to the Shoot Input field in the HandAnimationController section.选择hands节点,将player虚拟节点拖放到HandAnimationController部分的Shoot Input字段中。

Using Intersections
使用交叉点#

To implement shooting, you can use the properties of the PlayerDummy camera. This camera has its -Z axis pointing at the middle of the screen. So, you can do a ray cast from the camera to the middle of the screen, get the intersection, and check if you hit something.要实现拍摄,可以使用PlayerDummy相机的属性。 这个相机有它的-Z轴指向屏幕的中间。 因此,您可以从相机到屏幕中间进行光线投射,获取交叉点,并检查您是否击中了某些东西。

In the component code below, we will store two points (p0, p1): the camera point and the point of the mouse pointer. GetIntersection() method will cast a ray from p0 to p1 and check the intersection with an object's surface that has the matching intersection mask. If we get the intersection, the method returns hitObject and hitInfo values (the intersection point and normal).在下面的组件代码中,我们将存储两个点(p0p1):相机点和鼠标指针的点。 GetIntersection()方法将从p0投射一条射线到p1,并检查与具有匹配相交掩码的对象表面的相交。 如果我们得到交点,该方法返回hitObjecthitInfo值(交点和法线)。

  1. Create a WeaponController.cs component and copy the following code:创建一个WeaponController.cs组件并复制以下代码:

    WeaponController.cs

    源代码 (C#)
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    
    #region Math Variables
    #if UNIGINE_DOUBLE
    using Vec3 = Unigine.dvec3;
    #else
    using Vec3 = Unigine.vec3;
    #endif
    #endregion
    
    [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- this line is generated automatically for a new component
    public class WeaponController : Component
    {
    	public PlayerDummy shootingCamera = null;
    	public ShootInput shootInput = null;
    	public NodeDummy weaponMuzzle = null;
    	public int damage = 1;
    
    	// intersection mask
    	[ParameterMask(MaskType = ParameterMaskAttribute.TYPE.INTERSECTION)]
    	public int mask = ~0;
    
    	public void Shoot()
    	{
    		// initialize the camera point (p0) and the point of the mouse pointer (p1)
    		Vec3 p0, p1;
    		shootingCamera.GetDirectionFromMainWindow(out p0, out p1, Input.MousePosition.x, Input.MousePosition.y);
    
    		// create an intersection normal
    		WorldIntersectionNormal hitInfo = new WorldIntersectionNormal();
    		// get the first object intersected by the (p0,p1) line
    		Unigine.Object hitObject = World.GetIntersection(p0, p1, mask, hitInfo);
    
    		// if the intersection is found
    		if (hitObject)
    		{
    			// render the intersection normal
    			Visualizer.RenderVector(hitInfo.Point, hitInfo.Point + hitInfo.Normal, vec4.RED, 0.25f, false, 2.0f);
    		}
    	}
    
    	private void Update()
    	{
    		// handle input: check if the fire button is pressed
    		if (shootInput.IsShooting())
    			Shoot();
    	}
    }
  2. Add the component to the player dummy node.将组件添加到player虚拟节点。
  3. Assign PlayerDummy to the Shooting Camera field so that the component could get information from the camera.PlayerDummy分配给Shooting Camera字段,以便组件可以从相机获取信息。
  4. Assign the player dummy node to the Shoot Input field.player虚拟节点分配给Shoot Input字段。

To view the shooting intersection points and normals, you can enable Visualizer when the application is running:要查看拍摄交叉点和法线,您可以在应用程序运行时启用Visualizer:

  1. Open the console by pressing ~~打开控制台
  2. Type show_visualizer 1.类型show_visualizer 1

Hit Effect and Muzzle Flash
命中效果和枪口闪光#

Visual effects for shooting can be implemented in a separate component. You can get information about the hit point and spawn a hit prefab at this point oriented along the hit normal. For the muzzle flash, you can attach a NodeDummy to the muzzle of the pistol, and spawn a muzzle flash prefab at this position.用于拍摄的视觉效果可以在单独的组件中实现。 您可以获得有关命中点的信息,并在该点沿命中法线定向生成命中预制件。 对于枪口闪光,你可以在手枪的枪口上附加一个NodeDummy,并在这个位置产生一个枪口闪光预制件。

In the component code below, the OnHit() and OnShoot() methods implement this logic.在下面的组件代码中,OnHit()OnShoot()方法实现了这个逻辑。

  1. Create a VFXController.cs component and copy the code below. You can also use the existing data/fps/components/VFXController.cs component.创建一个VFXController.cs组件并复制下面的代码。 您也可以使用现有的data/fps/components/VFXController.cs组件。

    VFXController.cs

    源代码 (C#)
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    #region Math Variables
    #if UNIGINE_DOUBLE
    using Vec3 = Unigine.dvec3;
    using Mat4 = Unigine.dmat4;
    #else
    using Vec3 = Unigine.vec3;
    using Mat4 = Unigine.mat4;
    #endif
    #endregion
    [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- this line is generated automatically for a new component
    public class VFXController : Component
    {
    	// prefabs for hit and muzzle flash visualization
    	[ParameterFile(Filter = ".node")]
    	public string hitPrefab = null;
    
    	[ParameterFile(Filter = ".node")]
    	public string muzzleFlashPrefab = null;
    
    	public void OnShoot(Mat4 transform)
    	{
    		// if no hit prefab is specified, do nothing
    		if (string.IsNullOrEmpty(hitPrefab))
    			return;
    
    		// load the prefab for muzzle flash visualization
    		Node muzzleFlashVFX = World.LoadNode(muzzleFlashPrefab);
    		// set the muzzle flash node transformation
    		muzzleFlashVFX.WorldTransform = transform;
    	}
    
    	public void OnHit(Vec3 hitPoint, vec3 hitNormal, Unigine.Object hitObject)
    	{
    		// if no hit prefab is specified, do nothing
    		if (string.IsNullOrEmpty(hitPrefab))
    			return;
    
    		// load the prefab for hit visualization
    		Node hitVFX = World.LoadNode(hitPrefab);
    		// place the prefab in the hit point and set its direction according to the hit normal
    		hitVFX.WorldPosition = hitPoint;
    		hitVFX.SetWorldDirection(hitNormal, vec3.UP, MathLib.AXIS.Y);
    	}
    }
  2. Modify the WeaponController.cs component in order to use logic of the VFXController.cs.修改WeaponController.cs组件以便使用VFXController.cs的逻辑。

    WeaponController.cs

    源代码 (C#)
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    
    #region Math Variables
    #if UNIGINE_DOUBLE
    using Vec3 = Unigine.dvec3;
    #else
    using Vec3 = Unigine.vec3;
    #endif
    #endregion
    
    [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- this line is generated automatically for a new component
    public class WeaponController : Component
    {
    	public PlayerDummy shootingCamera = null;
    	public ShootInput shootInput = null;
    	public NodeDummy weaponMuzzle = null;
    	public VFXController vfx = null;
    	public int damage = 1;
    
    	// intersection mask
    	[ParameterMask(MaskType = ParameterMaskAttribute.TYPE.INTERSECTION)]
    	public int mask = ~0;
    
    	public void Shoot()
    	{
    		// spawn a muzzle flash
    		if (weaponMuzzle)
    			vfx.OnShoot(weaponMuzzle.WorldTransform);
    		// initialize the camera point (p0) and the point of the mouse pointer (p1)
    		Vec3 p0, p1;
    		shootingCamera.GetDirectionFromMainWindow(out p0, out p1, Input.MousePosition.x, Input.MousePosition.y);
    
    		// create an intersection normal
    		WorldIntersectionNormal hitInfo = new WorldIntersectionNormal();
    		// get the first object intersected by the (p0,p1) line
    		Unigine.Object hitObject = World.GetIntersection(p0, p1, mask, hitInfo);
    
    		// if the intersection is found
    		if (hitObject)
    		{
    			// render the intersection normal
    			Visualizer.RenderVector(hitInfo.Point, hitInfo.Point + hitInfo.Normal, vec4.RED, 0.25f, false, 2.0f);
    			// spawn a hit prefab at the intersection point
    			vfx.OnHit(hitInfo.Point, hitInfo.Normal, hitObject);
    		}
    	}
    
    	private void Update()
    	{
    		// handle input: check if the fire button is pressed
    		if (shootInput.IsShooting())
    			Shoot();
    	}
    }
  3. Add the VFXController.cs component to the player dummy node.VFXController.cs组件添加到player虚拟节点。
  4. Create a NodeDummy, call it muzzle, make it a child of the pistol skinned mesh, and place it near the end of the weapon muzzle.创建一个NodeDummy,将其称为muzzle,使其成为pistol蒙皮网格的子级,并将其放置在武器枪口的末端附近。

  5. Select the player dummy node, assign the muzzle node to the Weapon Muzzle field in the WeaponController section.选择player虚拟节点,将muzzle节点分配给WeaponController部分中的Weapon Muzzle字段。
  6. Assign the player dummy node to the Vfx field in the WeaponController section.player虚拟节点分配给WeaponController部分中的Vfx字段。

  7. Add the data/fps/bullet/bullet_hit.node to the Hit Prefab field of the VFXController section.data/fps/bullet/bullet_hit.node添加到VFXController部分的Hit Prefab字段中。
  8. Add the data/fps/bullet/bullet_spawn.node to the Muzzle Flash Prefab field.

    data/fps/bullet/bullet_spawn.node添加到Muzzle Flash Prefab场。

Now you can press Start and test the shooting visual effects.现在您可以按Start并测试拍摄视觉效果。

VFX Lifetime
特效生存期#

To control the duration of visual effects, you can add a component that will allow you to define a time interval for the node during which it will live and after which it will be deleted. The ready-to-use data/fps/components/LifeTime.cs component implements this logic.要控制视觉效果的持续时间,您可以添加一个组件,该组件允许您为节点定义一个时间间隔,在该时间间隔期间它将存活并在该时间间隔之后它将被删除。 即用型data/fps/components/LifeTime.cs组件实现了这个逻辑。

LifeTime.cs

源代码 (C#)
using System;
using System.Collections;
using System.Collections.Generic;
using Unigine;

[Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- this line is generated automatically for a new component
public class Lifetime : Component
{
	[ShowInEditor][Parameter(Tooltip = "Object's lifetime")]
	private float lifeTime = 1.0f;

	private float startTime = 0.0f;

	void Init()
	{
		// remember initialization time of an object
		startTime = Game.Time;
	}

	void Update()
	{
		// wait until the lifetime ends and delete the object
		if (Game.Time - startTime > lifeTime)
			node.DeleteLater();
	}
}

This component is already added to the bullet_hit.node and bullet_spawn.node prefabs:此组件已添加到bullet_hit.nodebullet_spawn.node预制件中:

  • For the bullet_hit.node, the Life Time parameter is set to 1 second.对于bullet_hit.nodeLife Time参数设置为1秒。

  • For the bullet_spawn.node, the Life Time parameter is set to 5 seconds.对于bullet_spawn.nodeLife Time参数设置为5秒。

Adding a HUD for the Crosshair and Player Stats
为十字准线和玩家统计数据添加HUD#

To make a HUD displaying some game info or other graphic elements, you can get an instance of the screen GUI and then add widgets as its children. We will use a WidgetSprite to make a crosshair at the center of the screen.要使HUD显示一些游戏信息或其他图形元素,您可以获取屏幕GUI的实例,然后添加小部件作为其子项。 我们将使用一个WidgetSprite在屏幕的中心做一个十字线。

  1. Create a HUD.cs component (or use the existing one in data/fps/components).创建一个HUD.cs组件(或使用data/fps/components中的现有组件)。

    HUD.cs

    源代码 (C#)
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    
    [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- this line is generated automatically for a new component
    public class HUD : Component
    {
    	// crosshair parameters
    	[ParameterFile]
    	public string crosshairImage = null;
    	public int crosshairSize = 16;
    
    	private WidgetSprite sprite = null;
    
    	private void Init()
    	{
    		// get an instance of the screen Gui
    		Gui screenGui = Gui.GetCurrent();
    
    		// add a sprite widget
    		sprite = new WidgetSprite(screenGui, crosshairImage);
    		// set the sprite size
    		sprite.Width = crosshairSize;
    		sprite.Height = crosshairSize;
    
    		// make the sprite stay in the screen center and overlap the other widgets
    		screenGui.AddChild(sprite, Gui.ALIGN_CENTER | Gui.ALIGN_OVERLAP);
    	}
    	
    	private void Update()
    	{
    		// write here code to be called before updating each render frame
    		
    	}
    }
  2. Create a NodeDummy, place it somewhere in the scene, name it HUD and add the HUD.cs component to it.创建一个NodeDummy,将其放置在场景中的某个位置,将其命名为HUD并将HUD.cs组件添加到其中。

  3. Add the data/fps/hud/textures/crosshair.png file to the Crosshair Image field.data/fps/hud/textures/crosshair.png文件添加到Crosshair Image字段。

Creating an Enemy
制造敌人#

The important part of any shooter is an enemy. We are going to create an enemy which 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.任何射手的重要部分都是敌人。 我们将创建一个敌人,它在追逐玩家的场景中移动,在距离玩家一定距离的地方开始射击,如果被玩家的子弹击中,就会被杀死(删除)。

Adding Enemy Model
添加敌人模型#

Before adding an enemy model, you should create it in a 3D modeling software.在添加敌人模型之前,您应该在3D建模软件中创建它。

Find our ready-to-use robot_enemy.node enemy prefab 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
人工智能有限状态机的应用#

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, ChaseAttack/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.下图描述了敌人在每个状态下应该做什么,以及它将如何切换不同的状态。 典型的转换是从IdleChase,从ChaseAttack,反之亦然。

  1. Create an EnemyLogic.cs component and copy the code below:创建一个EnemyLogic.cs组件并复制下面的代码:

    EnemyLogic.cs

    源代码 (C#)
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    
    #region Math Variables
    #if UNIGINE_DOUBLE
    using Vec3 = Unigine.dvec3;
    #else
    using Vec3 = Unigine.vec3;
    #endif
    #endregion
    
    // declare the enemy states
    public enum EnemyLogicState
    {
    	Idle,
    	Chase,
    	Attack,
    }
    
    [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- this line is generated automatically for a new component
    public class EnemyLogic : Component
    {
    	public NavigationMesh navigationMesh = null;
    	public Node player = null;
    	public Node intersectionSocket = null;
    	public float reachRadius = 0.5f;
    	public float attackInnerRadius = 5.0f;
    	public float attackOuterRadius = 7.0f;
    	public float speed = 1.0f;
    	public float rotationStiffness = 8.0f;
    	public float routeRecalculationInterval = 3.0f;
    
    	[ParameterMask(MaskType = ParameterMaskAttribute.TYPE.INTERSECTION)]
    	public int playerIntersectionMask = ~0;
    
    	// initialize the enemy state
    	private EnemyLogicState currentState = EnemyLogicState.Idle;
    
    	private bool targetIsVisible;
    	private Vec3 lastSeenPosition;
    	private vec3 lastSeenDirection;
    	private float lastSeenDistanceSqr;
    
    	private BodyRigid bodyRigid = null;
    	private WorldIntersection hitInfo = new WorldIntersection();
    	private Node[] hitExcludes = new Node[2];
    
    	private bool IsTargetVisible()
    	{
    		Vec3 direction = (player.WorldPosition - intersectionSocket.WorldPosition);
    		Vec3 p0 = intersectionSocket.WorldPosition;
    		Vec3 p1 = p0 + direction * 2.0f;
    
    		Unigine.Object hitObject = World.GetIntersection(p0, p1, playerIntersectionMask, hitExcludes, hitInfo);
    		if (hitObject == null)
    			return false;
    
    		return player.ID == hitObject.ID;
    	}
    
    	private void Init()
    	{
    		bodyRigid = node.ObjectBodyRigid;
    		hitExcludes[0] = node;
    		hitExcludes[1] = node.GetChild(0);
    
    		targetIsVisible = false;
    	}
    
    	private void Update()
    	{
    
    		UpdateTargetState();
    		UpdateOrientation();
    
    		// switch between the enemy states
    		switch (currentState)
    		{
    			case EnemyLogicState.Idle: ProcessIdleState(); break;
    			case EnemyLogicState.Chase: ProcessChaseState(); break;
    			case EnemyLogicState.Attack: ProcessAttackState(); break;
    		}
    
    		// switch the colors indicating the enemy states
    		vec4 color = vec4.BLACK;
    		switch (currentState)
    		{
    			case EnemyLogicState.Idle: color = vec4.BLUE; break;
    			case EnemyLogicState.Chase: color = vec4.YELLOW; break;
    			case EnemyLogicState.Attack: color = vec4.RED; break;
    		}
    
    		// visualize the enemy states
    		Visualizer.RenderPoint3D(node.WorldPosition + vec3.UP * 2.0f, 0.25f, color);
    		Visualizer.RenderPoint3D(node.WorldPosition + vec3.UP * 3.0f, 0.25f, IsTargetVisible() ? vec4.GREEN : vec4.RED);
    		Visualizer.RenderPoint3D(lastSeenPosition, 0.1f, vec4.MAGENTA);
    
    		// visualize the attack radus
    		Visualizer.RenderSphere(attackInnerRadius, node.WorldTransform, vec4.RED);
    		Visualizer.RenderSphere(attackOuterRadius, node.WorldTransform, vec4.RED);
    
    	}
    
    	private void UpdateTargetState()
    	{
    		targetIsVisible = IsTargetVisible();
    		if (targetIsVisible)
    			lastSeenPosition = player.WorldPosition;
    
    		lastSeenDirection = (vec3)(lastSeenPosition - node.WorldPosition);
    		lastSeenDistanceSqr = lastSeenDirection.Length2;
    		lastSeenDirection.Normalize();
    	}
    
    	private void UpdateOrientation()
    	{
    		vec3 direction = lastSeenDirection;
    		direction.z = 0.0f;
    
    		quat targetRotation = new quat(MathLib.SetTo(vec3.ZERO, direction.Normalized, vec3.UP, MathLib.AXIS.Y));
    		quat currentRotation = node.GetWorldRotation();
    
    		currentRotation = MathLib.Slerp(currentRotation, targetRotation, Game.IFps * rotationStiffness);
    		node.SetWorldRotation(currentRotation);
    	}
    
    	private void ProcessIdleState()
    	{
    		// check Idle -> Chase transition
    		if (targetIsVisible)
    		{
    			// change the current state to Chase
    			currentState = EnemyLogicState.Chase;
    			// remember the player last seen position
    			lastSeenPosition = player.WorldPosition;
    		}
    	}
    
    	private void ProcessChaseState()
    	{
    
    		vec3 currentVelocity = bodyRigid.LinearVelocity;
    		currentVelocity.x = 0.0f;
    		currentVelocity.y = 0.0f;
    		bool targetReached = (lastSeenDistanceSqr < reachRadius * reachRadius);
    		if (!targetReached)
    		{
    			currentVelocity.x = lastSeenDirection.x * speed;
    			currentVelocity.y = lastSeenDirection.y * speed;
    		}
    
    		// check Chase->Idle transition
    		if (!targetIsVisible)
    		{
    			currentState = EnemyLogicState.Idle;
    		}
    
    		// check Chase -> Attack transition
    		if (lastSeenDistanceSqr < attackInnerRadius * attackInnerRadius && targetIsVisible)
    		{
    			currentState = EnemyLogicState.Attack;
    			currentVelocity.x = 0.0f;
    			currentVelocity.y = 0.0f;
    		}
    
    		bodyRigid.LinearVelocity = currentVelocity;
    	}
    
    	private void ProcessAttackState()
    	{
    		// check Attack -> Chase transition
    		if (lastSeenDistanceSqr > attackOuterRadius * attackOuterRadius || !targetIsVisible)
    		{
    			currentState = EnemyLogicState.Chase;
    		}
    	}
    }
  2. Enable editing of the robot_enemy node and assign the component to the robot_root dummy node in the Parameters window.启用robot_enemy节点的编辑,并将组件分配给Parameters窗口中的robot_root虚拟节点。
  3. Right-click the player node reference in the World Nodes window and choose Unpack to Node Content. The node reference will be removed and its content will be displayed in the World Nodes hierarchy.右键单击World Nodes窗口中的player节点引用,然后选择Unpack to Node Content。 节点引用将被删除,其内容将显示在World Nodes层次结构中。

  4. Drag and drop the player_hit_box node to the Player field. The player_hit_box imitates the body of the player and is used in calculations.player_hit_box节点拖放到Player字段。 player_hit_box模仿玩家的身体,用于计算。
  5. Drag and drop the robot_intersection_socket node of the robot_enemy to Intersection Socket field. It is the node from which the robot will do intersection checks.robot_enemyrobot_intersection_socket节点拖放到Intersection Socket字段。 它是机器人进行交叉检查的节点。

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

  • The state of the robot: Idle — BLUE, Chase — YELLOW, Attack — RED.机器人的状态:Idle-BLUE,Chase-YELLOW,Attack-RED
  • If the target is visible: Yes — GREEN, No — RED.如果目标可见:Yes-GREEN,No-RED

Implementing Enemy Shooting
实施敌人射击#

Creating Guns
制造枪支#

To implement robot shooting, you need a bullet prefab that will be spawned at certain points when the robot is in the Attack state.要实现机器人射击,您需要一个子弹预制件,该预制件将在机器人处于Attack状态时在某些点产生。

In the EnemyFireController component we will add some shooting logic to make the robot shoot alternately from the left and right muzzle. Positions of these muzzles, where bullet nodes will be spawned, are defined by the positions of two nodes that we will assign to the Left Muzzle and Right Muzzle fields of the component.EnemyFireController组件中我们将添加一些射击逻辑,使机器人从左右枪口交替射击。 这些枪口的位置(子弹节点将生成的位置)由我们将分配给组件的Left MuzzleRight Muzzle字段的两个节点的位置定义。

  1. Create an EnemyFireController.cs component (or use the existing one in data/fps/components).创建一个EnemyFireController.cs组件(或使用data/fps/components中的现有组件)。

    EnemyFireController.cs

    源代码 (C#)
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    
    [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- this line is generated automatically for a new component
    public class EnemyFireController : Component
    {
    	public Node leftMuzzle = null;
    	public Node rightMuzzle = null;
    
    	[ParameterFile]
    	public string bulletPrefab = null;
    
    	public float shootInterval = 1.0f;
    
    	private float currentTime = 0.0f;
    	private bool isLeft = false;
    	private bool isFiring = false;
    
    	public void StartFiring()
    	{
    		isFiring = true;
    	}
    
    	public void StopFiring()
    	{
    		isFiring = false;
    	}
    
    	private void Init()
    	{
    		// reset the current time
    		currentTime = 0.0f;
    		// start shooting from the right muzzle
    		isLeft = false;
    	}
    
    	private void Update()
    	{
    		// if the enemy is in the Chase state, do nothing
    		if (!isFiring)
    			return;
    
    		// update the current time
    		currentTime += Game.IFps;
    
    		// check if the next shot should be fired
    		if (currentTime > shootInterval)
    		{
    			// reset the current time
    			currentTime = 0.0f;
    			// spawn a bullet
    			Node bullet = World.LoadNode(bulletPrefab);
    
    			// set the bullet transformation
    			bullet.WorldTransform = (isLeft) ? leftMuzzle.WorldTransform : rightMuzzle.WorldTransform;
    			// switch the muzzle for the next shot
    			isLeft = !isLeft;
    		}
    	}
    }
  2. Modify the EnemyLogic.cs component in order to use the implemented logic.修改EnemyLogic.cs组件以便使用实现的逻辑。

    EnemyLogic.cs

    源代码 (C#)
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    
    #region Math Variables
    #if UNIGINE_DOUBLE
    using Vec3 = Unigine.dvec3;
    #else
    using Vec3 = Unigine.vec3;
    #endif
    #endregion
    
    // declare the enemy states
    public enum EnemyLogicState
    {
    	Idle,
    	Chase,
    	Attack,
    }
    
    [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- this line is generated automatically for a new component
    public class EnemyLogic : Component
    {
    	public NavigationMesh navigationMesh = null;
    	public Node player = null;
    	public Node intersectionSocket = null;
    	public float reachRadius = 0.5f;
    	public float attackInnerRadius = 5.0f;
    	public float attackOuterRadius = 7.0f;
    	public float speed = 1.0f;
    	public float rotationStiffness = 8.0f;
    	public float routeRecalculationInterval = 3.0f;
    
    	[ParameterMask(MaskType = ParameterMaskAttribute.TYPE.INTERSECTION)]
    	public int playerIntersectionMask = ~0;
    
    	// initialize the enemy state
    	private EnemyLogicState currentState = EnemyLogicState.Idle;
    
    	private bool targetIsVisible;
    	private Vec3 lastSeenPosition;
    	private vec3 lastSeenDirection;
    	private float lastSeenDistanceSqr;
    
    	private BodyRigid bodyRigid = null;
    	private WorldIntersection hitInfo = new WorldIntersection();
    	private Node[] hitExcludes = new Node[2];
    
    	private EnemyFireController fireController = null;
    	private bool IsTargetVisible()
    	{
    		Vec3 direction = (player.WorldPosition - intersectionSocket.WorldPosition);
    		Vec3 p0 = intersectionSocket.WorldPosition;
    		Vec3 p1 = p0 + direction * 2.0f;
    
    		Unigine.Object hitObject = World.GetIntersection(p0, p1, playerIntersectionMask, hitExcludes, hitInfo);
    		if (hitObject == null)
    			return false;
    
    		return player.ID == hitObject.ID;
    	}
    
    	private void Init()
    	{
    		bodyRigid = node.ObjectBodyRigid;
    		hitExcludes[0] = node;
    		hitExcludes[1] = node.GetChild(0);
    
    		targetIsVisible = false;
    		// grab the EnemyFireController component
    		fireController = node.GetComponent<EnemyFireController>();
    	}
    
    	private void Update()
    	{
    
    		UpdateTargetState();
    		UpdateOrientation();
    
    		// switch between the enemy states
    		switch (currentState)
    		{
    			case EnemyLogicState.Idle: ProcessIdleState(); break;
    			case EnemyLogicState.Chase: ProcessChaseState(); break;
    			case EnemyLogicState.Attack: ProcessAttackState(); break;
    		}
    
    		// switch the colors indicating the enemy states
    		vec4 color = vec4.BLACK;
    		switch (currentState)
    		{
    			case EnemyLogicState.Idle: color = vec4.BLUE; break;
    			case EnemyLogicState.Chase: color = vec4.YELLOW; break;
    			case EnemyLogicState.Attack: color = vec4.RED; break;
    		}
    
    		// visualize the enemy states
    		Visualizer.RenderPoint3D(node.WorldPosition + vec3.UP * 2.0f, 0.25f, color);
    		Visualizer.RenderPoint3D(node.WorldPosition + vec3.UP * 3.0f, 0.25f, IsTargetVisible() ? vec4.GREEN : vec4.RED);
    		Visualizer.RenderPoint3D(lastSeenPosition, 0.1f, vec4.MAGENTA);
    
    		// visualize the attack radus
    		Visualizer.RenderSphere(attackInnerRadius, node.WorldTransform, vec4.RED);
    		Visualizer.RenderSphere(attackOuterRadius, node.WorldTransform, vec4.RED);
    
    	}
    
    	private void UpdateTargetState()
    	{
    		targetIsVisible = IsTargetVisible();
    		if (targetIsVisible)
    			lastSeenPosition = player.WorldPosition;
    
    		lastSeenDirection = (vec3)(lastSeenPosition - node.WorldPosition);
    		lastSeenDistanceSqr = lastSeenDirection.Length2;
    		lastSeenDirection.Normalize();
    	}
    
    	private void UpdateOrientation()
    	{
    		vec3 direction = lastSeenDirection;
    		direction.z = 0.0f;
    
    		quat targetRotation = new quat(MathLib.SetTo(vec3.ZERO, direction.Normalized, vec3.UP, MathLib.AXIS.Y));
    		quat currentRotation = node.GetWorldRotation();
    
    		currentRotation = MathLib.Slerp(currentRotation, targetRotation, Game.IFps * rotationStiffness);
    		node.SetWorldRotation(currentRotation);
    	}
    
    	private void ProcessIdleState()
    	{
    		// check Idle -> Chase transition
    		if (targetIsVisible)
    		{
    			// change the current state to Chase
    			currentState = EnemyLogicState.Chase;
    			// remember the player last seen position
    			lastSeenPosition = player.WorldPosition;
    		}
    	}
    
    	private void ProcessChaseState()
    	{
    
    		vec3 currentVelocity = bodyRigid.LinearVelocity;
    		currentVelocity.x = 0.0f;
    		currentVelocity.y = 0.0f;
    		bool targetReached = (lastSeenDistanceSqr < reachRadius * reachRadius);
    		if (!targetReached)
    		{
    			currentVelocity.x = lastSeenDirection.x * speed;
    			currentVelocity.y = lastSeenDirection.y * speed;
    		}
    
    		// check Chase->Idle transition
    		if (!targetIsVisible)
    		{
    			currentState = EnemyLogicState.Idle;
    		}
    
    		// check Chase -> Attack transition
    		if (lastSeenDistanceSqr < attackInnerRadius * attackInnerRadius && targetIsVisible)
    		{
    			currentState = EnemyLogicState.Attack;
    			currentVelocity.x = 0.0f;
    			currentVelocity.y = 0.0f;
    			// start firing
    			if (fireController)
    				fireController.StartFiring();
    		}
    
    		bodyRigid.LinearVelocity = currentVelocity;
    	}
    
    	private void ProcessAttackState()
    	{
    		// check Attack -> Chase transition
    		if (lastSeenDistanceSqr > attackOuterRadius * attackOuterRadius || !targetIsVisible)
    		{
    			currentState = EnemyLogicState.Chase;
    			// stop firing
    			if (fireController)
    				fireController.StopFiring();
    		}
    	}
    }
  3. Enable editing of the robot_enemy node (if required) and assign the EnemyFireController.cs component to the robot_root dummy object. 启用robot_enemy节点的编辑(如果需要),并将EnemyFireController.cs组件分配给robot_root虚拟对象。
  4. Drag and drop the LeftGunMuzzle and RightGunMuzzle dummy nodes to the corresponding fields.LeftGunMuzzleRightGunMuzzle虚拟节点拖放到相应的字段中。

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

Right Gun Muzzle Selected右枪口选择

Making a Moving and Destroyable Bullet
制造一颗移动的可摧毁的子弹#

After spawning, a bullet should move in the right 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, see below), 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 to the scene.data/fps/bullet/bullet.node添加到场景中。
  2. Create a Bullet.cs component and copy the following code:创建一个Bullet.cs组件并复制以下代码:

    Bullet.cs

    源代码 (C#)
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    #region Math Variables
    #if UNIGINE_DOUBLE
    using Vec3 = Unigine.dvec3;
    #else
    using Vec3 = Unigine.vec3;
    #endif
    #endregion
    
    [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- this line is generated automatically for a new component
    public class Bullet : Component
    {
    	public float speed = 10.0f;
    	public int damage = 1;
    
    	[ParameterFile]
    	public string hitPrefab = null;
    
    	[ParameterMask]
    	public int intersectionMask = ~0;
    
    	private WorldIntersectionNormal hitInfo = new WorldIntersectionNormal();
    
    	private void Init()
    	{
    	}
    	
    	private void Update()
    	{
    		// set the current position of the bullet
    		Vec3 currentPosition = node.WorldPosition;
    		// set the current world direction vector of the bullet pointing along the Y axis
    		vec3 currentDirection = node.GetWorldDirection(MathLib.AXIS.Y);
    
    		// change the bullet position
    		node.WorldPosition += currentDirection * speed * Game.IFps;
    
    		// get the first intersected object
    		Unigine.Object hitObject = World.GetIntersection(currentPosition, node.WorldPosition, intersectionMask, hitInfo);
    
    		// if no intersections are found, do nothing
    		if (hitObject == null)
    			return;
    
    		// load a prefab for hit visualization
    		Node hitEffect = World.LoadNode(hitPrefab);
    		// place the prefab in the hit point and set its direction according to the hit normal
    		hitEffect.WorldPosition = hitInfo.Point;
    		hitEffect.SetWorldDirection(hitInfo.Normal, vec3.UP, MathLib.AXIS.Y);
    
    		// delete the bullet
    		node.DeleteLater();
    	}
    }

  3. Enable editing of the bullet node and assign the component to the bullet static mesh.启用bullet节点的编辑,并将组件分配给bullet节点网格。
  4. Drag and drop data/fps/bullet/bullet_hit.node to the Hit Prefab field.data/fps/bullet/bullet_hit.node拖放到Hit Prefab字段。
  5. Assign the LifeTime.cs component to the bullet static mesh and set its Life Time to 5 seconds.LifeTime.cs组件分配给bullet静态网格体,并将其Life Time设置为5秒。

Using Pathfinding
使用寻路#

The enemy should be able to chase the player correctly and not get stuck. To give the enemy additional knowledge about how it can navigate through the level, you can use pathfinding. This requires creating a navigation mesh, which specifies areas available for navigation and can be generated based on the FBX model of the scene using special tools, for example, RecastBlenderAddon.敌人应该能够正确追逐玩家,而不是卡住。 为了给敌人更多关于它如何在关卡中导航的知识,你可以使用寻路。 这需要创建一个导航网格,它指定可用于导航的区域,并且可以使用特殊工具基于场景的FBX模型生成,例如RecastBlenderAddon

The previously added EnemyLogic component includes pathfinding logic which calculates the route for the robot. When the robot is in the Chase state, instead of going directly to the last seen target position, it will follow a path using a navigation mesh added to the scene. The path consists of a queue of route points calculated by using the PathRoute class functionality.先前添加的EnemyLogic组件包括寻路逻辑,其为机器人计算路线。 当机器人处于Chase状态时,它将使用添加到场景中的导航网格遵循路径,而不是直接进入最后看到的目标位置。 路径由使用PathRoute类功能计算的路由点队列组成。

In the data/fps/navmesh_import folder you can find a navigation mesh created for this project.data/fps/navmesh_import文件夹中,您可以找到为此项目创建的导航网格

  1. To place the mesh in the scene, click Create->Navigation->NavigationMesh in the Menu Bar and specify the navmesh_import/navmesh.fbx/navmesh.002.mesh file.要在场景中放置网格,请单击菜单栏中的Create->Navigation->NavigationMesh并指定navmesh_import/navmesh.fbx/navmesh.002.mesh文件。
  2. Align the mesh with the area.

    将网格与区域对齐。

  3. In the Parameters window, set the Height of the navigation mesh to 3 for proper route calculation.

    Parameters窗口,设置Height导航网格到3以进行正确的路线计算。

  4. Implement pathfinding logic in the EnemyLogic.cs component. Replace the existing code with the following one:EnemyLogic.cs组件中实现寻路逻辑。 将现有代码替换为以下代码:

    EnemyLogic.cs

    源代码 (C#)
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    
    #region Math Variables
    #if UNIGINE_DOUBLE
    using Vec3 = Unigine.dvec3;
    #else
    using Vec3 = Unigine.vec3;
    #endif
    #endregion
    
    // declare the enemy states
    public enum EnemyLogicState
    {
    	Idle,
    	Chase,
    	Attack,
    }
    
    [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- this line is generated automatically for a new component
    public class EnemyLogic : Component
    {
    	public NavigationMesh navigationMesh = null;
    	public Node player = null;
    	public Node intersectionSocket = null;
    	public float reachRadius = 0.5f;
    	public float attackInnerRadius = 5.0f;
    	public float attackOuterRadius = 7.0f;
    	public float speed = 1.0f;
    	public float rotationStiffness = 8.0f;
    	public float routeRecalculationInterval = 3.0f;
    
    	[ParameterMask(MaskType = ParameterMaskAttribute.TYPE.INTERSECTION)]
    	public int playerIntersectionMask = ~0;
    
    	// initialize the enemy state
    	private EnemyLogicState currentState = EnemyLogicState.Idle;
    
    	private bool targetIsVisible;
    	private Vec3 lastSeenPosition;
    	private vec3 lastSeenDirection;
    	private float lastSeenDistanceSqr;
    
    	private BodyRigid bodyRigid = null;
    	private WorldIntersection hitInfo = new WorldIntersection();
    	private Node[] hitExcludes = new Node[2];
    
    	private EnemyFireController fireController = null;
    	// create a queue of the route points
    	private Queue<Vec3> calculatedRoute = new Queue<Vec3>();
    
    	private PathRoute route = new PathRoute();
    	private bool shouldUpdateRoute = true;
    	private float lastCalculationTime = 0.0f;
    	private bool IsTargetVisible()
    	{
    		Vec3 direction = (player.WorldPosition - intersectionSocket.WorldPosition);
    		Vec3 p0 = intersectionSocket.WorldPosition;
    		Vec3 p1 = p0 + direction * 2.0f;
    
    		Unigine.Object hitObject = World.GetIntersection(p0, p1, playerIntersectionMask, hitExcludes, hitInfo);
    		if (hitObject == null)
    			return false;
    
    		return player.ID == hitObject.ID;
    	}
    
    	private void Init()
    	{
    		// initialize parameters of the point moving along the route inside the navigation mesh
    		route.Radius = 0.0f;
    		route.Height = 1.0f;
    		route.MaxAngle = 0.5f;
    		bodyRigid = node.ObjectBodyRigid;
    		hitExcludes[0] = node;
    		hitExcludes[1] = node.GetChild(0);
    
    		targetIsVisible = false;
    		// grab the EnemyFireController component
    		fireController = node.GetComponent<EnemyFireController>();
    		shouldUpdateRoute = true;
    		lastCalculationTime = Game.Time;
    
    	}
    
    	private void Update()
    	{
    
    		UpdateTargetState();
    		UpdateOrientation();
    		UpdateRoute();
    
    		// switch between the enemy states
    		switch (currentState)
    		{
    			case EnemyLogicState.Idle: ProcessIdleState(); break;
    			case EnemyLogicState.Chase: ProcessChaseState(); break;
    			case EnemyLogicState.Attack: ProcessAttackState(); break;
    		}
    
    		// switch the colors indicating the enemy states
    		vec4 color = vec4.BLACK;
    		switch (currentState)
    		{
    			case EnemyLogicState.Idle: color = vec4.BLUE; break;
    			case EnemyLogicState.Chase: color = vec4.YELLOW; break;
    			case EnemyLogicState.Attack: color = vec4.RED; break;
    		}
    
    		// visualize the enemy states
    		Visualizer.RenderPoint3D(node.WorldPosition + vec3.UP * 2.0f, 0.25f, color);
    		Visualizer.RenderPoint3D(node.WorldPosition + vec3.UP * 3.0f, 0.25f, IsTargetVisible() ? vec4.GREEN : vec4.RED);
    		Visualizer.RenderPoint3D(lastSeenPosition, 0.1f, vec4.MAGENTA);
    
    		// visualize the attack radus
    		Visualizer.RenderSphere(attackInnerRadius, node.WorldTransform, vec4.RED);
    		Visualizer.RenderSphere(attackOuterRadius, node.WorldTransform, vec4.RED);
    
    		// visualize the route points
    		foreach (vec3 route_point in calculatedRoute)
    			Visualizer.RenderPoint3D(route_point + vec3.UP, 0.25f, vec4.BLACK);
    
    	}
    	private void UpdateRoute()
    	{
    		if (Game.Time - lastCalculationTime < routeRecalculationInterval)
    			return;
    
    		if (shouldUpdateRoute)
    		{
    			// calculate the route to the player
    			route.Create2D(node.WorldPosition, lastSeenPosition, 1);
    			shouldUpdateRoute = false;
    		}
    
    		// if the route is calculated
    		if (route.IsReady)
    		{
    			// if the target point of the route is reached
    			if (route.IsReached)
    			{
    				// clear the queue
    				calculatedRoute.Clear();
    
    				// add all root points to the queue
    				for(int i = 1; i < route.NumPoints; ++i)
    					calculatedRoute.Enqueue(route.GetPoint(i));
    
    				shouldUpdateRoute = true;
    				lastCalculationTime = Game.Time;
    			}
    			else
    				// recalculate the route if the target point isn't reached
    				route.Create2D(node.WorldPosition, lastSeenPosition, 1);
    		}
    	}
    
    	private void UpdateTargetState()
    	{
    		targetIsVisible = IsTargetVisible();
    		if (targetIsVisible)
    			lastSeenPosition = player.WorldPosition;
    
    		lastSeenDirection = (vec3)(lastSeenPosition - node.WorldPosition);
    		lastSeenDistanceSqr = lastSeenDirection.Length2;
    		lastSeenDirection.Normalize();
    	}
    
    	private void UpdateOrientation()
    	{
    		vec3 direction = lastSeenDirection;
    		direction.z = 0.0f;
    
    		quat targetRotation = new quat(MathLib.SetTo(vec3.ZERO, direction.Normalized, vec3.UP, MathLib.AXIS.Y));
    		quat currentRotation = node.GetWorldRotation();
    
    		currentRotation = MathLib.Slerp(currentRotation, targetRotation, Game.IFps * rotationStiffness);
    		node.SetWorldRotation(currentRotation);
    	}
    
    	private void ProcessIdleState()
    	{
    		// check Idle -> Chase transition
    		if (targetIsVisible)
    		{
    			// change the current state to Chase
    			currentState = EnemyLogicState.Chase;
    			// remember the player last seen position
    			lastSeenPosition = player.WorldPosition;
    		}
    	}
    
    	private void ProcessChaseState()
    	{
    
    		vec3 currentVelocity = bodyRigid.LinearVelocity;
    		currentVelocity.x = 0.0f;
    		currentVelocity.y = 0.0f;
    		if (calculatedRoute.Count > 0)
    		{
    			float distanceToTargetSqr = (float)(calculatedRoute.Peek() - node.WorldPosition).Length2;
    
    			bool targetReached = (distanceToTargetSqr < reachRadius * reachRadius);
    			if (targetReached)
    				calculatedRoute.Dequeue();
    
    			vec3 direction = (vec3)(calculatedRoute.Peek() - node.WorldPosition);
    			direction.z = 0.0f;
    			direction.Normalize();
    
    			currentVelocity.x = direction.x * speed;
    			currentVelocity.y = direction.y * speed;
    
    		}
    
    		// check Chase->Idle transition
    		if (!targetIsVisible)
    		{
    			currentState = EnemyLogicState.Idle;
    		}
    
    		// check Chase -> Attack transition
    		if (lastSeenDistanceSqr < attackInnerRadius * attackInnerRadius && targetIsVisible)
    		{
    			currentState = EnemyLogicState.Attack;
    			currentVelocity.x = 0.0f;
    			currentVelocity.y = 0.0f;
    			// start firing
    			if (fireController)
    				fireController.StartFiring();
    		}
    
    		bodyRigid.LinearVelocity = currentVelocity;
    	}
    
    	private void ProcessAttackState()
    	{
    		// check Attack -> Chase transition
    		if (lastSeenDistanceSqr > attackOuterRadius * attackOuterRadius || !targetIsVisible)
    		{
    			currentState = EnemyLogicState.Chase;
    			// stop firing
    			if (fireController)
    				fireController.StopFiring();
    		}
    	}
    }
  5. Drag and drop the NavigationMesh node to the NavigationMesh field of the EnemyLogic component assigned to the robot.

    拖放NavigationMesh节点到NavigationMesh领域的EnemyLogic分配给机器人的组件。

You can visualize the route points as black squares for debugging by enabling Visualizer.您可以通过启用Visualizer将路由点可视化为黑色方块进行调试。

Controlling Health
控制健康#

The player and enemies should have a health level that will decrease each time they are hit by a bullet.玩家和敌人应该有一个生命值水平,每次被子弹击中时都会降低。

  1. Create a Health.cs component and copy the following code (or use the existing one in data/fps/components):创建一个Health.cs组件并复制以下代码(或使用data/fps/components中的现有代码):

    Health.cs

    源代码 (C#)
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    
    [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- this line is generated automatically for a new component
    public class Health : Component
    {
    	public int health = 5;
    
    	// flag indicating that the health value is less or equal to 0
    	public bool IsDead => health <= 0;
    
    	public void TakeDamage(int damage)
    	{
    		// calculate damage to health
    		health = MathLib.Max(health - damage, 0);
    	}
    }
  2. Add it to the visuals node of the robot_enemy.

    将其添加到visuals的节点robot_enemy.

  3. Add it to the player_hit_box node of the player.将其添加到playerplayer_hit_box节点。
  4. Modify the WeaponController.cs, EnemyLogic.cs and Bullet.cs components in order to use logic of the Health.cs.

    WeaponController.cs

    源代码 (C#)
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    
    #region Math Variables
    #if UNIGINE_DOUBLE
    using Vec3 = Unigine.dvec3;
    #else
    using Vec3 = Unigine.vec3;
    #endif
    #endregion
    
    [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- this line is generated automatically for a new component
    public class WeaponController : Component
    {
    	public PlayerDummy shootingCamera = null;
    	public ShootInput shootInput = null;
    	public NodeDummy weaponMuzzle = null;
    	public VFXController vfx = null;
    	public int damage = 1;
    
    	// intersection mask
    	[ParameterMask(MaskType = ParameterMaskAttribute.TYPE.INTERSECTION)]
    	public int mask = ~0;
    
    	public void Shoot()
    	{
    		// spawn a muzzle flash
    		if (weaponMuzzle)
    			vfx.OnShoot(weaponMuzzle.WorldTransform);
    		// initialize the camera point (p0) and the point of the mouse pointer (p1)
    		Vec3 p0, p1;
    		shootingCamera.GetDirectionFromMainWindow(out p0, out p1, Input.MousePosition.x, Input.MousePosition.y);
    
    		// create an intersection normal
    		WorldIntersectionNormal hitInfo = new WorldIntersectionNormal();
    		// get the first object intersected by the (p0,p1) line
    		Unigine.Object hitObject = World.GetIntersection(p0, p1, mask, hitInfo);
    
    		// if the intersection is found
    		if (hitObject)
    		{
    			// render the intersection normal
    			Visualizer.RenderVector(hitInfo.Point, hitInfo.Point + hitInfo.Normal, vec4.RED, 0.25f, false, 2.0f);
    			// spawn a hit prefab at the intersection point
    			vfx.OnHit(hitInfo.Point, hitInfo.Normal, hitObject);
    			// apply damage
    			Health health = hitObject.GetComponent<Health>();
    			if (health)
    				health.TakeDamage(damage);
    		}
    	}
    
    	private void Update()
    	{
    		// handle input: check if the fire button is pressed
    		if (shootInput.IsShooting())
    			Shoot();
    	}
    }

    EnemyLogic.cs

    源代码 (C#)
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    
    #region Math Variables
    #if UNIGINE_DOUBLE
    using Vec3 = Unigine.dvec3;
    #else
    using Vec3 = Unigine.vec3;
    #endif
    #endregion
    
    // declare the enemy states
    public enum EnemyLogicState
    {
    	Idle,
    	Chase,
    	Attack,
    }
    
    [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- this line is generated automatically for a new component
    public class EnemyLogic : Component
    {
    	public NavigationMesh navigationMesh = null;
    	public Node player = null;
    	public Node intersectionSocket = null;
    	public float reachRadius = 0.5f;
    	public float attackInnerRadius = 5.0f;
    	public float attackOuterRadius = 7.0f;
    	public float speed = 1.0f;
    	public float rotationStiffness = 8.0f;
    	public float routeRecalculationInterval = 3.0f;
    
    	[ParameterMask(MaskType = ParameterMaskAttribute.TYPE.INTERSECTION)]
    	public int playerIntersectionMask = ~0;
    
    	// initialize the enemy state
    	private EnemyLogicState currentState = EnemyLogicState.Idle;
    
    	private bool targetIsVisible;
    	private Vec3 lastSeenPosition;
    	private vec3 lastSeenDirection;
    	private float lastSeenDistanceSqr;
    
    	private BodyRigid bodyRigid = null;
    	private WorldIntersection hitInfo = new WorldIntersection();
    	private Node[] hitExcludes = new Node[2];
    
    	private EnemyFireController fireController = null;
    	private Health health = null;
    	// create a queue of the route points
    	private Queue<Vec3> calculatedRoute = new Queue<Vec3>();
    
    	private PathRoute route = new PathRoute();
    	private bool shouldUpdateRoute = true;
    	private float lastCalculationTime = 0.0f;
    	private bool IsTargetVisible()
    	{
    		Vec3 direction = (player.WorldPosition - intersectionSocket.WorldPosition);
    		Vec3 p0 = intersectionSocket.WorldPosition;
    		Vec3 p1 = p0 + direction * 2.0f;
    
    		Unigine.Object hitObject = World.GetIntersection(p0, p1, playerIntersectionMask, hitExcludes, hitInfo);
    		if (hitObject == null)
    			return false;
    
    		return player.ID == hitObject.ID;
    	}
    
    	private void Init()
    	{
    		// initialize parameters of the point moving along the route inside the navigation mesh
    		route.Radius = 0.0f;
    		route.Height = 1.0f;
    		route.MaxAngle = 0.5f;
    		bodyRigid = node.ObjectBodyRigid;
    		hitExcludes[0] = node;
    		hitExcludes[1] = node.GetChild(0);
    
    		targetIsVisible = false;
    		// grab the EnemyFireController component
    		fireController = node.GetComponent<EnemyFireController>();
    		// grab the Health component
    		health = node.GetComponentInChildren<Health>();
    		shouldUpdateRoute = true;
    		lastCalculationTime = Game.Time;
    
    	}
    
    	private void Update()
    	{
    
    		// check the enemy health
    		if (health != null && health.IsDead)
    			// delete the enemy if it's dead
    			node.DeleteLater();
    
    		UpdateTargetState();
    		UpdateOrientation();
    		UpdateRoute();
    
    		// switch between the enemy states
    		switch (currentState)
    		{
    			case EnemyLogicState.Idle: ProcessIdleState(); break;
    			case EnemyLogicState.Chase: ProcessChaseState(); break;
    			case EnemyLogicState.Attack: ProcessAttackState(); break;
    		}
    
    		// switch the colors indicating the enemy states
    		vec4 color = vec4.BLACK;
    		switch (currentState)
    		{
    			case EnemyLogicState.Idle: color = vec4.BLUE; break;
    			case EnemyLogicState.Chase: color = vec4.YELLOW; break;
    			case EnemyLogicState.Attack: color = vec4.RED; break;
    		}
    
    		// visualize the enemy states
    		Visualizer.RenderPoint3D(node.WorldPosition + vec3.UP * 2.0f, 0.25f, color);
    		Visualizer.RenderPoint3D(node.WorldPosition + vec3.UP * 3.0f, 0.25f, IsTargetVisible() ? vec4.GREEN : vec4.RED);
    		Visualizer.RenderPoint3D(lastSeenPosition, 0.1f, vec4.MAGENTA);
    
    		// visualize the attack radus
    		Visualizer.RenderSphere(attackInnerRadius, node.WorldTransform, vec4.RED);
    		Visualizer.RenderSphere(attackOuterRadius, node.WorldTransform, vec4.RED);
    
    		// visualize the route points
    		foreach (vec3 route_point in calculatedRoute)
    			Visualizer.RenderPoint3D(route_point + vec3.UP, 0.25f, vec4.BLACK);
    
    	}
    	private void UpdateRoute()
    	{
    		if (Game.Time - lastCalculationTime < routeRecalculationInterval)
    			return;
    
    		if (shouldUpdateRoute)
    		{
    			// calculate the route to the player
    			route.Create2D(node.WorldPosition, lastSeenPosition, 1);
    			shouldUpdateRoute = false;
    		}
    
    		// if the route is calculated
    		if (route.IsReady)
    		{
    			// if the target point of the route is reached
    			if (route.IsReached)
    			{
    				// clear the queue
    				calculatedRoute.Clear();
    
    				// add all root points to the queue
    				for(int i = 1; i < route.NumPoints; ++i)
    					calculatedRoute.Enqueue(route.GetPoint(i));
    
    				shouldUpdateRoute = true;
    				lastCalculationTime = Game.Time;
    			}
    			else
    				// recalculate the route if the target point isn't reached
    				route.Create2D(node.WorldPosition, lastSeenPosition, 1);
    		}
    	}
    
    	private void UpdateTargetState()
    	{
    		targetIsVisible = IsTargetVisible();
    		if (targetIsVisible)
    			lastSeenPosition = player.WorldPosition;
    
    		lastSeenDirection = (vec3)(lastSeenPosition - node.WorldPosition);
    		lastSeenDistanceSqr = lastSeenDirection.Length2;
    		lastSeenDirection.Normalize();
    	}
    
    	private void UpdateOrientation()
    	{
    		vec3 direction = lastSeenDirection;
    		direction.z = 0.0f;
    
    		quat targetRotation = new quat(MathLib.SetTo(vec3.ZERO, direction.Normalized, vec3.UP, MathLib.AXIS.Y));
    		quat currentRotation = node.GetWorldRotation();
    
    		currentRotation = MathLib.Slerp(currentRotation, targetRotation, Game.IFps * rotationStiffness);
    		node.SetWorldRotation(currentRotation);
    	}
    
    	private void ProcessIdleState()
    	{
    		// check Idle -> Chase transition
    		if (targetIsVisible)
    		{
    			// change the current state to Chase
    			currentState = EnemyLogicState.Chase;
    			// remember the player last seen position
    			lastSeenPosition = player.WorldPosition;
    		}
    	}
    
    	private void ProcessChaseState()
    	{
    
    		vec3 currentVelocity = bodyRigid.LinearVelocity;
    		currentVelocity.x = 0.0f;
    		currentVelocity.y = 0.0f;
    		if (calculatedRoute.Count > 0)
    		{
    			float distanceToTargetSqr = (float)(calculatedRoute.Peek() - node.WorldPosition).Length2;
    
    			bool targetReached = (distanceToTargetSqr < reachRadius * reachRadius);
    			if (targetReached)
    				calculatedRoute.Dequeue();
    
    			vec3 direction = (vec3)(calculatedRoute.Peek() - node.WorldPosition);
    			direction.z = 0.0f;
    			direction.Normalize();
    
    			currentVelocity.x = direction.x * speed;
    			currentVelocity.y = direction.y * speed;
    
    		}
    
    		// check Chase->Idle transition
    		if (!targetIsVisible)
    		{
    			currentState = EnemyLogicState.Idle;
    		}
    
    		// check Chase -> Attack transition
    		if (lastSeenDistanceSqr < attackInnerRadius * attackInnerRadius && targetIsVisible)
    		{
    			currentState = EnemyLogicState.Attack;
    			currentVelocity.x = 0.0f;
    			currentVelocity.y = 0.0f;
    			// start firing
    			if (fireController)
    				fireController.StartFiring();
    		}
    
    		bodyRigid.LinearVelocity = currentVelocity;
    	}
    
    	private void ProcessAttackState()
    	{
    		// check Attack -> Chase transition
    		if (lastSeenDistanceSqr > attackOuterRadius * attackOuterRadius || !targetIsVisible)
    		{
    			currentState = EnemyLogicState.Chase;
    			// stop firing
    			if (fireController)
    				fireController.StopFiring();
    		}
    	}
    }

    Bullet.cs

    源代码 (C#)
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    #region Math Variables
    #if UNIGINE_DOUBLE
    using Vec3 = Unigine.dvec3;
    #else
    using Vec3 = Unigine.vec3;
    #endif
    #endregion
    
    [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- this line is generated automatically for a new component
    public class Bullet : Component
    {
    	public float speed = 10.0f;
    	public int damage = 1;
    
    	[ParameterFile]
    	public string hitPrefab = null;
    
    	[ParameterMask]
    	public int intersectionMask = ~0;
    
    	private WorldIntersectionNormal hitInfo = new WorldIntersectionNormal();
    
    	private void Init()
    	{
    	}
    	
    	private void Update()
    	{
    		// set the current position of the bullet
    		Vec3 currentPosition = node.WorldPosition;
    		// set the current world direction vector of the bullet pointing along the Y axis
    		vec3 currentDirection = node.GetWorldDirection(MathLib.AXIS.Y);
    
    		// change the bullet position
    		node.WorldPosition += currentDirection * speed * Game.IFps;
    
    		// get the first intersected object
    		Unigine.Object hitObject = World.GetIntersection(currentPosition, node.WorldPosition, intersectionMask, hitInfo);
    
    		// if no intersections are found, do nothing
    		if (hitObject == null)
    			return;
    
    		// load a prefab for hit visualization
    		Node hitEffect = World.LoadNode(hitPrefab);
    		// place the prefab in the hit point and set its direction according to the hit normal
    		hitEffect.WorldPosition = hitInfo.Point;
    		hitEffect.SetWorldDirection(hitInfo.Normal, vec3.UP, MathLib.AXIS.Y);
    
    		// apply damage from the bullet
    		Health health = hitObject.GetComponent<Health>();
    		if (health != null)
    			health.TakeDamage(damage);
    
    		// delete the bullet
    		node.DeleteLater();
    	}
    }
    Modify the WeaponController.cs, EnemyLogic.cs and Bullet.cs components in order to use logic of the Health.cs.

    WeaponController.cs

    源代码 (C#)
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    
    #region Math Variables
    #if UNIGINE_DOUBLE
    using Vec3 = Unigine.dvec3;
    #else
    using Vec3 = Unigine.vec3;
    #endif
    #endregion
    
    [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- this line is generated automatically for a new component
    public class WeaponController : Component
    {
    	public PlayerDummy shootingCamera = null;
    	public ShootInput shootInput = null;
    	public NodeDummy weaponMuzzle = null;
    	public VFXController vfx = null;
    	public int damage = 1;
    
    	// intersection mask
    	[ParameterMask(MaskType = ParameterMaskAttribute.TYPE.INTERSECTION)]
    	public int mask = ~0;
    
    	public void Shoot()
    	{
    		// spawn a muzzle flash
    		if (weaponMuzzle)
    			vfx.OnShoot(weaponMuzzle.WorldTransform);
    		// initialize the camera point (p0) and the point of the mouse pointer (p1)
    		Vec3 p0, p1;
    		shootingCamera.GetDirectionFromMainWindow(out p0, out p1, Input.MousePosition.x, Input.MousePosition.y);
    
    		// create an intersection normal
    		WorldIntersectionNormal hitInfo = new WorldIntersectionNormal();
    		// get the first object intersected by the (p0,p1) line
    		Unigine.Object hitObject = World.GetIntersection(p0, p1, mask, hitInfo);
    
    		// if the intersection is found
    		if (hitObject)
    		{
    			// render the intersection normal
    			Visualizer.RenderVector(hitInfo.Point, hitInfo.Point + hitInfo.Normal, vec4.RED, 0.25f, false, 2.0f);
    			// spawn a hit prefab at the intersection point
    			vfx.OnHit(hitInfo.Point, hitInfo.Normal, hitObject);
    			// apply damage
    			Health health = hitObject.GetComponent<Health>();
    			if (health)
    				health.TakeDamage(damage);
    		}
    	}
    
    	private void Update()
    	{
    		// handle input: check if the fire button is pressed
    		if (shootInput.IsShooting())
    			Shoot();
    	}
    }

    EnemyLogic.cs

    源代码 (C#)
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    
    #region Math Variables
    #if UNIGINE_DOUBLE
    using Vec3 = Unigine.dvec3;
    #else
    using Vec3 = Unigine.vec3;
    #endif
    #endregion
    
    // declare the enemy states
    public enum EnemyLogicState
    {
    	Idle,
    	Chase,
    	Attack,
    }
    
    [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- this line is generated automatically for a new component
    public class EnemyLogic : Component
    {
    	public NavigationMesh navigationMesh = null;
    	public Node player = null;
    	public Node intersectionSocket = null;
    	public float reachRadius = 0.5f;
    	public float attackInnerRadius = 5.0f;
    	public float attackOuterRadius = 7.0f;
    	public float speed = 1.0f;
    	public float rotationStiffness = 8.0f;
    	public float routeRecalculationInterval = 3.0f;
    
    	[ParameterMask(MaskType = ParameterMaskAttribute.TYPE.INTERSECTION)]
    	public int playerIntersectionMask = ~0;
    
    	// initialize the enemy state
    	private EnemyLogicState currentState = EnemyLogicState.Idle;
    
    	private bool targetIsVisible;
    	private Vec3 lastSeenPosition;
    	private vec3 lastSeenDirection;
    	private float lastSeenDistanceSqr;
    
    	private BodyRigid bodyRigid = null;
    	private WorldIntersection hitInfo = new WorldIntersection();
    	private Node[] hitExcludes = new Node[2];
    
    	private EnemyFireController fireController = null;
    	private Health health = null;
    	// create a queue of the route points
    	private Queue<Vec3> calculatedRoute = new Queue<Vec3>();
    
    	private PathRoute route = new PathRoute();
    	private bool shouldUpdateRoute = true;
    	private float lastCalculationTime = 0.0f;
    	private bool IsTargetVisible()
    	{
    		Vec3 direction = (player.WorldPosition - intersectionSocket.WorldPosition);
    		Vec3 p0 = intersectionSocket.WorldPosition;
    		Vec3 p1 = p0 + direction * 2.0f;
    
    		Unigine.Object hitObject = World.GetIntersection(p0, p1, playerIntersectionMask, hitExcludes, hitInfo);
    		if (hitObject == null)
    			return false;
    
    		return player.ID == hitObject.ID;
    	}
    
    	private void Init()
    	{
    		// initialize parameters of the point moving along the route inside the navigation mesh
    		route.Radius = 0.0f;
    		route.Height = 1.0f;
    		route.MaxAngle = 0.5f;
    		bodyRigid = node.ObjectBodyRigid;
    		hitExcludes[0] = node;
    		hitExcludes[1] = node.GetChild(0);
    
    		targetIsVisible = false;
    		// grab the EnemyFireController component
    		fireController = node.GetComponent<EnemyFireController>();
    		// grab the Health component
    		health = node.GetComponentInChildren<Health>();
    		shouldUpdateRoute = true;
    		lastCalculationTime = Game.Time;
    
    	}
    
    	private void Update()
    	{
    
    		// check the enemy health
    		if (health != null && health.IsDead)
    			// delete the enemy if it's dead
    			node.DeleteLater();
    
    		UpdateTargetState();
    		UpdateOrientation();
    		UpdateRoute();
    
    		// switch between the enemy states
    		switch (currentState)
    		{
    			case EnemyLogicState.Idle: ProcessIdleState(); break;
    			case EnemyLogicState.Chase: ProcessChaseState(); break;
    			case EnemyLogicState.Attack: ProcessAttackState(); break;
    		}
    
    		// switch the colors indicating the enemy states
    		vec4 color = vec4.BLACK;
    		switch (currentState)
    		{
    			case EnemyLogicState.Idle: color = vec4.BLUE; break;
    			case EnemyLogicState.Chase: color = vec4.YELLOW; break;
    			case EnemyLogicState.Attack: color = vec4.RED; break;
    		}
    
    		// visualize the enemy states
    		Visualizer.RenderPoint3D(node.WorldPosition + vec3.UP * 2.0f, 0.25f, color);
    		Visualizer.RenderPoint3D(node.WorldPosition + vec3.UP * 3.0f, 0.25f, IsTargetVisible() ? vec4.GREEN : vec4.RED);
    		Visualizer.RenderPoint3D(lastSeenPosition, 0.1f, vec4.MAGENTA);
    
    		// visualize the attack radus
    		Visualizer.RenderSphere(attackInnerRadius, node.WorldTransform, vec4.RED);
    		Visualizer.RenderSphere(attackOuterRadius, node.WorldTransform, vec4.RED);
    
    		// visualize the route points
    		foreach (vec3 route_point in calculatedRoute)
    			Visualizer.RenderPoint3D(route_point + vec3.UP, 0.25f, vec4.BLACK);
    
    	}
    	private void UpdateRoute()
    	{
    		if (Game.Time - lastCalculationTime < routeRecalculationInterval)
    			return;
    
    		if (shouldUpdateRoute)
    		{
    			// calculate the route to the player
    			route.Create2D(node.WorldPosition, lastSeenPosition, 1);
    			shouldUpdateRoute = false;
    		}
    
    		// if the route is calculated
    		if (route.IsReady)
    		{
    			// if the target point of the route is reached
    			if (route.IsReached)
    			{
    				// clear the queue
    				calculatedRoute.Clear();
    
    				// add all root points to the queue
    				for(int i = 1; i < route.NumPoints; ++i)
    					calculatedRoute.Enqueue(route.GetPoint(i));
    
    				shouldUpdateRoute = true;
    				lastCalculationTime = Game.Time;
    			}
    			else
    				// recalculate the route if the target point isn't reached
    				route.Create2D(node.WorldPosition, lastSeenPosition, 1);
    		}
    	}
    
    	private void UpdateTargetState()
    	{
    		targetIsVisible = IsTargetVisible();
    		if (targetIsVisible)
    			lastSeenPosition = player.WorldPosition;
    
    		lastSeenDirection = (vec3)(lastSeenPosition - node.WorldPosition);
    		lastSeenDistanceSqr = lastSeenDirection.Length2;
    		lastSeenDirection.Normalize();
    	}
    
    	private void UpdateOrientation()
    	{
    		vec3 direction = lastSeenDirection;
    		direction.z = 0.0f;
    
    		quat targetRotation = new quat(MathLib.SetTo(vec3.ZERO, direction.Normalized, vec3.UP, MathLib.AXIS.Y));
    		quat currentRotation = node.GetWorldRotation();
    
    		currentRotation = MathLib.Slerp(currentRotation, targetRotation, Game.IFps * rotationStiffness);
    		node.SetWorldRotation(currentRotation);
    	}
    
    	private void ProcessIdleState()
    	{
    		// check Idle -> Chase transition
    		if (targetIsVisible)
    		{
    			// change the current state to Chase
    			currentState = EnemyLogicState.Chase;
    			// remember the player last seen position
    			lastSeenPosition = player.WorldPosition;
    		}
    	}
    
    	private void ProcessChaseState()
    	{
    
    		vec3 currentVelocity = bodyRigid.LinearVelocity;
    		currentVelocity.x = 0.0f;
    		currentVelocity.y = 0.0f;
    		if (calculatedRoute.Count > 0)
    		{
    			float distanceToTargetSqr = (float)(calculatedRoute.Peek() - node.WorldPosition).Length2;
    
    			bool targetReached = (distanceToTargetSqr < reachRadius * reachRadius);
    			if (targetReached)
    				calculatedRoute.Dequeue();
    
    			vec3 direction = (vec3)(calculatedRoute.Peek() - node.WorldPosition);
    			direction.z = 0.0f;
    			direction.Normalize();
    
    			currentVelocity.x = direction.x * speed;
    			currentVelocity.y = direction.y * speed;
    
    		}
    
    		// check Chase->Idle transition
    		if (!targetIsVisible)
    		{
    			currentState = EnemyLogicState.Idle;
    		}
    
    		// check Chase -> Attack transition
    		if (lastSeenDistanceSqr < attackInnerRadius * attackInnerRadius && targetIsVisible)
    		{
    			currentState = EnemyLogicState.Attack;
    			currentVelocity.x = 0.0f;
    			currentVelocity.y = 0.0f;
    			// start firing
    			if (fireController)
    				fireController.StartFiring();
    		}
    
    		bodyRigid.LinearVelocity = currentVelocity;
    	}
    
    	private void ProcessAttackState()
    	{
    		// check Attack -> Chase transition
    		if (lastSeenDistanceSqr > attackOuterRadius * attackOuterRadius || !targetIsVisible)
    		{
    			currentState = EnemyLogicState.Chase;
    			// stop firing
    			if (fireController)
    				fireController.StopFiring();
    		}
    	}
    }

    Bullet.cs

    源代码 (C#)
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    #region Math Variables
    #if UNIGINE_DOUBLE
    using Vec3 = Unigine.dvec3;
    #else
    using Vec3 = Unigine.vec3;
    #endif
    #endregion
    
    [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- this line is generated automatically for a new component
    public class Bullet : Component
    {
    	public float speed = 10.0f;
    	public int damage = 1;
    
    	[ParameterFile]
    	public string hitPrefab = null;
    
    	[ParameterMask]
    	public int intersectionMask = ~0;
    
    	private WorldIntersectionNormal hitInfo = new WorldIntersectionNormal();
    
    	private void Init()
    	{
    	}
    	
    	private void Update()
    	{
    		// set the current position of the bullet
    		Vec3 currentPosition = node.WorldPosition;
    		// set the current world direction vector of the bullet pointing along the Y axis
    		vec3 currentDirection = node.GetWorldDirection(MathLib.AXIS.Y);
    
    		// change the bullet position
    		node.WorldPosition += currentDirection * speed * Game.IFps;
    
    		// get the first intersected object
    		Unigine.Object hitObject = World.GetIntersection(currentPosition, node.WorldPosition, intersectionMask, hitInfo);
    
    		// if no intersections are found, do nothing
    		if (hitObject == null)
    			return;
    
    		// load a prefab for hit visualization
    		Node hitEffect = World.LoadNode(hitPrefab);
    		// place the prefab in the hit point and set its direction according to the hit normal
    		hitEffect.WorldPosition = hitInfo.Point;
    		hitEffect.SetWorldDirection(hitInfo.Normal, vec3.UP, MathLib.AXIS.Y);
    
    		// apply damage from the bullet
    		Health health = hitObject.GetComponent<Health>();
    		if (health != null)
    			health.TakeDamage(damage);
    
    		// delete the bullet
    		node.DeleteLater();
    	}
    }

Deleting the Killed
删除被杀#

The nodes 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 true, the node will be deleted. We need to implement the same check for the player.Health组件具有由机器人的EnemyLogic组件检查的IsDead标志。 如果标志为true,则该节点将被删除。 我们需要对玩家实施相同的检查。

  1. Create a PlayerLogic.cs component (or use the existing one in data/fps/components)创建一个PlayerLogic.cs组件(或使用data/fps/components中的现有组件)

    PlayerLogic.cs

    源代码 (C#)
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    
    [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- this line is generated automatically for a new component
    public class PlayerLogic : Component
    {
    	private Health health = null;
    	private void Init()
    	{
    		// grab the Health component
    		health = node.GetComponentInChildren<Health>();
    	}
    	
    	private void Update()
    	{
    		// apply damage to the player's health
    		if (health != null && health.IsDead)
    		{
    			// delete the player
    			node.DeleteLater();
    			Game.Player = null;
    		}
    	}
    }
  2. Add the PlayerLogic component to the player node.PlayerLogic组件添加到player节点。

Switching Game States
切换游戏状态#

The game should have different states depending on the occurrence of certain events. For example, you can obtain a list of enemies, and if the list becomes empty, you win. You lose if the player gets killed or time runs out.根据某些事件的发生,游戏应该有不同的状态。 例如,您可以获得敌人列表,如果列表变为空,则获胜。 如果玩家被杀或时间耗尽,你就输了。

To switch between Play and Win/Lose states, we have a GameController component.要在PlayWin/Lose状态之间切换,我们有一个GameController组件。

  1. Create a GameController.cs component and copy the following code (or use the existing one in data/fps/components):创建一个GameController.cs组件并复制以下代码(或使用data/fps/components中的现有代码):

    GameController.cs

    源代码 (C#)
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    
    public enum GameState
    {
    	Gameplay,
    	Win,
    	Lose,
    }
    
    [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- this line is generated automatically for a new component
    public class GameController : Component
    {
    	public GameState state = GameState.Gameplay;
    }
  2. Modify the EnemyLogic.cs and PlayerLogic.cs components in order to use logic of the GameCotroller.cs.修改EnemyLogic.csPlayerLogic.cs组件以便使用GameCotroller.cs的逻辑。

    EnemyLogic.cs

    源代码 (C#)
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    
    #region Math Variables
    #if UNIGINE_DOUBLE
    using Vec3 = Unigine.dvec3;
    #else
    using Vec3 = Unigine.vec3;
    #endif
    #endregion
    
    // declare the enemy states
    public enum EnemyLogicState
    {
    	Idle,
    	Chase,
    	Attack,
    }
    
    [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- this line is generated automatically for a new component
    public class EnemyLogic : Component
    {
    
    	public GameController gameController = null;
    
    	public NavigationMesh navigationMesh = null;
    	public Node player = null;
    	public Node intersectionSocket = null;
    	public float reachRadius = 0.5f;
    	public float attackInnerRadius = 5.0f;
    	public float attackOuterRadius = 7.0f;
    	public float speed = 1.0f;
    	public float rotationStiffness = 8.0f;
    	public float routeRecalculationInterval = 3.0f;
    
    	[ParameterMask(MaskType = ParameterMaskAttribute.TYPE.INTERSECTION)]
    	public int playerIntersectionMask = ~0;
    
    	// initialize the enemy state
    	private EnemyLogicState currentState = EnemyLogicState.Idle;
    
    	private bool targetIsVisible;
    	private Vec3 lastSeenPosition;
    	private vec3 lastSeenDirection;
    	private float lastSeenDistanceSqr;
    
    	private BodyRigid bodyRigid = null;
    	private WorldIntersection hitInfo = new WorldIntersection();
    	private Node[] hitExcludes = new Node[2];
    
    	private EnemyFireController fireController = null;
    	private Health health = null;
    	// create a queue of the route points
    	private Queue<Vec3> calculatedRoute = new Queue<Vec3>();
    
    	private PathRoute route = new PathRoute();
    	private bool shouldUpdateRoute = true;
    	private float lastCalculationTime = 0.0f;
    	private bool IsTargetVisible()
    	{
    		Vec3 direction = (player.WorldPosition - intersectionSocket.WorldPosition);
    		Vec3 p0 = intersectionSocket.WorldPosition;
    		Vec3 p1 = p0 + direction * 2.0f;
    
    		Unigine.Object hitObject = World.GetIntersection(p0, p1, playerIntersectionMask, hitExcludes, hitInfo);
    		if (hitObject == null)
    			return false;
    
    		return player.ID == hitObject.ID;
    	}
    
    	private void Init()
    	{
    		// initialize parameters of the point moving along the route inside the navigation mesh
    		route.Radius = 0.0f;
    		route.Height = 1.0f;
    		route.MaxAngle = 0.5f;
    		bodyRigid = node.ObjectBodyRigid;
    		hitExcludes[0] = node;
    		hitExcludes[1] = node.GetChild(0);
    
    		targetIsVisible = false;
    		// grab the EnemyFireController component
    		fireController = node.GetComponent<EnemyFireController>();
    		// grab the Health component
    		health = node.GetComponentInChildren<Health>();
    		shouldUpdateRoute = true;
    		lastCalculationTime = Game.Time;
    
    	}
    
    	private void Update()
    	{
    
    		// check the game state
    		if (gameController.state != GameState.Gameplay)
    			return;
    
    		// check the enemy health
    		if (health != null && health.IsDead)
    			// delete the enemy if it's dead
    			node.DeleteLater();
    
    		UpdateTargetState();
    		UpdateOrientation();
    		UpdateRoute();
    
    		// switch between the enemy states
    		switch (currentState)
    		{
    			case EnemyLogicState.Idle: ProcessIdleState(); break;
    			case EnemyLogicState.Chase: ProcessChaseState(); break;
    			case EnemyLogicState.Attack: ProcessAttackState(); break;
    		}
    
    		// switch the colors indicating the enemy states
    		vec4 color = vec4.BLACK;
    		switch (currentState)
    		{
    			case EnemyLogicState.Idle: color = vec4.BLUE; break;
    			case EnemyLogicState.Chase: color = vec4.YELLOW; break;
    			case EnemyLogicState.Attack: color = vec4.RED; break;
    		}
    
    		// visualize the enemy states
    		Visualizer.RenderPoint3D(node.WorldPosition + vec3.UP * 2.0f, 0.25f, color);
    		Visualizer.RenderPoint3D(node.WorldPosition + vec3.UP * 3.0f, 0.25f, IsTargetVisible() ? vec4.GREEN : vec4.RED);
    		Visualizer.RenderPoint3D(lastSeenPosition, 0.1f, vec4.MAGENTA);
    
    		// visualize the attack radus
    		Visualizer.RenderSphere(attackInnerRadius, node.WorldTransform, vec4.RED);
    		Visualizer.RenderSphere(attackOuterRadius, node.WorldTransform, vec4.RED);
    
    		// visualize the route points
    		foreach (vec3 route_point in calculatedRoute)
    			Visualizer.RenderPoint3D(route_point + vec3.UP, 0.25f, vec4.BLACK);
    
    	}
    	private void UpdateRoute()
    	{
    		if (Game.Time - lastCalculationTime < routeRecalculationInterval)
    			return;
    
    		if (shouldUpdateRoute)
    		{
    			// calculate the route to the player
    			route.Create2D(node.WorldPosition, lastSeenPosition, 1);
    			shouldUpdateRoute = false;
    		}
    
    		// if the route is calculated
    		if (route.IsReady)
    		{
    			// if the target point of the route is reached
    			if (route.IsReached)
    			{
    				// clear the queue
    				calculatedRoute.Clear();
    
    				// add all root points to the queue
    				for(int i = 1; i < route.NumPoints; ++i)
    					calculatedRoute.Enqueue(route.GetPoint(i));
    
    				shouldUpdateRoute = true;
    				lastCalculationTime = Game.Time;
    			}
    			else
    				// recalculate the route if the target point isn't reached
    				route.Create2D(node.WorldPosition, lastSeenPosition, 1);
    		}
    	}
    
    	private void UpdateTargetState()
    	{
    		targetIsVisible = IsTargetVisible();
    		if (targetIsVisible)
    			lastSeenPosition = player.WorldPosition;
    
    		lastSeenDirection = (vec3)(lastSeenPosition - node.WorldPosition);
    		lastSeenDistanceSqr = lastSeenDirection.Length2;
    		lastSeenDirection.Normalize();
    	}
    
    	private void UpdateOrientation()
    	{
    		vec3 direction = lastSeenDirection;
    		direction.z = 0.0f;
    
    		quat targetRotation = new quat(MathLib.SetTo(vec3.ZERO, direction.Normalized, vec3.UP, MathLib.AXIS.Y));
    		quat currentRotation = node.GetWorldRotation();
    
    		currentRotation = MathLib.Slerp(currentRotation, targetRotation, Game.IFps * rotationStiffness);
    		node.SetWorldRotation(currentRotation);
    	}
    
    	private void ProcessIdleState()
    	{
    		// check Idle -> Chase transition
    		if (targetIsVisible)
    		{
    			// change the current state to Chase
    			currentState = EnemyLogicState.Chase;
    			// remember the player last seen position
    			lastSeenPosition = player.WorldPosition;
    		}
    	}
    
    	private void ProcessChaseState()
    	{
    
    		vec3 currentVelocity = bodyRigid.LinearVelocity;
    		currentVelocity.x = 0.0f;
    		currentVelocity.y = 0.0f;
    		if (calculatedRoute.Count > 0)
    		{
    			float distanceToTargetSqr = (float)(calculatedRoute.Peek() - node.WorldPosition).Length2;
    
    			bool targetReached = (distanceToTargetSqr < reachRadius * reachRadius);
    			if (targetReached)
    				calculatedRoute.Dequeue();
    
    			vec3 direction = (vec3)(calculatedRoute.Peek() - node.WorldPosition);
    			direction.z = 0.0f;
    			direction.Normalize();
    
    			currentVelocity.x = direction.x * speed;
    			currentVelocity.y = direction.y * speed;
    
    		}
    
    		// check Chase->Idle transition
    		if (!targetIsVisible)
    		{
    			currentState = EnemyLogicState.Idle;
    		}
    
    		// check Chase -> Attack transition
    		if (lastSeenDistanceSqr < attackInnerRadius * attackInnerRadius && targetIsVisible)
    		{
    			currentState = EnemyLogicState.Attack;
    			currentVelocity.x = 0.0f;
    			currentVelocity.y = 0.0f;
    			// start firing
    			if (fireController)
    				fireController.StartFiring();
    		}
    
    		bodyRigid.LinearVelocity = currentVelocity;
    	}
    
    	private void ProcessAttackState()
    	{
    		// check Attack -> Chase transition
    		if (lastSeenDistanceSqr > attackOuterRadius * attackOuterRadius || !targetIsVisible)
    		{
    			currentState = EnemyLogicState.Chase;
    			// stop firing
    			if (fireController)
    				fireController.StopFiring();
    		}
    	}
    }

    PlayerLogic.cs

    源代码 (C#)
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    
    [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- this line is generated automatically for a new component
    public class PlayerLogic : Component
    {
    	private Health health = null;
    	public GameController gameController = null;
    	private void Init()
    	{
    		// grab the Health component
    		health = node.GetComponentInChildren<Health>();
    	}
    	
    	private void Update()
    	{
    		// apply damage to the player's health
    		if (health != null && health.IsDead)
    		{
    			// delete the player
    			node.DeleteLater();
    			Game.Player = null;
    			// change the game state to Lose
    			gameController.state = GameState.Lose;
    		}
    	}
    }
  3. Create a NodeDummy, name it gameplay_systems, and assign the GameController component to it.创建一个NodeDummy,将其命名为gameplay_systems,并将GameController组件分配给它。

  4. Drag and drop gameplay_systems to the Game Controller field of the player and robot_enemy.gameplay_systems拖放到playerrobot_enemyGame Controller字段中。

Trying Out
尝试#

Now you are ready to add more enemies and see if you can fight off an attack by a gang of angry robots!现在你已经准备好添加更多的敌人,看看你是否可以击退一群愤怒的机器人的攻击!

最新更新: 2024-08-16
Build: ()