[Unity Demo]从零开始制作空洞骑士Hollow Knight第二集:通过InControl插件实现绑定玩家输入以及制作小骑士移动空闲动画
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 一、通过InControl插件实现绑定玩家输入
- 二、制作小骑士移动和空闲动画
- 1.制作动画
- 2.玩家移动和翻转图像
- 3.状态机思想实现动画切换
- 总结
前言
好久没来CSDN看看,突然看到前两年自己写的文章从零开始制作空洞骑士只做了一篇就突然烂尾了,刚好最近开始学习做Unity,我决定重启这个项目,从零开始制作空洞骑士!第一集我们导入了素材和远程git管理项目,OK这期我们就从通过InControl插件实现绑定玩家输入以及制作小骑士移动和空闲动画。
一、通过InControl插件实现绑定玩家输入
其实这一部挺难的,因为InControl插件你在网上绝对不超过五个视频资料,但没办法空洞骑士就是用这个来控制键盘控制器输入的,为了原汁原味就只能翻一下InControl提供的Examples示例里学习。
学习完成后直接来看我写的InputHandler.cs和GameManager.cs

在GameManager.cs中我们暂且先只用实现一个单例模式:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class GameManager : MonoBehaviour
{private static GameManager _instance;public static GameManager instance{get{if(_instance == null){_instance = FindObjectOfType<GameManager>();}if (_instance == null){Debug.LogError("Couldn't find a Game Manager, make sure one exists in the scene.");}else if (Application.isPlaying){DontDestroyOnLoad(_instance.gameObject);}return _instance;}}private void Awake(){if(_instance != this){_instance = this;DontDestroyOnLoad(this);return;}if(this != _instance){Destroy(gameObject);return;}}}
在来到InputHandler.cs之前,我们还需要创建映射表HeroActions:
using System;
using InControl;public class HeroActions : PlayerActionSet
{public PlayerAction left;public PlayerAction right;public PlayerAction up;public PlayerAction down;public PlayerTwoAxisAction moveVector;public HeroActions(){left = CreatePlayerAction("Left");left.StateThreshold = 0.3f;right = CreatePlayerAction("Right");right.StateThreshold = 0.3f;up = CreatePlayerAction("Up");up.StateThreshold = 0.3f;down = CreatePlayerAction("Down");down.StateThreshold = 0.3f;moveVector = CreateTwoAxisPlayerAction(left, right, up, down);moveVector.LowerDeadZone = 0.15f;moveVector.UpperDeadZone = 0.95f;}
}
OK到了最关键的InputHandler.cs了:
using System;
using System.Collections;
using System.Collections.Generic;
using GlobalEnums;
using InControl;
using UnityEngine;public class InputHandler : MonoBehaviour
{public InputDevice gameController;public HeroActions inputActions;public void Awake(){inputActions = new HeroActions();}public void Start(){MapKeyboardLayoutFromGameSettings();if(InputManager.ActiveDevice != null && InputManager.ActiveDevice.IsAttached){}else{gameController = InputDevice.Null;}Debug.LogFormat("Input Device set to {0}.", new object[]{gameController.Name});}
//暂时没有GameSettings后续会创建的,这里是指将键盘按键绑定到HeroActions 中private void MapKeyboardLayoutFromGameSettings(){AddKeyBinding(inputActions.up, "W");AddKeyBinding(inputActions.down, "S");AddKeyBinding(inputActions.left, "A");AddKeyBinding(inputActions.right, "D");}private static void AddKeyBinding(PlayerAction action, string savedBinding){Mouse mouse = Mouse.None;Key key;if (!Enum.TryParse(savedBinding, out key) && !Enum.TryParse(savedBinding, out mouse)){return;}if (mouse != Mouse.None){action.AddBinding(new MouseBindingSource(mouse));return;}action.AddBinding(new KeyBindingSource(new Key[]{key}));}}
给我们的小骑士创建一个HeroController.cs,检测输入最关键的是一行代码:
move_input = inputHandler.inputActions.moveVector.Vector.x;
using System;
using System.Collections;
using System.Collections.Generic;
using HutongGames.PlayMaker;
using GlobalEnums;
using UnityEngine;public class HeroController : MonoBehaviour
{public ActorStates hero_state;public ActorStates prev_hero_state;public bool acceptingInput = true;public float move_input;public float RUN_SPEED = 5f;private Rigidbody2D rb2d;private BoxCollider2D col2d;private GameManager gm;private InputHandler inputHandler;private void Awake(){SetupGameRefs();}private void SetupGameRefs(){rb2d = GetComponent<Rigidbody2D>();col2d = GetComponent<BoxCollider2D>();gm = GameManager.instance;inputHandler = gm.GetComponent<InputHandler>();}void Start(){}void Update(){orig_Update();}private void orig_Update(){if (hero_state == ActorStates.no_input){}else if(hero_state != ActorStates.no_input){LookForInput();}}private void FixedUpdate(){if (hero_state != ActorStates.no_input){Move(move_input);}}private void Move(float move_direction){if(acceptingInput){rb2d.velocity = new Vector2(move_direction * RUN_SPEED, rb2d.velocity.y);}}private void LookForInput(){if (acceptingInput){move_input = inputHandler.inputActions.moveVector.Vector.x;}}
}[Serializable]
public class HeroControllerStates
{public bool facingRight;public bool onGround;public HeroControllerStates(){facingRight = true;onGround = false;}
}
记得一句话:FixedUpdate()处理物理移动,Update()处理逻辑
二、制作小骑士移动和空闲动画
1.制作动画
素材找到Idle和Walk文件夹,创建两个同名animation,sprite往上面一放自己就做好了。
可能你注意到我没有给这两个动画连线,其实是我想做个动画状态机,通过核心代码
animator.Play()来管理动画的切换。

2.玩家移动和翻转图像
我们还需要给小骑士添加更多的功能比如翻转图像:
using System;
using System.Collections;
using System.Collections.Generic;
using HutongGames.PlayMaker;
using GlobalEnums;
using UnityEngine;public class HeroController : MonoBehaviour
{public ActorStates hero_state;public ActorStates prev_hero_state;public bool acceptingInput = true;public float move_input;public float RUN_SPEED = 5f;private Rigidbody2D rb2d;private BoxCollider2D col2d;private GameManager gm;private InputHandler inputHandler;public HeroControllerStates cState;private HeroAnimatorController animCtrl;private void Awake(){SetupGameRefs();}private void SetupGameRefs(){if (cState == null)cState = new HeroControllerStates();rb2d = GetComponent<Rigidbody2D>();col2d = GetComponent<BoxCollider2D>();animCtrl = GetComponent<HeroAnimatorController>();gm = GameManager.instance;inputHandler = gm.GetComponent<InputHandler>();}void Start(){}void Update(){orig_Update();}private void orig_Update(){if (hero_state == ActorStates.no_input){}else if(hero_state != ActorStates.no_input){LookForInput();}}private void FixedUpdate(){if (hero_state != ActorStates.no_input){Move(move_input);if(move_input > 0f && !cState.facingRight ){FlipSprite();}else if(move_input < 0f && cState.facingRight){FlipSprite();}}}private void Move(float move_direction){if (cState.onGround){SetState(ActorStates.grounded);}if(acceptingInput){rb2d.velocity = new Vector2(move_direction * RUN_SPEED, rb2d.velocity.y);}}public void FlipSprite(){cState.facingRight = !cState.facingRight;Vector3 localScale = transform.localScale;localScale.x *= -1f;transform.localScale = localScale;}private void LookForInput(){if (acceptingInput){move_input = inputHandler.inputActions.moveVector.Vector.x;}}/// <summary>/// 设置玩家的ActorState的新类型/// </summary>/// <param name="newState"></param>private void SetState(ActorStates newState){if(newState == ActorStates.grounded){if(Mathf.Abs(move_input) > Mathf.Epsilon){newState = ActorStates.running;}else{newState = ActorStates.idle;}}else if(newState == ActorStates.previous){newState = prev_hero_state;}if(newState != hero_state){prev_hero_state = hero_state;hero_state = newState;animCtrl.UpdateState(newState);}}private void OnCollisionEnter2D(Collision2D collision){if(collision.gameObject.layer == LayerMask.NameToLayer("Wall")){cState.onGround = true;}}private void OnCollisionStay2D(Collision2D collision){if (collision.gameObject.layer == LayerMask.NameToLayer("Wall")){cState.onGround = true;}}private void OnCollisionExit2D(Collision2D collision){if (collision.gameObject.layer == LayerMask.NameToLayer("Wall")){cState.onGround = false;}}
}[Serializable]
public class HeroControllerStates
{public bool facingRight;public bool onGround;public HeroControllerStates(){facingRight = true;onGround = false;}
}
创建命名空间GlobalEnums,创建一个新的枚举类型:
using System;namespace GlobalEnums
{public enum ActorStates{grounded,idle,running,airborne,wall_sliding,hard_landing,dash_landing,no_input,previous}
}
3.状态机思想实现动画切换
有了这些我们就可以有效控制动画切换,创建一个新的脚本给Player:

可以看到我们创建了两个属性记录当前的actorState和上一个actorState来实现动画切换(后面会用到的)
using System;
using GlobalEnums;
using UnityEngine;public class HeroAnimatorController : MonoBehaviour
{private Animator animator;private AnimatorClipInfo[] info;private HeroController heroCtrl;private HeroControllerStates cState;private string clipName;private float currentClipLength;public ActorStates actorStates { get; private set; }public ActorStates prevActorState { get; private set; }private void Start(){animator = GetComponent<Animator>();heroCtrl = GetComponent<HeroController>();actorStates = heroCtrl.hero_state;PlayIdle();}private void Update(){UpdateAnimation();}private void UpdateAnimation(){//info = animator.GetCurrentAnimatorClipInfo(0);//currentClipLength = info[0].clip.length;//clipName = info[0].clip.name;if(actorStates == ActorStates.no_input){//TODO:}else if(actorStates == ActorStates.idle){//TODO:PlayIdle();}else if(actorStates == ActorStates.running){PlayRun();}}private void PlayRun(){animator.Play("Run");}public void PlayIdle(){animator.Play("Idle");}public void UpdateState(ActorStates newState){if(newState != actorStates){prevActorState = actorStates;actorStates = newState;}}
}
总结
最后给大伙看看效果怎么样,可以看到运行游戏后cState,hero_state和prev_hero_state都没有问题,动画也正常播放:


累死我了我去睡个觉顺便上传到github,OK大伙晚安醒来接着更新。
相关文章:
[Unity Demo]从零开始制作空洞骑士Hollow Knight第二集:通过InControl插件实现绑定玩家输入以及制作小骑士移动空闲动画
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言一、通过InControl插件实现绑定玩家输入二、制作小骑士移动和空闲动画 1.制作动画2.玩家移动和翻转图像3.状态机思想实现动画切换总结 前言 好久没来CSDN看看&…...
基于鸿蒙API10的RTSP播放器(七:亮度调节功能测试)
目标: 当我的手指在设备左方进行上下移动的时候,可以进行屏幕亮度的调节,在调节的同时,有实时的调节进度条显示 步骤: 界面逻辑:使用Stack() 组件,完成音量图标和进度条的组合显示,…...
基于SpringBoot+Vue的校内跑腿业务管理系统
作者:计算机学姐 开发技术:SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等,“文末源码”。 专栏推荐:前后端分离项目源码、SpringBoot项目源码、SSM项目源码 系统展示 【2025最新】基于JavaSpringBootVueMySQL的…...
嵌入式鸿蒙系统开发语言与开发方法分析
大家好,今天主要给大家分享一下,HarmonyOS系统的主力开发语言ArkTS语言开发方法,它是基于TypeScript(简称TS)语言扩展而来。 第一:ArkTS语言基本特性 目的:声明式UI,让开发者以更简洁,更自然的方式开发高性能应用。 声明式 UI基本特性: 基本UI描述:ArkTS定义了各种装饰…...
SpringBoot开发——整合Spring Data MongoDB
文章目录 一、MongoDB简介1、MongoDB是什么2、MongoDB 基本概念(1)文档(2)集合(3)数据库3、MongoDB的系统数据库4、MongoDB数据模型二、SpringBoot整合Spring Data MongoDB1、创建项目,添加Spring Data MongoDB依赖2、创建实体类Student3、创建StudentRepository接口4、创建…...
camouflaged object detection中的decoder最核心的作用
在 camouflaged object detection(COD)任务中,decoder 的确有一个核心作用是进行 上采样 以恢复图像的分辨率,但这并不是它唯一或最核心的作用。我们可以从更广泛的视角来看 decoder 的作用。 1. 上采样(Upsampling&a…...
Java volatile
Volatile 作用:保证变量的可见性,有序性(禁止指令重排序)。不保证原子性。 如何保证可见性的? 场景:每个 线程 下都有一块 工作内存。要使用变量需要从 主内存 中把 变量 读取出来,使用完成后写…...
一条sql是如何执行的详解
一条sql是如何执行的详解 1. SQL 解析(Parsing) 2. 查询重写(Query Rewrite) 3. 查询规划(Query Planning) 4. 查询执行(Query Execution) 5. 结果返回 示例:查询执…...
“先天项目经理圣体”丨超适合做项目经理的4种人
总有人在问,什么样的人适合做项目经理,当项目经理需要什么样的特质? 你别说,还真有那么一些人是“先天项目经理圣体”,天生就是吃项目经理这碗饭的。 沟通达人丨靠“嘴”走天下 我们知道项目经理大部分的时间都在进行…...
如何从object中抽取某几个值,然后转换成数组
可以使用Object.entries(), Array.prototype.filter()和Array.prototype.map()或者解构赋值的方式从对象中抽取某些值并转换为数组 示例 1:使用 Object.entries(), filter() 和 map() const obj {a: 1,b: 2,c: 3,d: 4 };const keysToExtract [a, c];const extr…...
数据结构(14)——哈希表(1)
欢迎来到博主的专栏:数据结构 博主ID:代码小豪 文章目录 哈希表的思想映射方法(哈希函数)除留余数法 哈希表insert闭散列负载因子扩容find和erase 哈希表的思想 在以往的线性表中,查找速度取决于线性表是否有序&#…...
K近邻算法_分类鸢尾花数据集
import numpy as np import pandas as pd from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score1.数据预处理 iris load_iris() df pd.DataFrame(datairis.data, columnsiris.featur…...
nacos和eureka的区别详解
Nacos 和 Eureka 都是服务发现和注册中心的解决方案,但它们在功能、设计和使用场景上有所不同。以下是它们的详细区别: 1. 基本概念 Eureka:是由 Netflix 开发的服务发现工具。它主要用于 Java 微服务架构中的服务注册与发现。Eureka 通过 R…...
AI大模型包含哪些些技术?
Prompt Prompt提示是模型接收以生成响应或完成任务的初始文本输入。 我们给AI一组Prompt输入,用于指导模型生成响应以执行任务。这个输入可以是一个问题、一段描述、一组关键词,或任何其他形式的文本,用于引导模型产生特定内容的响应。 Tra…...
分布式技术概览
文章目录 分布式技术1. 分布式数据库(Distributed Databases)2. 分布式文件系统(Distributed File Systems)3. 分布式哈希表(Distributed Hash Tables, DHTs)4. 分布式缓存(Distributed Caching…...
动手学习RAG: moka-ai/m3e 模型微调deepspeed与对比学习
动手学习RAG: 向量模型动手学习RAG: moka-ai/m3e 模型微调deepspeed与对比学习动手学习RAG:迟交互模型colbert微调实践 bge-m3 1. 环境准备 pip install transformers pip install open-retrievals注意安装时是pip install open-retrievals,但调用时只…...
Nacos rce-0day漏洞复现(nacos 2.3.2)
Nacos rce-0day漏洞复现(nacos 2.3.2) NACOS是 一个开源的服务发现、配置管理和服务治理平台,属于阿里巴巴的一款开源产品。影像版本:nacos2.3.2或2.4.0版本指纹:fofa:app“NACOS” 从 Github 官方介绍文档可以看出国…...
yjs04——matplotlib的使用(多个坐标图)
1.多个坐标图与一个图的折线对比 1.引入包;字体(同) import matplotlib.pyplot as plt import random plt.rcParams[font.family] [SimHei] plt.rcParams[axes.unicode_minus] False 2.创建幕布 2.1建立图层幕布 一个图:plt.fig…...
MOS管和三极管有什么区别?
MOS管是基于金属-氧化物-半导体结构的场效应晶体管,它的控制电压作用于氧化物层,通过调节栅极电势来控制源漏电流。MOS管是FET中的一种,现主要用增强型MOS管,分为PMOS和NMOS。 MOS管的三个极分别是G(栅极),D(漏极)&…...
医院多参数空气质量监控和压差监测系统简介@卓振思众
在现代医院管理中,确保患者和医疗人员的健康与安全是首要任务。为实现这一目标,医院需要依赖高科技设施来维持最佳的环境条件。特别是,多参数空气质量监测系统和压差监测系统在这一方面发挥了不可替代的作用。【卓振思众】多参数空气质量监测…...
VB.net复制Ntag213卡写入UID
本示例使用的发卡器:https://item.taobao.com/item.htm?ftt&id615391857885 一、读取旧Ntag卡的UID和数据 Private Sub Button15_Click(sender As Object, e As EventArgs) Handles Button15.Click轻松读卡技术支持:网站:Dim i, j As IntegerDim cardidhex, …...
Cesium1.95中高性能加载1500个点
一、基本方式: 图标使用.png比.svg性能要好 <template><div id"cesiumContainer"></div><div class"toolbar"><button id"resetButton">重新生成点</button><span id"countDisplay&qu…...
在鸿蒙HarmonyOS 5中使用DevEco Studio实现录音机应用
1. 项目配置与权限设置 1.1 配置module.json5 {"module": {"requestPermissions": [{"name": "ohos.permission.MICROPHONE","reason": "录音需要麦克风权限"},{"name": "ohos.permission.WRITE…...
Android第十三次面试总结(四大 组件基础)
Activity生命周期和四大启动模式详解 一、Activity 生命周期 Activity 的生命周期由一系列回调方法组成,用于管理其创建、可见性、焦点和销毁过程。以下是核心方法及其调用时机: onCreate() 调用时机:Activity 首次创建时调用。…...
高效线程安全的单例模式:Python 中的懒加载与自定义初始化参数
高效线程安全的单例模式:Python 中的懒加载与自定义初始化参数 在软件开发中,单例模式(Singleton Pattern)是一种常见的设计模式,确保一个类仅有一个实例,并提供一个全局访问点。在多线程环境下,实现单例模式时需要注意线程安全问题,以防止多个线程同时创建实例,导致…...
C++使用 new 来创建动态数组
问题: 不能使用变量定义数组大小 原因: 这是因为数组在内存中是连续存储的,编译器需要在编译阶段就确定数组的大小,以便正确地分配内存空间。如果允许使用变量来定义数组的大小,那么编译器就无法在编译时确定数组的大…...
力扣热题100 k个一组反转链表题解
题目: 代码: func reverseKGroup(head *ListNode, k int) *ListNode {cur : headfor i : 0; i < k; i {if cur nil {return head}cur cur.Next}newHead : reverse(head, cur)head.Next reverseKGroup(cur, k)return newHead }func reverse(start, end *ListNode) *ListN…...
如何应对敏捷转型中的团队阻力
应对敏捷转型中的团队阻力需要明确沟通敏捷转型目的、提升团队参与感、提供充分的培训与支持、逐步推进敏捷实践、建立清晰的奖励和反馈机制。其中,明确沟通敏捷转型目的尤为关键,团队成员只有清晰理解转型背后的原因和利益,才能降低对变化的…...
破解路内监管盲区:免布线低位视频桩重塑停车管理新标准
城市路内停车管理常因行道树遮挡、高位设备盲区等问题,导致车牌识别率低、逃费率高,传统模式在复杂路段束手无策。免布线低位视频桩凭借超低视角部署与智能算法,正成为破局关键。该设备安装于车位侧方0.5-0.7米高度,直接规避树枝遮…...
【Linux手册】探秘系统世界:从用户交互到硬件底层的全链路工作之旅
目录 前言 操作系统与驱动程序 是什么,为什么 怎么做 system call 用户操作接口 总结 前言 日常生活中,我们在使用电子设备时,我们所输入执行的每一条指令最终大多都会作用到硬件上,比如下载一款软件最终会下载到硬盘上&am…...
