惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

B
Blog RSS Feed
Spread Privacy
Spread Privacy
T
Threatpost
C
Cisco Blogs
P
Palo Alto Networks Blog
AI
AI
Cyberwarzone
Cyberwarzone
NISL@THU
NISL@THU
P
Privacy & Cybersecurity Law Blog
G
GRAHAM CLULEY
Simon Willison's Weblog
Simon Willison's Weblog
T
Tor Project blog
Latest news
Latest news
AWS News Blog
AWS News Blog
D
Docker
S
SegmentFault 最新的问题
博客园 - 聂微东
WordPress大学
WordPress大学
Vercel News
Vercel News
S
Securelist
爱范儿
爱范儿
J
Java Code Geeks
Know Your Adversary
Know Your Adversary
S
Schneier on Security
Hugging Face - Blog
Hugging Face - Blog
F
Fortinet All Blogs
Last Week in AI
Last Week in AI
D
DataBreaches.Net
宝玉的分享
宝玉的分享
D
Darknet – Hacking Tools, Hacker News & Cyber Security
MongoDB | Blog
MongoDB | Blog
Engineering at Meta
Engineering at Meta
K
Kaspersky official blog
美团技术团队
博客园 - 叶小钗
阮一峰的网络日志
阮一峰的网络日志
量子位
博客园_首页
Attack and Defense Labs
Attack and Defense Labs
S
Secure Thoughts
Google Online Security Blog
Google Online Security Blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
腾讯CDC
T
Threat Research - Cisco Blogs
雷峰网
雷峰网
有赞技术团队
有赞技术团队
www.infosecurity-magazine.com
www.infosecurity-magazine.com
P
Privacy International News Feed
S
Security Affairs

元视角

.NET 生态下的 Agent 框架选型:从 ReAct 到原生推理 - 元视角 从「能用」到「好用」:LLM 流式响应实现方式的探索之路 - 元视角 当我用 2000 条聊天记录,让 AI 为我画一幅自画像 - 元视角 基于 Supabase 的 AI 应用开发探索 - 元视角 微博 × MCP:社交媒体新玩法解锁 - 元视角 四点钟海棠花未眠 - 元视角 Semantic Kernel × MCP:智能体的上下文增强探索 - 元视角 基于 K-Means 聚类分析实现人脸照片的快速分类 - 元视角 容器技术驱动下的代码沙箱实践与思考 - 元视角 温故而知新:后端通用查询方案的再思考 - 元视角 浅议 CancellationToken 在前后端协同取消场景中的应用 - 元视角 Semantic Kernel 视角下的 Text2SQL 实践与思考 - 元视角 关于 ChatGPT 的流式传输,你需要知道的一切 - 元视角 RAG 的是与非、Rewrite 和 Rerank - 元视角 使用 EFCore 和 PostgreSQL 实现向量存储及检索 - 元视角 基于 LLaMA 和 LangChain 实践本地 AI 知识库 - 元视角 使用 llama.cpp 在本地部署 AI 大模型的一次尝试 - 元视角 如何为 Git 配置多个 SSH Key - 元视角 C# 使用 LibUsbDotNet 实现 USB 设备检测 - 元视角 基于 C# 实现样式与数据分离的打印方案 - 元视角 基于 SVG 的图形交互方案实践 - 元视角 前端视频播放技术概览 - 元视角 温故而知新,再话 Python 动态导入 - 元视角 后 GPT 时代,NLP 不存在了? - 元视角 视频是不能 P 的系列:使用 Milvus 实现海量人脸快速检索 - 元视角 GDI+下字体大小自适应方案初探 - 元视角 小爱音箱集成 ChatGPT 的不完全教程 - 元视角 程序员视角下的三体世界随想 - 元视角 关于 Docker 容器配置信息的渐进式思考 - 元视角 在 Docker 容器内集成 Crontab 定时任务 - 元视角 为你的服务器集成 LDAP 认证 - 元视角 似花还似非花 - 元视角 视频是不能 P 的系列:使用 Dlib 实现人脸识别 - 元视角 浅议分布式链路追踪与日志的整合 - 元视角 关于 Git 大文件上传这件小事 - 元视角 .NET 进程内队列 Channel 的入门与应用 - 元视角 使用 Fody 实现 .NET 的静态编织 - 元视角 .NET Core + ELK 搭建可视化日志分析平台(下) - 元视角 聊一聊前端图片懒加载背后的故事 - 元视角 支持外部链接跳转的 Vue Router 扩展实现 - 元视角 视频是不能 P 的系列:OpenCV 和 Dlib 实现表情包 - 元视角 不得不说的 ASP.NET Core 集成测试 - 元视角 再议 DDD 视角下的 EFCore 与 领域事件 - 元视角 Vue.js 前端项目容器化部署实践极简教程 - 元视角 再见,人间四月天 - 元视角 Python 图像风格化迁移助力画家梦想 - 元视角 利用 ASP.NET Core 中的标头传播实现分布式链路追踪 - 元视角 利用 gRPC 实现文件的上传与下载 - 元视角 七种武器:延迟队列的原理和实现总结 - 元视角 gRPC 流式传输极简入门指南 - 元视角 Envoy 集成 Jaeger 实现分布式链路追踪 - 元视角 浅议非典型 Web 应用场景下的身份认证 - 元视角 gRPC 借助 Any 类型实现接口的泛化调用 - 元视角 分布式丛林探险系列之 Redis 集群模式 - 元视角 分布式丛林探险系列之 Redis 主从复制模式 - 元视角 通过 Python 预测 2021 年双十一交易额 - 元视角 gRPC 搭配 Swagger 实现微服务文档化 - 元视角 SSL/TLS 加密传输与数字证书的前世今生 - 元视角 使用 Python 自动识别防疫健康码 - 元视角 你不可不知的容器编排进阶技巧 - 元视角 ASP.NET Core 搭载 Envoy 实现 gRPC 服务代理 - 元视角 再话 AOP,从简化缓存操作说起 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务身份认证(JWT) - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的监控预警 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的反向代理 - 元视角 ASP.NET Core gRPC 打通前端世界的尝试 - 元视角 EFCore 实体命名约定库:EFCore.NamingConventions - 元视角 ASP.NET Core gRPC 集成 Polly 实现优雅重试 - 元视角 ASP.NET Core gRPC 健康检查的探索与实现 - 元视角 ASP.NET Core gRPC 拦截器的使用技巧分享 - 元视角 SnowNLP 使用自定义语料进行模型训练 - 元视角 使用 HttpMessageHandler 实现 HttpClient 请求管道自定义 - 元视角 ABP vNext 的实体与服务扩展技巧分享 - 元视角 ABP vNext 对接 Ant Design Vue 实现分页查询 - 元视角 源代码探案系列之 .NET Core 跨域中间件 CORS - 元视角 源代码探案系列之 .NET Core 限流中间件 AspNetCoreRateLimit - 元视角 源代码探案系列之 .NET Core 并发限制中间件 ConcurrencyLimiter - 元视角 通过 EmbededFileProvider 实现 Blazor 的静态文件访问 - 元视角 低代码,想说爱你不容易 - 元视角 记一次失败的 ThoughtWorks 面试经历 - 元视角 从 C# 1.0 到 C# 9.0,历代 C# 语言特性一览 - 元视角 通过 Python 分析 2020 年全年微博热搜数据 - 元视角 基于 Python 和 Selenium 实现 CSDN 一键三连自动化 - 元视角 使用多线程为你的 Python 爬虫提速的 N 种姿势,你会几种? - 元视角 实现网页长截图的常见思路总结 - 元视角 温故而知新,由 ADO.NET 与 Dapper 所联想到的 - 元视角 视频是不能 P 的系列:OpenCV 人脸检测 - 元视角 作为技术宅的我,是这样追鬼滅の刃的 - 元视角 使用 Python 抽取《半泽直树》原著小说人物关系 - 元视角 厉害了!打工人用 Python 分析西安市职位信息 - 元视角 使用 dotTrace 对 .NET 应用进行性能分析与优化 - 元视角 一道 HashSet 面试题引发的蝴蝶效应 - 元视角 基于选项模式实现.NET Core 的配置热更新 - 元视角 Dapper.Contrib 在 Oracle 环境下引发 ORA-00928 异常问题的解决 - 元视角 .NET Core 中对象池(Object Pool)的使用 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(下):EventBus 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(中):RabbitMQ 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(上):基础篇 - 元视角 记一次从已损坏的 Git 仓库中找回代码的经历 - 元视角 .NET Core 原生 DI 扩展之属性注入实现 - 元视角
使用 Mecanim 动画系统来控制 2D 动画 - 元视角
飞鸿踏雪 · 2015-02-11 · via 元视角

各位朋友,大家好,我是秦元培,欢迎大家关注我的博客,我的博客地址是http://blog.csdn.net/qinyuanpei。今天我想和大家分享的话题是在 Unity3D 中使用 Mecanim 动画系统来控制 2D 动画。

相信在大家的印象中,Mecanim 动画系统主要运用在 3D 动画中,因为 Mecanim 动画系统提供了像动画重定向、人体骨骼动画等 3D 动画的特性,那么 Unity3D 的 Mecanim 动画系统能不能用来控制 2D 动画呢?如果在以前,博主和大家的理解是一样的,认为 Mecanim 只能运用到 3D 动画中,对于 2D 动画只能使用传统的逐帧动画和骨骼动画。可是前不久有位朋友问我,为什么不使用动画组件来控制 2D 动画呢?博主心想啊,这 Mecanim 动画系统真的能控制 2D 动画吗?经过博主查找大量资料和亲身实践,发现 Mecanim 是可以用来控制 2D 动画的,而且由于状态机的引入,我们对动画状态的控制会变得更为简单,从写代码的角度来看,这样可以减少我们的代码量便于维护。那么好了,今天我们就来一起学习下如何使用 Mecanim 动画系统来控制 2D 动画吧!

传统 2D 动画的实现方式

在 Unity3D 中传统 2D 动画的实现方式是基于逐帧动画的原理实现的,这种实现方式在 Unity3D 没有推出 Unity2D 前甚至在 Unity2D 推出后相当长的一段时间内,基本上我们最为常用的实现方式,博主在刚开始学习 Unity3D 的时候通常是以 2D 形式来展开的,因为博主认为 2D 和 3D 在原理上基本是相通的,如果我们掌握了 2D 游戏的基本原理,那么在实现 3D 游戏的时候就会相对容易些。我们来看看一个最简单的 2D 动画的脚本实现:

//精灵渲染器
private SpriteRenderer mRenderer;
//精灵集合
public Sprite[] Sprites;
//FPS,即每秒钟的画面帧数
public float FPS = 24;
//精灵索引
private int index = 0;
//当前时间
private float currentTime = 0;

void Start () 
{
    mRenderer = GetComponent<SpriteRenderer>();
}
	
void Update () 
{
    //获取当前时间
    currentTime += Time.deltaTime;
    //如果达到了更新画面的时间
    if(currentTime >= 1 / FPS)
    {
        //使索引增加
        index += 1;
        //清除时间记录
        currentTime = 0;
        //当索引更新到最后一帧时,索引重置
        if(index >= Sprites.Length)
        {
            index = 0;
        }
    }

    //更新画面
    mRenderer.sprite = Sprites[index];
}

通过分析,我们可以发现这段脚本存在以下问题:

  • 动画维护困难:每增加一个动画就需要添加一个数组,不仅增加了动画的维护难度,同时降低了脚本的效率。
  • 状态维护困难:因为在 Update 方法里实现的是一个动画,因此当我们需要在各个动画状态间进行切换的时候,我们需要使用更多的代码来维护相关逻辑。

使用 Mecanim 动画系统的实现方式

为了解决传统的 2D 动画实现方式中存在的动画维护困难、状态维护困难这两个问题,我们需要一种更好的方案来实现 2D 动画的控制,这种方案需要提供较为方便的动画维护功能,即各个动画是独立的,当改变了某一个动画时,其余的动画不会发生改变。其次,这种方案需要提供较为方便的状态维护功能,即各个动画状态切换是方便的,我们可以更好地从这一种状态切换到另一种状态。关于动画状态切换,大家可以去了解下有限状态机(FSM)的概念,这里我们不做深入的探究,这里我们选择 Unity3D 的 Mecanim 动画系统,因为 Mecanim 动画系统正好解决了这两个问题。好了,下面我们来一起学习一个 2D 动画的实例:

首先我们在场景中创建一个名为 PlayerController 的空物体,然后在该物体的下面增加一个精灵组件(Sprite),并将其命名为 PlayerSprite,这样做的好处是 Unity3D 将为我们自动创建较为规范的命名。好了,现在我们选择 PlayerController 这个物体,然后通过 Window -> Animation 菜单打开 Animation 窗口:

创建第一个动画 创建第一个动画

首先我们点击 AddCurve 按钮,此时将弹出一个对话框让我们保存动画文件,这里我们存储为 Player@Idle.anim,并将其保存在项目目录下的 Animations\Player 目录下,这样可以方便我们维护和查找特定的动画文件。在保存完动画文件后,此时会弹出如下的界面,我们选择 PlayerSprite 节点下的 SpriteRenderer,然后选择 Sprite,因为这里我们的 2D 动画主要是通过改变 SpriteRenderer 的 Sprite 属性来实现的,最后我们点击 Sprite 节点后面的加号来完成对象的选取。此时会在动画窗口中显示时间轴和刻度线,我们将在这里完成动画的编辑。大家可以注意到默认情况下,动画面板添加了两帧,即第 1 帧和最后一帧,其总时间是 1 秒,同时我们注意到这里有一个采样率(Sample),其实这就是当前动画的 FPS 了。好了,现在我们开始制作第一个动画:

动画序列 动画序列

在资源文件夹中,我们可以找到当前动画的图片素材,注意到这个图片中总共有 12 帧画面,因此我们可以按照 0.05s 的间隔来分配整个时间轴,所以我们可以这样添加帧:

为 Idle 动画添加帧 为 Idle 动画添加帧

好了,现在我们就完成了一个 Idle 动画的制作,现在打开角色的动画控制器 PlayerController,这是 Unity3D 为我们自动创建的一个动画控制器,因为我们现在只有一个 Idle 动画,所以在 Animaotr 窗口中我们可以看到只有一个 Idle 状态,现在我们将这个状态设为默认状态。好了,现在我们可以直接运行游戏,发现在场景中角色开始循环播放 Idle 动画了。好了,现在让我们重复刚才的步骤,来完成角色的其余动画。

Idle 动画效果演示 Idle 动画效果演示

经过一番努力,我们现在已经完成了角色所有动画的制作,现在我们来设计角色的动画状态机:

玩家角色控制器设计 玩家角色控制器设计

设计好角色的动画状态机后我们开始来编写脚本,以实现角色动画的控制:

using UnityEngine;
using System.Collections;

public class PlayerController : MonoBehaviour {

    public enum PlayerState
    {
        Idle,
        Move,
        LightAttack,
        WeightAttack
    }

    public PlayerState State=PlayerState.Idle;
    //玩家移动速度
    public float WalkSpeed = 0.75f;
    public float RunSpeed = 1.5f;
    //玩家跳跃力的强度
    public float JumpForce = 200f;

    //位置限制
    public float MinX = -5.80f;
    public float MaxX =  5.80f;
    public float MinY = -1.80f;
    public float MaxY =  0.35f;

    //玩家朝向,默认朝右
    public bool isFaceRight = true;

    //动画组件
    private Animator mAnim;
    //2D刚体
    private Rigidbody2D mRig2D;


    void Start () 
    {
        mAnim=GetComponent<Animator>();
        mRig2D=GetComponent<Rigidbody2D>();
    }

    void Update()
    {
        SpriteMove();
        SpriteAttack();
        SpriteJump();
        SpriteIdle();
    }

    /// <summary>
    /// 精灵Idle
    /// </summary>
    private void SpriteIdle()
    {
        //当玩家无任何操作时恢复到Idle状态
        if (!Input.anyKey)
        {
            mAnim.SetBool("Jump", false);
            mAnim.SetBool("Attack", false);
            mAnim.SetBool("BigAttack", false);
            mAnim.SetBool("Skill", false);
            mAnim.SetBool("BigSkill", false);
            State=PlayerState.Idle;
        }
    }

    /// <summary>
    /// 精灵攻击
    /// </summary>
    private void SpriteAttack()
    {
        //轻击,键位J
        if(Input.GetKey(KeyCode.J))
        {
            mAnim.SetBool("Attack", true);
            State=PlayerState.LightAttack;
        }

        //重击,键位K
        if(Input.GetKey(KeyCode.K))
        {
            mAnim.SetBool("BigAttack", true);
            State=PlayerState.WeightAttack;
        }

    }

    /// <summary>
    /// 精灵跳跃
    /// </summary>
    private void SpriteJump()
    {
        if (Input.GetKey(KeyCode.I))
        {
            mAnim.SetBool("Jump", true);
            mRig2D.AddForce(new Vector2(0, Time.deltaTime * JumpForce), ForceMode2D.Impulse);
        }
    }

    private void SpriteMove()
    {
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");

        Vector2 mPos = mRig2D.position;

        mAnim.SetFloat("Speed", Mathf.Sqrt(h * h + v * v));

        float mPosX, mPosY;

        if (Mathf.Sqrt(h * h + v * v) > 0.5f){
            mPosX = mPos.x + h * Time.deltaTime * RunSpeed;
            mPosY = mPos.y + v * Time.deltaTime * RunSpeed;
        }else{
            mPosX = mPos.x + h * Time.deltaTime * WalkSpeed;
            mPosY = mPos.y + v * Time.deltaTime * WalkSpeed;
        }


        mRig2D.MovePosition(new Vector2(mPosX, mPosY));

        if (h > 0 && !isFaceRight)
        {
            FlipSrite();
        }
        else if (h < 0 && isFaceRight)
        {
            FlipSrite();
        }
    }


    void FlipSrite()
    {
        if(isFaceRight){
            transform.rotation=Quaternion.Euler(0,180,0);
            isFaceRight=false;
        }else{
            transform.rotation=Quaternion.Euler(0,0,0);
            isFaceRight=true;
        }
    }
}

好了,现在我们可以来看看最终的效果,博主这里是想利用这些素材来制作一个横板过关的游戏,可是因为文章篇幅有限,所以这部分内容只能留到以后再和大家分享了。

最终效果演示 最终效果演示

Mecanim 动画系统应用扩展

好了,到现在为止,基于 Mecanim 动画系统的 2D 动画控制基本上讲解完了。下面我们说说 Mecaanim 动画系统应用扩展。通过前面的学习,我们知道 Unity2D 使用的 Mecanim 动画系统主要是通过改变游戏体的属性来实现某种特定的动画效果的,例如我们这里的动画是通过改变角色精灵附加的 SpriteRenderer 组件的 Sprite 属性来实现的,因此从本质上来说 Unity2D 的动画控制器是一种属性动画。总体来说,Unity2D 可以实现以下类型的动画:

  • 位移动画:通过 Transform 组件的 Position 属性实现
  • 旋转动画:通过 Transform 组件的 Rotation 属性实现
  • 伸缩动画:通过 Transform 组件的 Scale 属性实现
  • 渐变动画:通过更改指定组件的颜色或材质实现
  • 脚本动画:通过更改指定脚本的变量或字段实现

好了,这就是今天这篇文章的全部内容了,希望大家喜欢!