又整了个小Demo,感觉程序化动画还挺好玩。先上效果图,使用到的所有模型均来源于网络。

程序化动画生成守护者移动

实现思路:守护者共6条腿,初始化先激活两条腿14可移动,每移动完一条腿顺序激活下一条腿移动并取消激活移动完的腿(1移动完激活2并关闭1);守护者身上带有一个Controller包含了6指示器,分别往地上发射射线寻找对应腿的落点,找到落点让腿移动;Controller带着6各指示器去追林克,然后守护者的躯体去追Controller。

球球是Controller,带6个指示器,去追林克,身体去追Controller

6条腿需要用IK来实现,本文直接使用了Final IK,图省事,没有自己写IK脚本。给每条腿的根节点都挂上CCD IK,然后设置好子关节,本文每隔1个关节设置了一个子关节,不然计算起来太耗时,帧率太低。对应放进去的关节要设置一下Rotation Limit这里使用了Angle限制。

腿部IK如何实现?首先需要给脚底寻找落点,这里特地没有把Foot设置为CCD IK的最后一个关节,而是把接近脚踝的位置设置为CCD IK的最后一个关节,这样设置好,可以我们自己手动设置脚的落点,保证始终落在垂直落点的方向上,而脚踝和脚底的距离大约0.25-0.35,这也是为什么后面脚本中会出现normal * 0.25f的原因,因为相当于找到踝关节落点,然后我们让ankle(子物体包含了脚底)始终LookAt这个ankle.position + info.normal,这就相当于我们自己实现了简化版的落脚点GrounderIK。

接下来给每个脚底挂上IKFootSolver脚本。

代码写的稀烂,凑合看吧。思路:从指示器发射一条射线,如果落点落在了角度60度以内的环形范围内,并且当前leg允许移动且每次指示器找到的落点距离上一次落点大于设定的distance,就可以计算脚底应该落在那里,发送给ccdIK.solver.IKPosition。如果当脚底落在60度的环形范围之外,例如:落在了两个红色区域内圈外圈,分别对应两个红点重新寻找落点,同理绿色和蓝色范围;下图表示指示器落在了距离大于环形外圈的地方,则对应靠外的红点,以该红点位置向下发射射线,重新寻找落点。。。这样守护者的脚才不会伸的太直或太弯。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using RootMotion.FinalIK;
using UnityEngine.Serialization;public class IKFootSolver : MonoBehaviour
{public Transform indicator;public Transform guardian;private Vector3 arcPos1outer;private Vector3 arcPos1inner;private Vector3 arcPos2outer;private Vector3 arcPos2inner;private Vector3 arcPos3outer;private Vector3 arcPos3inner;private int terrainLayer = 1 << 3;private Vector3 currentPosition;public Vector3 newPosition;private Vector3 oldPosition;public Vector3 newOutRangePosition;[SerializeField] private float stepDistance = 1.5f;public float stepHeight;[SerializeField] private float speed = 7f;public float lerp = 1.01f;public CCDIK ccdIK;RaycastHit info;RaycastHit Inner6PointRaycastInfo;public Transform ankle;public bool stopMove;public bool inRange;public bool callbackMoveNextLeg = true;public int newOutRangeArea;public int oldOutRangeArea;public bool moveLeg;private void Awake(){oldPosition = transform.position;}private void Start(){ccdIK.solver.IKPosition = currentPosition;}// Update is called once per framevoid Update(){ankle.LookAt(ankle.position + info.normal);//transform.position = currentPosition;Ray ray = new Ray(indicator.position, Vector3.down);if (Physics.Raycast(ray, out info, 10, terrainLayer) && lerp == 1.01f){//计算本体和腿的方向向量Vector3 guarPos = new Vector3(guardian.position.x, 0, guardian.position.z);Vector3 legPos = new Vector3(ccdIK.gameObject.transform.position.x, 0, ccdIK.gameObject.transform.position.z);Vector3 dir1 = legPos - guarPos;//计算射线点和本体的方向向量Vector3 infoPos = new Vector3(info.point.x, 0, info.point.z);Vector3 dir2 = infoPos - guarPos;arcPos1outer = guardian.position + dir1.normalized * 6.6f + Vector3.up * 10;arcPos1inner = guardian.position + dir1.normalized * 5.3f + Vector3.up * 10;arcPos2outer = arcPos1outer + Quaternion.AngleAxis(-110, Vector3.up) * dir1.normalized * 2.8f;arcPos2inner = arcPos1inner + Quaternion.AngleAxis(-105, Vector3.up) * dir1.normalized * 2.2f;arcPos3outer = arcPos1outer + Quaternion.AngleAxis(110, Vector3.up) * dir1.normalized * 2.8f;arcPos3inner = arcPos1inner + Quaternion.AngleAxis(105, Vector3.up) * dir1.normalized * 2.2f;inRange = Vector3.Angle(dir1.normalized, dir2.normalized) <= 30 && dir2.magnitude >= 9.5f / 2 && dir2.magnitude <= 14 / 2;//如果是范围内if (inRange){if (Vector3.Distance(newPosition, info.point) > stepDistance && !stopMove){//Debug.Log("inRange--");lerp = 0;newPosition = info.point + 0.25f * info.normal;inRange = true;callbackMoveNextLeg = true;}}else{if (Vector3.Distance(newOutRangePosition, info.point) > stepDistance && !stopMove){if (Vector3.Angle(dir1.normalized, dir2.normalized) <= 15){if (dir2.magnitude >= 7){newOutRangeArea = 1;Ray ray1 = new Ray(arcPos1outer, Vector3.down);if (Physics.Raycast(ray1, out Inner6PointRaycastInfo, 20, terrainLayer)){//Debug.LogError("Cond1outer--");lerp = 0;newPosition = Inner6PointRaycastInfo.point + 0.25f * Inner6PointRaycastInfo.normal;newOutRangePosition = info.point;callbackMoveNextLeg = true;}}else{newOutRangeArea = 2;Ray ray1 = new Ray(arcPos1inner, Vector3.down);if (Physics.Raycast(ray1, out Inner6PointRaycastInfo, 20, terrainLayer)){//Debug.LogError("Cond1inner--");lerp = 0;newPosition = Inner6PointRaycastInfo.point + 0.25f * Inner6PointRaycastInfo.normal;newOutRangePosition = info.point;callbackMoveNextLeg = true;}}}else if (Vector3.Angle(dir1.normalized, dir2.normalized) * Mathf.Sign(Vector3.Cross(dir2.normalized, dir1.normalized).y) > 15){if (dir2.magnitude >= 7){newOutRangeArea = 3;Ray ray2 = new Ray(arcPos2outer, Vector3.down);if (Physics.Raycast(ray2, out Inner6PointRaycastInfo, 20, terrainLayer)){//Debug.LogError("Cond2outer--");lerp = 0;newPosition = Inner6PointRaycastInfo.point + 0.25f * Inner6PointRaycastInfo.normal;newOutRangePosition = info.point;callbackMoveNextLeg = true;}}else{newOutRangeArea = 4;Ray ray2 = new Ray(arcPos2inner, Vector3.down);if (Physics.Raycast(ray2, out Inner6PointRaycastInfo, 20, terrainLayer)){//Debug.LogError("Cond2inner--");lerp = 0;newPosition = Inner6PointRaycastInfo.point + 0.25f * Inner6PointRaycastInfo.normal;newOutRangePosition = info.point;callbackMoveNextLeg = true;}}}else if (Vector3.Angle(dir1.normalized, dir2.normalized) * Mathf.Sign(Vector3.Cross(dir2.normalized, dir1.normalized).y) < -15){if (dir2.magnitude >= 7){newOutRangeArea = 5;Ray ray3 = new Ray(arcPos3outer, Vector3.down);if (Physics.Raycast(ray3, out Inner6PointRaycastInfo, 20, terrainLayer)){//Debug.LogError("Cond3outer--");lerp = 0;newPosition = Inner6PointRaycastInfo.point + 0.25f * Inner6PointRaycastInfo.normal;newOutRangePosition = info.point;callbackMoveNextLeg = true;}}else{newOutRangeArea = 6;Ray ray3 = new Ray(arcPos3inner, Vector3.down);if (Physics.Raycast(ray3, out Inner6PointRaycastInfo, 20, terrainLayer)){//Debug.LogError("Cond3inner--");lerp = 0;newPosition = Inner6PointRaycastInfo.point + 0.25f * Inner6PointRaycastInfo.normal;newOutRangePosition = info.point;callbackMoveNextLeg = true;}}}moveLeg = false;}}}if (lerp < 1){Vector3 footPosition = Vector3.Lerp(oldPosition, newPosition, lerp);footPosition.y += Mathf.Sin(lerp * Mathf.PI) * stepHeight;//ankle.LookAt(ankle.position + info.normal);currentPosition = footPosition;lerp += Time.deltaTime * speed;if (!stopMove){ccdIK.solver.IKPosition = currentPosition;}}else{lerp = 1.01f;oldPosition = newPosition;oldOutRangeArea = newOutRangeArea;if (callbackMoveNextLeg){ccdIK.solver.IKPosition = newPosition;//Debug.Log("OnMoveNextLeg:" + this.gameObject.name + ": " + callbackMoveNextLeg);EventHandler.CallMoveNextLeg(callbackMoveNextLeg, this.gameObject.name);callbackMoveNextLeg = false;}//inCondition234 = false;}}}

接下来是控制腿部的移动顺序,很简单,初始化设置两条腿能动,其他不能动,用事件顺序激活其他腿。GetMatchedLeg方法是找到对应的腿,14配对,25配对,36配对,腿1执行完了就激活腿2动,以此类推。

public static class EventHandler
{public static event Action<bool,string> MoveNextLeg;public static void CallMoveNextLeg(bool onFinishMoveLeg, string legName){MoveNextLeg?.Invoke(onFinishMoveLeg, legName);}
}
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class GuardianLegMoveOrderController : MonoBehaviour
{public IKFootSolver[] iKFootSolvers;private IKFootSolver lastIKFootSovler;private void Start(){for (int i = 0; i < iKFootSolvers.Length; i++){if (i % 3 == 0){iKFootSolvers[i].stopMove = false;}else{iKFootSolvers[i].stopMove = true;}}lastIKFootSovler = iKFootSolvers[0];EventHandler.MoveNextLeg += OnMoveNextLeg;}private void OnDestroy(){EventHandler.MoveNextLeg -= OnMoveNextLeg;}private void OnMoveNextLeg(bool onFinishMoveLeg, string legName){//Debug.Log("CallOnMoveNextLeg:" + legName + ": " + onFinishMoveLeg);if(onFinishMoveLeg){iKFootSolvers[Convert.ToInt32(legName.Replace("Foot", "")) - 1].stopMove = true;iKFootSolvers[GetMatchedLeg(legName.Replace("Foot", "")) - 1].stopMove = false;}}private int GetMatchedLeg(string legName){int leg = legName switch{"1" => 2,"2" => 3,"3" => 1,"4" => 5,"5" => 6,"6" => 4,};return leg;}
}

腿弄好了,接下来移动守护者的身体。

public class GuardianMovement : MonoBehaviour
{public Vector3 guardianBodyPosition;private GuardianLegMoveOrderController controller;public Transform guardianBody;public Transform target;public float speed = 5f;// Start is called before the first frame updatevoid Start(){controller = gameObject.GetComponent<GuardianLegMoveOrderController>();}// Update is called once per framevoid Update(){var dist1 = Vector3.Magnitude(controller.gameObject.transform.position - target.position);//var dist2 = Vector3.Distance(controller.gameObject.transform.position, guardianBody.position);if (target != null && dist1 > 12){MoveIndicator();MoveBody();}}private void OnTriggerEnter(Collider other){Debug.Log(other.gameObject.name);if (other.gameObject.CompareTag("Player")){target = other.gameObject.transform;}}private void MoveIndicator(){var dir1 = target.position - controller.gameObject.transform.position;//目标方向controller.gameObject.transform.Translate(dir1.normalized * Time.deltaTime * speed, Space.World);//向目标方向移动,normalized归一实现匀速移动        }private void MoveBody(){var dir2 = controller.gameObject.transform.position - guardianBody.position;//目标方向guardianBody.transform.Translate(dir2.normalized * Time.deltaTime * speed * 0.85f, Space.World);//向目标方向移动,normalized归一实现匀速移动}
}

移动完守护者,就可以让守护者转动头部瞄准林克了

public class HeadTurnToTarget : MonoBehaviour
{public Transform target;public GuardianMovement guardianMovement;public GameObject head;public GameObject eye;public GameObject targetHead;public GameObject laser;public float lerp;public float targetAngle;public float turnAngle;private void Start(){guardianMovement = FindObjectOfType<GuardianMovement>();laser.SetActive(false);}// Update is called once per framevoid Update(){target = guardianMovement.target;if (target != null){targetHead = GameObject.Find("mixamorig:Head").gameObject;TurnHeadToTarget();            }}public void TurnHeadToTarget(){var dir = target.position - head.transform.position;targetAngle = Vector3.Angle(dir.normalized, head.transform.up) * Vector3.Cross(dir.normalized, head.transform.up).y;head.transform.eulerAngles += new Vector3(0, 0, targetAngle * Time.deltaTime * 2);if (targetAngle <=1){laser.SetActive(true);LockTarget();}else{laser.SetActive(false);}//head.transform.eulerAngles = Vector3.Lerp(head.transform.eulerAngles, new Vector3(0, 0, head.transform.eulerAngles.z + targetAngle), 0.05f);}public void LockTarget(){var distance = (targetHead.transform.position - eye.transform.position).magnitude;laser.transform.position = eye.transform.position;laser.transform.localScale = new Vector3(0.02f, 0.02f, distance * 0.5f);laser.transform.LookAt(targetHead.transform.position);}
}

这里面用到了一个laser,是红色的一道闪烁的激光(Cylinder物体),根据守护者到林克的距离调整激光的长度,然后把激光位置调整到守护者眼睛处,并指向林克,这里激光的pivot使用adjust pivot插件调整到圆珠底部了(该插件unity asset store免费),在上面代码中已经写了,如何实现闪烁?

这里用到了Shader Graph自己写了一个简单的闪烁效果。实际上下图的shader graph是闪烁的,但是截图没有闪烁。仅供参考。

根据这个Shader Graph创建一个材质赋给激光就行了。

结束。

Unity 程序化动画:还原塞尔达旷野之息 守护者 (六足)相关推荐

  1. Unity UI xlua 热更:还原塞尔达旷野之息 (持续更新:已补充箭头动效)

    整了个小Demo仿照<塞尔达传说:旷野之息>,实现 鼠标悬停在Button上时,能够改变Button-Text颜色,并且在Button前显示一个小箭头 标题鼠标指针悬停和移走,改变标题颜色 ...

  2. Unity Shader 卡通渲染 (三):仿塞尔达荒野之息 Shader(顶点色控制细节)

    上一篇传送门: https://blog.csdn.net/qq_27534999/article/details/100925621 顶点色在卡通渲染中有挺多应用,本篇会在上一篇的基础上,运用模型顶 ...

  3. Unity Shader 卡通渲染 (一):仿塞尔达荒野之息 Shader(简易版)

    温馨提示: 本系列文章面向那些 Shader 刚刚入门,想寻求进一步提升的群体,如果对 Shader 一无所知的话,建议自行搜索其他 Shader入门教程观看学习,再食用本系列文章. 前言: 说起卡通 ...

  4. RenderDoc塞尔达荒野之息抓帧分析

    RenderDoc是一种抓帧工具,主要用来分析游戏开发中渲染流程,官网: https://renderdoc.org/ 我是用的版本是RenderDoc_1.14_64.zip 塞尔达荒野之息使用Ce ...

  5. 塞尔达荒野之息vs艾尔登法环

    玩了荒野之息(switch版本)再去玩艾尔登法环,感觉艾尔登法环完全没有外界宣扬的那么好.我是七彩虹3060显卡,特效全开了,一开始场景还比较精致的,后面也很多场景比较粗糙. 然后就是感觉这个人物很笨 ...

  6. 分享几张塞尔达荒野之息精彩壁纸

    分享几张塞尔达荒野之息精彩壁纸 001-荒野之息经典封面.jpg 002-林克醒来看向海拉陆大陆经典场面.jpg 003-林克射箭特写.jpg 004-海拉鲁荒野之息.jpg 005-林克攀爬峭壁.j ...

  7. 塞尔达amiibo_塞尔达荒野之息pC版(附带全Amiibo)安装教程,最无敌的游戏

    点击上方「蓝字」关注我们 给你最好的 <塞尔达传说:荒野之息(The Legend of Zelda: Breath of the Wild)>是任天堂旗下经典角色扮演游戏系列<塞尔 ...

  8. 塞尔达 amiibo数据_塞尔达传说:旷野之息Amiibo道具制作_碧海风云

    本文出自微信公众号[碧海风云]之<塞尔达传说:旷野之息Amiibo道具制作_碧海风云> Amiibo介绍 Amiibo是任天堂发行的内置NFC芯片的产品,可以在特定游戏中与Switch机器 ...

  9. 塞尔达:旷野之息个人对比上古卷轴V:天际

    上古卷轴5是我之前玩过最优秀的作品.玩塞尔达的时候就有跟上古卷轴5比对,真的都是神作.两个游戏的自由度都是真的高. 主线剧情上,老滚5印象不深了,当时就知道战斗,只记住了开头砍头现场,还有奥杜因这个龙 ...

最新文章

  1. 详解 Amazon Go 三大核心技术
  2. POJ-1122 FDNY to the Rescue!---Dijkstra+反向建图
  3. 白话详细解读(七)----- Batch Normalization
  4. ROS-kinetic 机器语音 之科大讯飞SDK
  5. JVM(4)——对象访问
  6. Linux 权限设置
  7. 傅立叶变换,时域,频域一
  8. 电力-101规约说明书2
  9. 国际版链克口袋 获取方法
  10. java游戏实例_Java游戏俄罗斯方块的实现实例
  11. C语言——学习笔记(全)
  12. 跨境电商的三个增长点:产品曝光 品类轮转 入自建站
  13. html代码向左居右对齐
  14. 笨方法学python 习题14
  15. 公众号文章的动态图片如何制作
  16. Franka Emika Panda 机械臂环境配置
  17. 让你成熟至少5岁的8句话
  18. box-shadow(盒子阴影)的使用
  19. leetcode392. 判断子序列
  20. WEB前端学习课程推荐

热门文章

  1. Adobe AfterEffects CC 2023(AE2023)安装教程与下载方式
  2. 微信新功能,帮你用品牌成交客户!
  3. 湖南工业大学c语言 期末考试程序改错,高考各科答题规范,90%的同学都不了解!现在改还来得及!...
  4. 科技云报道:从容联云收购过河兵看云通讯赛道如何升维
  5. HDU 1269 迷宫城堡(强连通图的判定)
  6. 找对方法,事半功倍!有目标抓重点!
  7. Zookeeper分布式入门——ZK四字命令(一)
  8. MongoDB University笔记总结 - M103_Chapter0:Introduction Setup
  9. 2018-狗年年终总结
  10. 【SIP基础】SIP协议中网络角色定义