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

射击功能实现

Now that our character is ready, let's implement shooting, add shooting controls, and use raycasting (intersections) to check if the bullet hits the target.现在我们的角色已准备就绪,让我们实现射击功能、添加射击控制,并使用射线检测(相交测试,intersections)来检查子弹是否击中目标。

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:让我们实现一个新的组件来检测开火按钮是否被按下。这是推荐的做法,因为我们将在其他组件中使用这个逻辑:

  • 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 the ShootInput component and copy the following code to it.创建ShootInput组件并将以下代码复制到其中:

    ShootInput.h

    ShootInput.h

    源代码 (C++)
    #pragma once
    #include <UnigineComponentSystem.h>
    class ShootInput :
        public Unigine::ComponentBase
    {
    public:
    	COMPONENT_DEFINE(ShootInput, Unigine::ComponentBase);
    
    	bool isShooting();
    };

    #

    ShootInput.cpp

    ShootInput.cpp

    源代码 (C++)
    #include "ShootInput.h"
    REGISTER_COMPONENT(ShootInput);
    using namespace Unigine;
    using namespace Math;
    
    bool ShootInput::isShooting()
    {
    	// 返回LMBUTTON的当前状态并检查鼠标在屏幕上的捕获情况
    	return Input::isMouseButtonDown(Input::MOUSE_BUTTON_LEFT) && Input::isMouseGrab;
    }

    #

  2. Add the ShootInput property to the player Dummy Object.ShootInput属性(property)添加到player (Dummy Object)

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

    HandAnimationController.h

    HandAnimationController.h

    源代码 (C++)
    #pragma once
    #include <UnigineComponentSystem.h>
    #include <UnigineGame.h>
    #include "FirstPersonController.h"
    #include "ShootInput.h"  
    class HandAnimationController :
        public Unigine::ComponentBase
    {
    public:
    	COMPONENT_DEFINE(HandAnimationController, Unigine::ComponentBase);
    
    	PROP_PARAM(Node, player_node, nullptr);
    
    	PROP_PARAM(Float, moveAnimationSpeed, 30.0f);
    	PROP_PARAM(Float, shootAnimationSpeed, 30.0f);
    	PROP_PARAM(Float, idleWalkMixDamping, 5.0f);
    	PROP_PARAM(Float, walkDamping, 5.0f);
    	PROP_PARAM(Float, shootDamping, 1.0f);
    
    	// 动画参数
    	PROP_PARAM(File, idleAnimation);
    	PROP_PARAM(File, moveForwardAnimation);
    	PROP_PARAM(File, moveBackwardAnimation);
    	PROP_PARAM(File, moveRightAnimation);
    	PROP_PARAM(File, moveLeftAnimation);
    	PROP_PARAM(File, shootAnimation);
    
    	// 注册在World Logic各阶段调用的方法
    	COMPONENT_INIT(init);
    	COMPONENT_UPDATE(update);
    
    	Unigine::Math::vec2 getLocalMovementVector();
    	void shoot();
    
    protected:
    	// 声明在World Logic各阶段调用的方法
    	void init();
    	void update();
    
    private:
    	FirstPersonController *fpsController = nullptr;
    		ShootInput * shootInput = nullptr; 
    
    	Unigine::ObjectMeshSkinnedPtr meshSkinned = nullptr;
    	float currentIdleWalkMix = 0.0f; // 0待机动画 1行走动画
    	float currentShootMix = 0.0f; // 0待机/行走混合 1射击动画
    	float currentWalkForward = 0.0f;
    	float currentWalkBackward = 0.0f;
    	float currentWalkRight = 0.0f;
    	float currentWalkLeft = 0.0f;
    
    	float currentWalkIdleMixFrame = 0.0f;
    	float currentShootFrame = 0.0f;
    	int numShootAnimationFrames = 0;
    
    	// 设置动画层数量
    	const int numLayers = 6;
    };

    #

    HandAnimationController.cpp

    HandAnimationController.cpp

    源代码 (C++)
    #include "HandAnimationController.h"
    
    REGISTER_COMPONENT(HandAnimationController);
    using namespace Unigine;
    using namespace Math;
    
    Unigine::Math::vec2 HandAnimationController::getLocalMovementVector()
    {
    		return Math::vec2(
    			Math::dot(fpsController->getSlopeAxisY(), fpsController->getHorizontalVelocity()),
    			Math::dot(fpsController->getSlopeAxisX(), fpsController->getHorizontalVelocity())
    		);
    
    }
    
    void HandAnimationController::init()
    {
    	fpsController = ComponentSystem::get()->getComponent<FirstPersonController>(player_node);
    
    	 shootInput = ComponentSystem::get()->getComponent<ShootInput>(player_node); 
    
    	// 获取组件所属节点
    	// 并转换为ObjectMeshSkinned类型
    	meshSkinned = checked_ptr_cast<Unigine::ObjectMeshSkinned>(node);
    
    	// 为每个对象设置动画层数量
    	meshSkinned->setNumLayers(numLayers);
    
    	// 为每层设置动画
    	meshSkinned->setLayerAnimationFilePath(0, FileSystem::guidToPath(FileSystem::getGUID(idleAnimation.getRaw())));
    	meshSkinned->setLayerAnimationFilePath(1, FileSystem::guidToPath(FileSystem::getGUID(moveForwardAnimation.getRaw())));
    	meshSkinned->setLayerAnimationFilePath(2, FileSystem::guidToPath(FileSystem::getGUID(moveBackwardAnimation.getRaw())));
    	meshSkinned->setLayerAnimationFilePath(3, FileSystem::guidToPath(FileSystem::getGUID(moveRightAnimation.getRaw())));
    	meshSkinned->setLayerAnimationFilePath(4, FileSystem::guidToPath(FileSystem::getGUID(moveLeftAnimation.getRaw())));
    	meshSkinned->setLayerAnimationFilePath(5, FileSystem::guidToPath(FileSystem::getGUID(shootAnimation.getRaw())));
    
    	int animation = meshSkinned->getLayerAnimationResourceID(5);
    	numShootAnimationFrames = meshSkinned->getLayerNumFrames(5);
    
    	// 启用所有动画层
    	for (int i = 0; i < numLayers; ++i)
    		meshSkinned->setLayerEnabled(i, true);
    }
    
    void HandAnimationController::shoot()
    {
    	// 启用射击动画
    	currentShootMix = 1.0f;
    	// 将动画层帧数重置为0
    	currentShootFrame = 0.0f;
    }
    
    void HandAnimationController::update()
    {
    	vec2 movementVector = getLocalMovementVector();
    
    	// 检查角色是否在移动
    	bool isMoving = movementVector.length2() > Math::Consts::EPS;
    	// 输入处理:检查开火键是否按下
    	if (shootInput->isShooting())
    		shoot(); 
    
    	// 计算各层权重的目标值
    	float targetIdleWalkMix = (isMoving) ? 1.0f : 0.0f;
    	float targetWalkForward = (float)Math::max(0.0f, movementVector.x);
    	float targetWalkBackward = (float)Math::max(0.0f, -movementVector.x);
    	float targetWalkRight = (float)Math::max(0.0f, movementVector.y);
    	float targetWalkLeft = (float)Math::max(0.0f, -movementVector.y);
    
    	// 应用当前层权重
    	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);
    
    	// 更新动画帧:为所有层设置相同帧数确保同步
    	meshSkinned->setLayerFrame(0, currentWalkIdleMixFrame);
    	meshSkinned->setLayerFrame(1, currentWalkIdleMixFrame);
    	meshSkinned->setLayerFrame(2, currentWalkIdleMixFrame);
    	meshSkinned->setLayerFrame(3, currentWalkIdleMixFrame);
    	meshSkinned->setLayerFrame(4, currentWalkIdleMixFrame);
    	// 将每层动画当前帧设置为0重新开始播放
    	meshSkinned->setLayerFrame(5, currentShootFrame);
    
    	currentWalkIdleMixFrame += moveAnimationSpeed * Game::getIFps();
    	currentShootFrame = Math::min(currentShootFrame + shootAnimationSpeed * Game::getIFps(), (float)numShootAnimationFrames);
    
    	// 平滑更新当前权重值
    	currentIdleWalkMix = Math::lerp(currentIdleWalkMix, targetIdleWalkMix, idleWalkMixDamping * Game::getIFps());
    
    	currentWalkForward = Math::lerp(currentWalkForward, targetWalkForward, walkDamping * Game::getIFps());
    	currentWalkBackward = Math::lerp(currentWalkBackward, targetWalkBackward, walkDamping * Game::getIFps());
    	currentWalkRight = Math::lerp(currentWalkRight, targetWalkRight, walkDamping * Game::getIFps());
    	currentWalkLeft = Math::lerp(currentWalkLeft, targetWalkLeft, walkDamping * Game::getIFps());
    
    	currentShootMix = Math::lerp(currentShootMix, 0.0f, shootDamping * Game::getIFps());
    }

    #

Using Raycasting
使用射线检测实现射击#

To implement shooting, you can use the properties of the PlayerDummy camera. This camera has its -Z axis pointing at the center of the screen. So, you can cast a ray from the camera to the center of the screen, get the intersection, and check if you hit anything.要实现射击功能,可以利用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 if the ray intersects with any object's surface (that has the matching Intersection mask to restrict the check results). If the intersection with such surface is detected, the method returns the hitObject and hitInfo values (the intersection point and normal).在下面的组件代码中,我们将存储两个点(p0, p1):摄像机点和鼠标指针点。getIntersection()方法将从p0向p1发射射线,并检查射线是否与任何物体表面相交(该表面需具有匹配的Intersection遮罩以限制检查结果)。如果检测到与这样的表面相交,该方法将返回hitObjecthitInfo值(交点和法线)。

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

    WeaponController.h

    WeaponController.h

    源代码 (C++)
    #pragma once
    #include <UnigineComponentSystem.h>
    #include <UnigineVisualizer.h>
    #include "ShootInput.h"
    
    class WeaponController :
        public Unigine::ComponentBase
    {
    public:
    	COMPONENT_DEFINE(WeaponController, Unigine::ComponentBase);
    
    	PROP_PARAM(Node, shooting_camera, nullptr);
    	PROP_PARAM(Node, shoot_input_node, nullptr);
    
    	Unigine::PlayerDummyPtr shootingCamera = nullptr;
    	ShootInput *shootInput = nullptr;
    	int damage = 1;
    
    	// 定义子弹可以击中的物体的相交检测遮罩
    	int mask = ~0;
    
    	// 注册在World Logic各阶段调用的方法
    	COMPONENT_INIT(init);
    	COMPONENT_UPDATE(update);
    
    	void shoot();
    
    protected:
    	// 声明在World Logic各阶段调用的方法
    	void init();
    	void update();
    };

    #

    WeaponController.cpp

    WeaponController.cpp

    源代码 (C++)
    #include "WeaponController.h"
    REGISTER_COMPONENT(WeaponController);
    using namespace Unigine;
    using namespace Math;
    
    void WeaponController::shoot()
    {
    
    	// 设置射线起点(p0)为摄像机位置,终点(p1)为摄像机方向100单位外的点
    	Vec3 p0 = shootingCamera->getWorldPosition();
    	Vec3 p1 = shootingCamera->getWorldPosition() + (Vec3)shootingCamera->getWorldDirection() * 100;
    
    	// 创建用于存储交点法线的对象
    	WorldIntersectionNormalPtr hitInfo = WorldIntersectionNormal::create();
    	// 获取(p0,p1)射线第一个相交的物体
    	Unigine::ObjectPtr hitObject = World::getIntersection(p0, p1, mask, hitInfo);
    	// 如果检测到相交
    	if (hitObject)
    	{
    		// 使用Visualizer在击中点渲染表面法线向量
    		Visualizer::renderVector(hitInfo->getPoint(), hitInfo->getPoint() + (Vec3)hitInfo->getNormal(), vec4_red, 0.25f, false, 2.0f);
    	}
    }
    
    void WeaponController::init()
    {
    	// 获取已分配ShootInput组件的摄像机
    	shootingCamera = checked_ptr_cast<Unigine::PlayerDummy>(shooting_camera.get());
    
    	// 获取分配给'shoot_input_node'节点的ShootInput组件
    	shootInput = ComponentSystem::get()->getComponent<ShootInput>(shoot_input_node.get());
    }
    
    void WeaponController::update()
    {
    	// 处理用户输入:检查"开火"按钮是否被按下
    	if (shootInput->isShooting())
    		shoot();
    }

    #

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

  2. Add the component to the player Dummy Object.将组件添加到playerDummy Object上。
  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 Object to the Shoot Input field.playerDummy Object分配给Shoot Input字段。

To view the bullet-surface intersection points and surface normals in these points, you can enable Visualizer when the application is running:要在应用程序运行时查看子弹与表面的交点及交点处的表面法线,可以启用可视化工具:

  1. Open the console by pressing ~~键打开控制台。
  2. Type show_visualizer 1输入show_visualizer 1命令。

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

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