说实话今天是二十三号,不过我在补二十二号的博客,现在已经是晚上了,今天也发生了很多有趣的事情比如遇到了打伞也没有用的雨,倔强的我就算全身湿透也不愿放弃外出欣赏风景的机会,于是我买了一身新衣服加人字拖,松花江畔独自小酌。享受孤独和美好的景色也是生活的一部分。
让我们来进行一个回忆,这个过程可能和我第一天的日记有一些重叠,不过不要在意,因为这是我从第一个游戏又做到第六个游戏了,刚开始我直接做第六个游戏,出现了一系列问题,我就决定先打一下基础,了解一下这个软件的逻辑结构和代码使用方式。
1、挂上角色和场景
2、创建一个组件并将角色挂在上面
3、在组件上挂上rigidbody和collider
4、创建C#脚本控制角色
5、由于这个游戏存在AI(人工智能——敌人),且和玩家角色的 很多行为都一样,这里我们选择利用C#中的继承,将玩家和敌人的共同点都放在一个公共的Character上,这会减少代码量让代码显得有层次感。
6、创建我们需要的变量和需要实例化的接口:
protected CharacterController cc;//这里是cc控件
protected Animator animator;//动画控件
protected bool rotateComplete = true;//旋转是否完成(转身)
Vector3 pendingVelocity;//向量——》速度
public float runSpeed;//跑动速度
public float jumpPower;//跳跃瞬间给速
public int damage = 100;//一次攻击的伤害
public int health = 100;//生命值
public GameObject deathFX;//死亡特效,需要用GameObject点出来
取到控件:
private void Awake()
{
cc = GetComponent<CharacterController>();//得到当前CharacterController脚本
animator = GetComponentInChildren<Animator>();//得到角色下的Animator,子节点下的animator(model)
}
由于我们将Character脚本挂在了Character控件上而不是Player,且Player是Character的子节点,这时候要想得到子节点的脚本就需要用GetComponentInChildren方法。
刷新方法,这个是比较重要的,因为每一帧都要检测,且大部分方法都要在这里进行调用以对游戏产生实时的影响:
private void Update()
{
//移动
pendingVelocity.z = 0f;//z轴速度锁定
cc.Move(pendingVelocity * Time.deltaTime);//移动方向为速度乘以前后帧差时间
//更新动画
animator.SetFloat("Speed", cc.velocity.magnitude);//magnitude返回了向量的长度,即距离(0,0,0)。最常用来返回物体的移动速度,这个只能读取,也可以用标准化后除以速度
animator.SetBool("Grounded", cc.isGrounded);//methodname,bool
//更新重力
pendingVelocity.y += cc.isGrounded ? 0f : Physics.gravity.y * 10f * Time.deltaTime;//在地上就没有y速度,没在地上就要受到重力作用,单位i力*g*时间帧差
AttackCheck();
}
这里其实有一个小问题,就是pendingVelocity的值在哪里取到
更新动画的方法就是对animator中设置的过程判断量进行传入,对其值进行更新。
更新重力是为了让物体飞起来的时候有一个竖直向下大小-10m/s^2的加速度,判断条件就是在不在地上
至于最后的攻击检测,就是看这个角色有没有被攻击,不管是人或者是敌人都有被攻击的可能。
下面对各个模块进行说明:
public void Jump()
{
if (cc.isGrounded)//在地上
{
pendingVelocity.y = jumpPower;//跳跃后初始速度
}
}
很简单的跳跃方法,先利用自带的charactercontroller组件中的isGrounded方法得到物体是否在地上,再巨顶是否进行y轴速度的改变(跳跃)
public void Move(float inputX)//移动函数
{
pendingVelocity.x = inputX * runSpeed;//x轴速度大小为,单位方向乘以运动的速度runSpeed
}
移动函数,其实和跳跃函数差不多,本质就是对物体速度对应轴速度的改变,不过这里有方向和速度,两部分分开给,明显这里的inputX是要用我们的getAxis方法来得到,而runspeed则需要我们来自己设置,来确定速度的大小。
public void Rotate(Vector3 lookDir, float turnSpeed)//转向速度
{
rotateComplete = false;//旋转未完成
// var targetPos = transform.position + lookDir;//目标位置是原位置加上方向向量后的位置
var characterPos = transform.position;//取到现在的位置
//去掉y轴影响
//characterPos.y = 0;
// targetPos.y = 0;
//角色面朝的向量
// Vector3 faceToDir = targetPos - characterPos;//目标向量减去原向量,也就是LookDir
Vector3 faceToDir = lookDir;
//角色面朝目标方向的四元数
Quaternion faceToQuat = Quaternion.LookRotation(faceToDir);
//球面插值
Quaternion slerp = Quaternion.Slerp(transform.rotation, faceToQuat, turnSpeed * Time.deltaTime);
if (slerp == faceToQuat)
{
rotateComplete = true;
}
transform.rotation = slerp;
}
这个比较复杂,我决定先看一遍代码明天早上再复习一遍然后写这个解析
2019年7月24日 09:03:40
我们在上面声明的bool变量在这里派上了用场,用来判断旋转是否完成。刚开始赋值。其实这个变量本质上只在这个旋转方法里面用,在外部并不会影响什么,因此初值对结果没有影响。其中取消y影响的代码已经被我注释了,因为对物体的运动感不影响,我不知道这段代码的作用,也不知道自己是不是以后会知道,先标出来。这里他用两个向量的插值来确定面向的方向,而实际上在二维空间内,横方向只有正负一,因此我们可以直接把接收来的lookDIr来用于面向方向的判断,不需要进行减法直接进行赋值。——>下面用到了一个库(方法),四元数库。我们通过角色要面向的方向可以通过Quaternion.LookRotation方法来得到一个四元数类型的字段,我们需要通过这个四元数字段来当作形参来调用Slerp方法。:Quaternion.Slerp(transform.rotation, faceToQuat, turnSpeed * Time.deltaTime);
这里的理解方法是从transform.rotation旋转到faceToQuat(四元数)这个角度,且旋转速度为每一帧以turnSpeed进行转动,一旦我们新创建的四元数和我们通过面向方向得到的四元数相等了,旋转就完成了,transform.rotation也就变成了slerp(四元数)
死亡函数:
public void Death()
{
var fx = Instantiate(deathFX, transform.position, Quaternion.Euler(Vector3.zero));
Destroy(fx, 2);
Destroy(gameObject);
}
Instantiate是用来复制东西的,在官方API中Quaternion.Euler:Returns a rotation that rotates z degrees around the z axis, x degrees around the x axis, and y degrees around the y axis.
意思是绕指定轴旋转指定度数。而instantiate是复制一个deathFX物体并旋转Vector3.zero角度后放在Transform.position位置。//加载物体5秒后销毁游戏物体 Destroy (gameObject, 5);
可见我们在两秒之后摧毁对应游戏组件再摧毁玩家角色本身。特效在人消失的两秒后消失。
承受伤害函数:
public void TakeDamage(Character inflicter, int damage)
{
inflicter.Jump();
health -= damage;
if (health<=0)
{
Death();
}
}
敌人和我们都可以受到伤害,因此这个方法两方都有调用,不过这里存在同一命名空间的自调用,因此一起说明。
public void AttackCheck()
{
var dist = cc.height / 2;
//向下射线检测
RaycastHit hit;
if (Physics.Raycast(transform.position, Vector3.down, out hit, dist + 0.05f))
//public static bool Raycast(Vector3 origin, Vector3 direction, out RaycastHit hitInfo, float maxDistance = Mathf.Infinity, int layerMask = DefaultRaycastLayers, QueryTriggerInteraction queryTriggerInteraction = QueryTriggerInteraction.UseGlobal);
{
if (hit.transform.GetComponent<Character>()&&hit.transform!=transform)
{
hit.transform.GetComponent<Character>().TakeDamage(this, damage);
}
}
}
这人受到伤害跳一下的设定真的很超级玛丽,生命值自减也无所谓,生命值缩减到零或者零之下死亡也没毛病,主要是谁来调用函数。可见,上面的先声明了一个变量来接收角色的高度,而这里要利用的就是射线,通过设置射线的长度和方向、起点,来判断我们是否踩中了敌人的头部Physics.Raycast(transform.position, Vector3.down, out hit, dist + 0.05f)
,取到当前角色位置,竖直向下方向,长度为dist+0.05,判断hit是否成功。
hit若能得到被hit的角色组件并击中的不是施加攻击的角色,则会给被hit角色一个伤害。即:hit.transform.GetComponent<Character>().TakeDamage(this, damage);
说实话,前面这一大串:hit.tansform.GetComponent(),其实就是一个Character,如果前面要是命名了一个变量来接收,就不用写。其中hit.transform就挂着游戏对象,因此这里的this就是对应的击中角色,damage代表掉血。
下面看AI敌人的制作方法
下面看AI敌人的制作方法
下面看AI敌人的制作方法
下面看AI敌人的制作方法
下面看AI敌人的制作方法
下面看AI敌人的制作方法
下面看AI敌人的制作方法:
Character character;
float lastCheckStateTime = 0;
float simulateInputX;
bool simulateJump;
void Start()
{
character = GetComponent<Character>();
}
void Update()
{
if (Time.time > lastCheckStateTime + 2)//判断时间间隔是否超过两秒
{
lastCheckStateTime = Time.time; //改变检测时间的前time秒
simulateInputX = Random.Range(-1f, 1f);//输入的X值在正负一之间取到
simulateJump = Random.Range(0, 2) == 1 ? true : false;//跳跃
}
MoveControl(simulateInputX);
JumpControl(simulateJump);
}
void MoveControl(float inputX)
{
character.Move(inputX);
if (inputX != 0)
{
var dir = Vector3.right * inputX;//转向方向为单位向量乘以接收的正负一,其实说到底也是-+1
character.Rotate(dir, 10);
}
}
void JumpControl(bool jump)
{
if (jump)
{
character.Jump();//调用跳跃方法
simulateJump = false;//每次AI跳跃之后一定要把simulateJump给关了
}
}
这里是直接对方法的调用,不过是由自己随机输入来传给他的父亲命名空间。再创建一个脚本,自己写一个相机:
public Transform target;//位置类型的目标,玩家的transform
public float distance = 8.0f;//与目标角色的距离
public float hight = -1.0f;//相机的高度
private void LateUpdate()//刷新函数
{
if (!target)//目标未选定
{
return;
}
transform.position = target.position;//取到角色的位置
//Z轴间隔距离
transform.position -= Vector3.forward * distance;//在Z轴与角色有一定的距离
//transform.position =transform.position - Vector3.forward * distance;
//高度调整
transform.position = new Vector3(transform.position.x, hight, transform.position.z);//重置高度
}