黑魂复刻第一季(傅老师)

1.玩家输入模块Player Input Module

按键输入原理

灵魂画师博老师:

image-20220720143557788

Key (键盘输入)–> PI脚本(PlayerInput) --> signal(信号)
Joy (手柄输入) --> PI脚本(PlayerInput) --> signal(信号)

创建场景

在Hierarchy窗口创建Capsule(胶囊)和Plane(平面),并重置坐标值,Capsule的Position Y值抬高1

添加按键输入

在Capsule新建一个PalyerInput脚本

// Variable
public string keyUp = "w";
public string keyDown = "s";
public string keyLeft = "a";
public string keyRight = "d";

效果如下:
image-20220720141645144

把输入的信号转换成电脑能读懂的signal

public float Dup;
public float Dright;

void Update ()
{
    Dup = (Input.GetKey(keyUp)?1.0f:0) - (Input.GetKey(keyDown)?1.0f:0); //三目运算符
    Dright = (Input.GetKey(keyRight)?1.0f:0) - (Input.GetKey(keyLeft)?1.0f:0); 
}

2.输入衰减与使能旗飘 Damping and Enable Flag.

SmoothDamp平滑缓冲

代码如下:

// Variable
public string keyUp = "w";
public string keyDown = "s";
public string keyLeft = "a";
public string keyRight = "d";

public float Dup;
public float Dright;

private float targetDup; //目标值
private float targetDright;
private float velocityDup; //暂存值
private float velocityDright;

void Update ()
{
    targetDup = (Input.GetKey(keyUp)?1.0f:0) - (Input.GetKey(keyDown)?1.0f:0); //三目运算符
    targetDright = (Input.GetKey(keyRight)?1.0f:0) - (Input.GetKey(keyLeft)?1.0f:0); 
    
    Dup = Mathf.SmoothDamp(Dup,targetDup,ref velocityDup,0.1f);
    Dright = Mathf.SmoothDamp(Dright, targetDright, ref velocityDright, 0.1f);
}

smoothdamp(平滑缓冲)东西不是僵硬的移动而是做减速缓冲运动到指定位置
Mathf.SmoothDamp(需要修改的参数, 目标值 ,ref 暂存值,平滑所需的时间);

添加关闭输入开关

public bool inputEnabled = true; //Flag

void Update ()
{
    if (!inputEnabled)
    {
        targetDup = 0;
        targetDright = 0;
    }
}

3.导入模型Import Models

导入材质更改参数

  • 把材质和模型导入Project文件夹中

  • 添加Plane,并把地板贴图并拖入材质球中,Tiling属性设为9,9,地板大小也放大三倍(扩大地图)

  • capsule改名为PlayerHandle并放入人物模型,成为PlayerHandle子物件,remove(删除)掉Mesh Renderer(网格渲染器)和Mesh Filter(网格过滤器)

  • 调整PlayerHandle和人物模型的位置及大小

更改材质密度大小

image-20220702095845832

scene 中的gizmos设置(优化场景显示)

image-20220721224353457

3d lcons 调整场景图标大小

selection Qutline 取消显示模型外边框

4.新增动画控制器Add Animator Controller

  • 在Assets窗口中新建一个folder改名为Animator,并在Animator文件夹中新建一个Animator Controller名字就叫Actor
  • 没有animator创建就在windows-animator窗口可以找到
  • 放入idle和walk动画,并给人物模型的Animator的controller加入刚刚创建名为Actor的动画控制器
  • 在Project窗口找到人物模型,在rig-Animation Type的属性改为Humanoid(关于这个的详细设置以后再更新)
  • Hierarchy窗口的人物模型animator组件的Apply Root Motion(根运动)选项消勾

5.一维混合树1D Blend Tree

一个混合树可以包含多个动画

右键单击 Animator Controller 窗口上的空白区域。 从显示的上下文菜单中,选择 Create State > From New Blend Tree。 双击混合树 (Blend Tree) 以进入混合树视图 (Blend Tree Graph)。

把BlendTree改名为ground,双击Blend改名为forward,在inspector窗口加两个motion filed并放入动画

image-20220724114848868

6.串联玩家控制与角色控制模块Connect PI and AC modules

在PlayerHandle添加一个名为ActorController的脚本

image-20220724115759797

代码如下:

//ActorController
public GameObject modle;	//挂载一个GameObject名为modle
public PlayerInput pi;		//挂载一个pi叫PlayerInput

[SerializeField]//序列化的数据类型可以通过这个特性显示到编辑器上
private Animator anim;	//声明一个animator组件,把ybot子物体拖进去

void Awake() //awake>enable>start
{
    pi = GetComponent<PlayerInput>();	//得到pi的PlayerInput脚本
    anim = model.GetConmponent<Animator>(); //得到modle的animator动画组件
}
void Update()	//帧更新
{
    anim.SetFloat("forward", pi.Dup);	//取pi的Dup变量更改animator的forward的值
}

7.角色行走#1Actor walks#1

image-20220724122702081

Dup和Dright值数范围是-1到1,而animator的forword数值变化值数范围是0到1

因此我们把Dup和Dright的值变为长度m,方向和由Dup和Dright的变化而变化

根据这幅图的原理我们把以前的代码更改一下:

//ActorController
void Update()
{
    anim.SetFloat("forward", Mathf.Sqrt((pi.Dup * pi.Dright) + (pi.Dright * pi.Dright))); //图片的m值
    model.transform.forward = pi.Dright * transform.right + pi.Dup * transform.forward;	//旋转
}

然后我们把代码进行封装一下,更改PallyerInput脚本:

//PallyerInput
public float Dmag;	//D向量模,用来更改动画forward的值
public Vector3 Dvec;

void Update()
{
    Dmag = Mathf.Sqrt((Dup * Dright) + (Dright * Dright));
    Dvec = Dright * transform.right + Dup * transform.forward;
}

然后我们把ActorController脚本改一下,漂漂亮亮的:

//ActorController
void Update()
{
    anim.SetFloat("forward", pi.Dmag); //图片的m值
    model.transform.forward = pi.Dvec	//旋转
}

但是这个代码有个缺陷:停止按键会立刻朝着前方,这个我们下一课进行解决

8.角色行走#2Actor walks#2

我们是以Dup和Dright的大小作为方向的,但是松手后Dup和Dright的值数大小会组件变成0,forward向量的值不能为0导致模型方向重置,解决方法很简单在Dmag到0之前暂停这段代码执行即可:

//ActorController
void Update()
{
    anim.SetFloat("forward", pi.Dmag); //图片的m值
    if (pi.Dmag > 0.1f)
    {
        model.transform.forward = pi.Dvec	//旋转
    }
}
//ActorController
public float walkSpeed = 1.4f

private Rigidbody rb;	//声明一个rigidbody组件
private Vector3 movingVec;	//声明一个模型移动方向

void Awake()
{
    rb = GetComponent<Rigidbody>();
}

void Update()
{
    movingVec = pi.Dmag * model.transform.forward * walkSpeed; //每秒向model.transform.forward移动Dmag米
}
    
void FixedUpdate()  //注意rigidbody一定要在fixedupdate里写,update是每帧执行一次,Fixedupdate是固定每0.02秒执行uici(Unity默认值)
{
    //rb.position += movingVec * Time.fixedDeltaTime; //胶囊速度乘以时间
    rb.velocity = new Vector3(movingVec.x, rb.velocity.y, movingVec.z);
}

完整代码如下:

//PallyerInput
public float Dmag;	//D向量模,用来更改动画forward的值
public Vector3 Dvec;

void Update()
{
    Dmag = Mathf.Sqrt((Dup * Dright) + (Dright * Dright));
    Dvec = Dright * transform.right + Dup * transform.forward;
}
//ActorController
public float walkSpeed = 1.4f
public GameObject modle;	//挂载一个GameObject名为modle
public PlayerInput pi;		//挂载一个pi叫PlayerInput

[SerializeField]//序列化的数据类型可以通过这个特性显示到编辑器上
private Animator anim;	//声明一个animator组件,把ybot子物体拖进去
private Rigidbody rb;	//声明一个rigidbody组件
private Vector3 movingVec;	//声明一个模型移动方向

void Awake() //awake>enable>start
{
    pi = GetComponent<PlayerInput>();	//得到pi的PlayerInput脚本
    anim = model.GetConmponent<Animator>(); //得到modle的animator动画组件
    rb = GetComponent<Rigidbody>();
}
void Update()	//帧更新
{
    anim.SetFloat("forward", pi.Dmag); //图片的m值
    if (pi.Dmag > 0.1f)
    {
        model.transform.forward = pi.Dvec	//旋转
    }
    movingVec = pi.Dmag * model.transform.forward * walkSpeed; //每秒向model.transform.forward移动Dmag米
}
void FixedUpdate()  //注意rigidbody一定要在fixedupdate里写,update是每帧执行一次,Fixedupdate是固定每0.02秒执行uici(Unity默认值)
{
    rb.velocity = new Vector3(movingVec.x, rb.velocity.y, movingVec.z);
}

9.角色奔跑Actor run

在blend tree添加跑步动画

image-20220725142134641

更改Threshold的值如果改不了就把Automate Thresholds选项消勾

因为需要实现跑步功能所以我们要添加新的按键,代码如下:

//PlayerInput
[Header("===== key settings =====")]//序列化,Inspector面板中显示文字
public string keyA;
public string keyB;
public string keyC;
public string keyD;

[Header("===== Output signals =====")]
// 1.pressing signal
public bool run;

void Update()
{
    run = Input.Getkey(keyA);
}

//ActorConntroller
anim.SetFloat("forward", pi.Dmag * ((pi.run) ? 2.0f : 1.0f));
movingVec = pi.Dmag * model.transform.forward * walkSpeed *((pi.run) ? 2.7f : 1.0f); //三元运算符

10.线性插值与球形线性插值Lerp & Slerp

//ActorConntroller
//移动渐变
anim.SetFloat("forward", pi.Dmag * Mathf.Lerp(anim.GetFloat("forward"),(pi.run) ? 2.0f :1.0f),0.5f));
//转向渐变
Vector3 targetForward = Vector3.Slerp(model.transform.forward, pi.DVec, 0.5f);//渐变
model.transform.forward = targetForward;//利用slerp把current值转到目标值实现平滑的转身

11.椭圆映射法Elliptical Grid Mapping

论文地址:https://arxiv.org/ftp/arxiv/papers/1509/1509.06344.pdf

在游戏运行时我们会发现一个问题,玩家斜向移动速度会变快,为了修改这个我们用论文第五页公式。

image-20220725150716443

代码如下:

//PlayerInput
private void SquareToCirclie(Vector2 input)
{
    Vector2 output = Vector2.zero;
    output.x = input.x * Mathf.Sqrt(1 - (input.y * input.y)/2.0f);
    output.y = input.y * Mathf.Sqrt(1 - (input.x * input.x)/2.0f);
    
    returm output;
}
Vector2 tempDAxis = SquareToCirclie (new Vector2(Dright,Dup));
float Dright2 = tempDAxis.x;
float Dup2 = tempDAxis.y;

12.一次性触发控制(Trigger Oonce Signal)

实现跳跃功能:

//PlayerInput
// 2. trigger once signal
public bool jump;
private bool lastJump;

void Update ()
{
    bool newJump = Input.GetKey (keyB);
    if (newJump != lastJump && newJump == true)//newJump == true,解释如下图
    {
        jump = true;
    }
    else 
    {
        jump = false;
    }
    lastJump = newJump;
}

这种触发器按下会触发两次信号,所以我们要触发前面的信号:

image-20220725154850238

13.新增跳跃动画Jump Animation

animator窗口添加Jump动画,详细操作我就不写了毕竟做了我们做很多次,所以直接上代码:

//ActorController
if(pi.jump)
{
    anim.SetTrigger("jump");
}

14.巧妙地重置动画控制器触发Reset Triggers

我们会发现一个问题,跳跃键按两次会自动跳两次,所以我们在ground的Inspector窗口新建一个名为FSMClearSingnals的脚本:

image-20220725183729914

新建脚本会出现以下预设:

  • OnStateEnter:当一个转换开始并且状态机开始评估这个状态时
  • OnStateUpdate:OnStateUpdate 在 OnStateEnter 和 OnStateExit 回调之间的每个更新帧上调用
  • OnStateExit:当转换结束并且状态机完成评估此状态时
  • OnStateMove:OnStateMove 在 Animator.OnAnimatorMove () 之后被调用。处理和影响根运动的代码应该在这里实现
  • OnAnimatorIK:OnStateIK 在 Animator.OnAnimatorIK () 之后被调用。设置动画 IK(反向运动学)的代码应该在这里实现。

代码如下:

public string[] clearAtEnter;
public string[] clearAtExit;

override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
    foreach (var signal in clearAtEnter)
    {
        animator.ResetTrigger(signal);//清除信号
    }
override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
    foreach (var signal in clearAtExit)
    {
        animator.ResetTrigger(signal);
    }
}

image-20220725191601811

15.套出FSM On Enter&Exit方法FSM On Enter&Exit

在jump的Inspector窗口新建一个名为FSMOnEnter和FSMOnExit的脚本:

public string[] onEnterMessages;
override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
    foreach (var msg in onEnterMessages)
    {
        //animator.gameObject.SendMessage(msg);
        animator.gameObject.SendMessageUpwards(msg);//向上发信息
    }
}
public string[] onExitMessages;
override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
    foreach (var msg in onEnterMessages)
    {
        //animator.gameObject.SendMessage(msg);
        animator.gameObject.SendMessageUpwards(msg);//向上发信息
    }
}

image-20220725195233722

然后在ActorController脚本进行拦截:

//ActorController
public void OnJumpEnter()
{
    print ("On jump enter");
}
public void OnJumpExit()
{
    print ("On jump exit");
}

16.锁死平面移动Lock Planar

解决跳跃状态移动的bug

直接在上个脚本改:

//ActorController

private bool lockPlanar = false;//锁死平面移动

void Update()
{
    if (lockPlanar == false)
    {
         movingVec = pi.Dmag * model.transform.forward * walkSpeed;    
    }
}

public void OnJumpEnter()
{
    pi.inputEnabled = false;
    lockPlanar = true;
    //print ("On jump enter");
}
public void OnJumpExit()
{
    //print ("On jump exit");
    pi.inputEnablie = true;
    lockPlanar = false;
}

17.跳跃向量Jump Thrust

//ActorController
public float jumpVelocity = 3.0f;//为了写活跳跃值

private Vector3 thrustVec;	//新增跳跃冲量向量
void FixedUpdate()
{
    rb.velocity = new Vector3(movingVec.x, rb.velocity.y, movingVec.z) + thrustVec;
    thrustVec = Vector3.zero;	//一帧触发然后变成0
}
public void OnJumpEnter()
{
    pi.inputEnabled = false;
    lockPlanar = true;
    thrustVec = new Vector3 (0, jumpVelocity, 0);
    //print ("On jump enter");
}

18.新增降落状态Add Fall State

在动画控制器里添加下落动画名字为fall,并调整好状态机,过程很简单就不详写了

image-20220726213250236

19.新增落地侦测器Add On Ground Sensor

在这里傅老师用的是Physics.OverlapCapsule这个方法用来检测地面撞击,这个函数判定胶囊中有多少碰撞体:

Collider[] OverlapCapsule(Vector3 point0, Vector3 point1, float radius, int layerMask);

image-20220726215000444

在PlayerHandle新建一个空对象,名为sensor。在sensor新建名为OnGroundSensor的脚本,代码如下:

//OnGroundSensor
public CapsuleCollider capcol;	//把PlayerHandle拽进来

private Vector3 point1;
private Vector3 ponit2;
private float radius;	//定义半径值

void Awake ()
{
    radius = capcol.radius;	//把PlayerHandle抓取半径值
}

void FixedUpdate ()
{
    point1 = transform.position + transform.up * radius;
    point2 = transform.position + transform.up * capcol.height - transform.up * radius;
    Collider[] outputCols = Physics.OverlapCapsule(point1, ponit2, radius, LayerMask.GetMask("Ground"));//给地板加个Ground Layer
    if (outputCols.Length != 0)
    {
        foreach (var col in outputCols)
        {
            print ("collision:" + col.name)
        }
    }
}

image-20220726214321556

20.使用落地侦测器Use On Ground Sensor

我们上节课已经写好了落地检测,现在我们来使用它们:

//OnGroundSensor
if (outputCols.Length != 0)
{
        //foreach (var col in outputCols)
        //{
        //    print ("collision:" + col.name)
        //}
    SendMessageUpwards("IsGround");//向上传递信号
}
else 
{
    SendMessageUpwards("IsNotGround");
}

我们在ActorController脚本进行拦截:

//ActorController

public void IsGround()
{
    anim.SetBoll ("isGround", true);
}
public void IsNotGround()
{
    anim.SetBoll ("isGround", false);
}

添加一个布尔类型的isGround信号并在fall->ground添加这个信号:

image-20220726233145573

调整动画优先级防止出现落地时优先播放fall动画,如果还是出现落地播放fall动画可以降低跳跃的高度;或者打开jump->fall动画的setting,启用interruption source下的current stage

image-20220726233658071

image-20220726233609110

21.重新修正降落状态Rearrange Fall State

这样更好的解决了下落的问题:

public void OnGroundEnter()//OnJumpExit改成OnGroundEnter
{
    pi.inputEnablid = true;
    lockPlanar = false;
}

image-20220727161019714

22.失足掉落 Fall form Ground State

动画控制器ground->fall添加一个isGround=false触发条件:

image-20220727161446484

为了防止移动下落没有抛物线的这种情况,我们加一个OnFallEnter:

image-20220727161703370

public void OnFallEnter()
{
    pi.inputEnablid = false;
    lockPlanar = true;
}

出现跳跃发生Fall动画原因:跳跃值太高会产生下落效果,但是下落动画播放之后才会返回所以有下落的僵持效果,我们把Interruption Spirce改为Curremt state打断它即可

image-20220727163134333

23.新增翻滚状态Add Roll State

在动画控制器添加下落root的动画,然后加个roll的trigger信号来检测控制下落,动画控制roll优先

image-20220727170618485

//ActorController
if (rb.velocity.magnitude > 5.0f)
{
    anim.SetTrigger("rool");
}

烘焙roll动画添加翻滚效果:

image-20220727173530968

24.走路翻滚+跑步跳跃Walk Roll and Sprint Jump

走路翻滚:我们要fall从ground进入roll,所以我们添加动画条件

image-20220727180731990

image-20220727181132099

更改动画条件:

image-20220727180822021

image-20220727181223768

由于翻滚没有高度很是不美观,所以我们添加一个向上冲量:

//ActorController
public float rollVelocity = 3.0f;

public void OnRollEnter()
{
    thrustVec = new Vector3 (0, rollVelocity, 0);
    pi.inputEnable = false;
    lockPlanar = true;
}

rool动画添加OnRollEnter来触发

image-20220727181532909

25.新增后跳状态Add Jab State

forward速度在0-1.1之间为roll,1.1-2.1为jump,在0-0.1之间增加jab后跳动作

image-20220727182745398

我们继续添加后跳Jab动画,添加条件,更改优先级,Has Exit Time 消勾,然后再拉回来驾轻就熟…

image-20220727183822225

image-20220727184126246

接下来我们给jab添加向后的冲量,所以我们给jab加之前写好的FSM On Enter脚本,挂上OnJabEnter脚本

image-20220727190233093

添加代码如下:

//ActorController
public float jabVelocity = 3.0f;

public void OnJabEnter()
{
    //thrustVec = new Vector3 (0, 0, -javaVelocity);//错误方法,一个是方向错误,瞬时冲量类似于瞬移
    thrustVec = model.transform.forward * -jabVelocity;//添加一个负的向前向量的值
    pi.inputEnabled = false;
    lockPlanar = true;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吾日心悦如石水

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值