1,创建坦克类
在c++中继承pawn命名为tank,创建c++类TankPlayerController继承PlayerController
TankPlayerController中#include "Tank_Pawn"会报错,解决办法
先编译一下,然后先选择Development Editor再选择Development报错就修复了
注意的是,命名一个类,类名为TankrController但是在项目中前面要加一个A,这个容易忽略
由于在controller中通过getPawn函数就能获取到控制的角色,TankPlayerController中getPawn拿到的是BP_tank坦克蓝图(因为GameMode的默认DefaultPawn是拖动到游戏中的蓝图坦克),它继承的是pawn,没法转成ATank因此要把BP_Tank-->类设置--->父类改成Tank
创建TankAIController继承AIController,敌人坦克不仅要拿到自身的pawn还要拿到主角tank
加入头文件
函数:
找到BP_tank(这是拖动游戏中的坦克蓝图,它继承c++写好的tank)将属性AIController改成TankAIController
TankPlayerController是c++写的,要把UI界面的坦克瞄准点显示要放到蓝图中的controller,因此,右键创建子类的蓝图类BP_TankPlayerController。在beginPlay中add 当前widget到当前窗口
当前坦克没有物理特性,添加物理特性,在坦克蓝图类中属性Physics--->Simulate Physics打钩,MassInKg输多少公斤
2,找到坦克瞄准点对应的空间坐标
坦克每时每刻都要判断当前瞄准点对准的物体是否是坦克,因此在TankPlayerController中的Tick中
在下面函数中传入一个三维坐标HitLocation找到瞄准点撞到的物体坐标
要找到撞击到物体的点,首先找到起点和射线方向,起点就是相机的点,方向这样求,通过GetViewportSize传入两个int值得到当前分辨率下横向和纵向的像素,由于屏幕中的瞄准点在屏幕中横向二分之一(CrosshariXLocation)纵向三分之一(CrosshairYLocation)。因此,横向像素乘以CrosshariXLocation得到瞄准点的屏幕坐标
通过DeprojectScreenPositionToWorld函数传入瞄准点的屏幕坐标,得到屏幕坐标转到世界空间中的点和方向,这个方向就是我们要求的射线方向
上面的函数通过将屏幕坐标转化为方向,DeprojectScreenPositionToWorld函数主要判断这个方向上屏幕这个点上有没有撞击点,返回的bool如果是true,才进行下面的射线检测
然后根据该方向(WorldDirection),求出起点,通过PlayerCameraManager拿到相机位置,该位置+方向*10000得到终点,通过GetWorld的射线函数输入起始点和终点得到该射线碰到任意物体的碰撞点
3,添加炮塔和炮管
由于射击时旋转炮塔,因此要用c++写,创建TankTurrent继承StaticMeshComponent,第一行的意思是让该类可以在蓝图类中可以添加,倒数第二行是在定义了炮筒移动速度为20后让该属性可以在蓝图的属性中可以自己调整
这样,在坦克的蓝图类就可以添加该c++类到蓝图中,而且右侧属性也有了写好的移动速度
这里的射击不同于一般的技能射击,它是先让Tank传射击点给火控组件Aiming Component,然后旋转炮塔旋转到目标,再移动炮管的上下位置,调整好角度再发射炮弹,因此还得用c++写炮管TankBarrel
c++创建TankAimingComponent继承Actor Component,include炮管和炮塔,起点是炮管的位置,我们在炮管蓝图中右下角Socket Manager中添加一个Socket(节点),
TankAimingComponent写个函数拿到炮管和炮塔,函数声明这样写,因为要在坦克蓝图中调用该c++类的函数把炮管和炮塔传进去,第一行意思是蓝图中可调用该函数
函数里这样写:
在蓝图中beginPlay就拿到炮筒和炮塔
TankAimingComponent中写个函数AimAt,在里面拿到Barrel->GetSocketLocation的起始位置,通过SuggestProjectileVelocity函数传入起始和终点坐标得到角度,得到最后的FireVector,通过.GetSafeNormal单位化
然后知道了向量FireVector就调用炮塔的旋转函数,
得到炮塔前向向量的Rotation和目标Rotation,然后它们的Yaw相减,得到yaw插值,
要让炮塔转动时平滑旋转而不是一下子转过去,该函数因为在Controller中的tick调用,tick是每帧都执行,因此让它每秒转20度(MaxDegreesPerSecond),那么每帧就转多少度呢?
答:每帧转 20*每帧的时间,得到的就是每帧转的度数,那么每帧时间用DeltaTimeSeconds获得,也即从上一帧到当前帧的时间
得到的RotationChange是每帧旋转的角度,然后加上当前的Rotation,并通过SetRelativeRotation来设置当前帧的旋转Rotation,这样就实现了旋转的平滑过渡
bug:上图倒数第二句话新的Rotation=变化的Rotation+当前的Rotation,但是当前的Rotation是通过GetForwardVector获得,如果坦克移动的时候,GetForwardVector向前的方向也在变化,因此,要把CurrentRotation改成RelativeRotation
float NewRotationYaw=RotationChange+RelativeRotation.Yaw
同理炮管的上下移动也一样
效果:
bug:炮塔在转到180度后再向右转时突然向左绕了一大圈,也即转的是补角,这是由于180度转过后变成了-179度
解决办法:加上判断,如果changeYaw为负数,加上360度即可
4,实现开火功能
创建炮弹,创建Projectile的C++文件继承Acor,因为要设置子弹的样子,右键该类,创建基于该c++文件的蓝图类BP_Projectile,在Projectile中要生成子弹因此
在属性中指定生成的子弹是哪个蓝图。
在c++类TankAimingComponent中写一个Fire函数,该函数可在蓝图中调用,鼠标左键触发Fire函数
在坦克点击鼠标发射炮弹时,炮弹生成但是不会向前走,以前项目中是在炮弹蓝图中添加一个ProjectileMovement,设置初始速度,但是在这个项目中要在c++类中生成ProjectileMovement,在Projectile中include
声明
在构造函数中,生成该组件
然后写发射子弹的函数
然后添加装弹间隔,设置一个private的double值LastFireTime,发射一次就通过FPlatformTime::Seconds记录当前时间,判断时间间隔是否超过开火时间
5,用枚举值设置当前坦克状态并根据不同状态让准星显示不同颜色
设置当前坦克的状态,在火控类中最前面声明一个枚举
在火控组件中的每一帧都进行判断并设置当前状态,如果当前炮塔旋转角度大于3度设置为瞄准状态,如果填弹时间不够,就为填弹状态
要让准星显示不同颜色,找到准星的UI,找到Color属性,点击bind打开绑定函数,添加变量,类型为Aiming Component的蓝图,给不同的枚举值赋予不同颜色,右键输入toColor就可以设置颜色,连线
虽然Get了变量的类型为Aiming Component但是没有设置(Set)它的引用到底是什么。我们需要在Controller的beginPlay中把它的引用设置给UI,因为Aiming Component这个类可以通过坦克get Component by Class拿到,而拿到Tank需要在TankPlayerController中调用GetControllerTank方法,为了让GetControlledTank函数在蓝图中被调用
这样,坦克在不同状态下准星的颜色就会变化
6,设置坦克移动
创建c++类tankTrack继承staticMeshComponent,命名为TankTrack,在该类中定义加速度(TankMaxDrivingForce)和施加力的百分比(SetThrottle)
要让履带行走,不仅要计算当前给履带施加力的大小,同时也要计算力的施加位置
施加力的大小就是前向向量*力的大小*力的百分比,施加力的位置就是当前组件的Location,那么给谁施加力呢?给履带还是给坦克?应该是给父物体施加这个力,这个力作用在履带上,那么要找到父物体,GetOwner得到该Actor,通过GetRootComponent得到TankBody
要调用GetOwner()—>GetRootComponent()的AddForceAtLocation方法发现没有这个函数这是因为该函数返回的是一个SceneComponent,该组件只有位置信息没有collision属性。
我们通过层级关系发现SceneComponent下的子类PrimitiveComponent组件具有Collision属性,因此强转成子类,然后调用AddForceAtLocation方法即可
设置q和e键控制左履带和右履带的行走,调用c++类的Set Throttle函数
2,让坦克在空中的时候按住移动键还能移动,在蓝图中的做法是,找到履带,属性中绑定Hit事件
但是要用c++来做,步骤复写BeginPlay函数,在BeginPlay中绑定Hit事件
在OnHit中产生撞击才会调用施加力的效果
3,进一步完善移动功能,坦克把移动交给移动组件,该组件处理后再交给履带移动,创建类TankMovementComponent继承NavMovementComponent
首先在该类的初始化函数中拿到左右履带的引用,
在蓝图中:
然后创建向前走和向右走的函数,调用两个履带移动
在蓝图中:
bug1:解决坦克松开移动按键后要溜很久才停下:
在TankBody属性中修改Linear Damping和Angular Damping
bug2:在坦克开始移动时,头部突然翘起,
解决办法:
因为施加力作用在履带上,把力的位置放在履带前面,在履带前面Create Socket添加一个位置,放在履带前面
代码中即可
bu:3:解决坦克移动时候出现漂移效果:
产生原因:在移动时坦克向右前方移动会产生向右的力,通过求当前的速度方向在RightVector方向上的点积得到该速度在右边的分量速度大小SideSpeed,我们应当在右侧分量上给它一个相等的向左的力让其抵消,向左的力=质量*加速度,加速度SideSpeedAcceleration=速度/时间*方向,最后由于给两个履带施加力,所以力除以2