Unity教程(九)角色攻击的改进
Unity开发2D类银河恶魔城游戏学习笔记
Unity教程(零)Unity和VS的使用相关内容
Unity教程(一)开始学习状态机
Unity教程(二)角色移动的实现
Unity教程(三)角色跳跃的实现
Unity教程(四)碰撞检测
Unity教程(五)角色冲刺的实现
Unity教程(六)角色滑墙的实现
Unity教程(七)角色蹬墙跳的实现
Unity教程(八)角色攻击的基本实现
Unity教程(九)角色攻击的改进
Unity教程(十)Tile Palette搭建平台关卡
如果你更习惯用知乎
Unity开发2D类银河恶魔城游戏学习笔记目录
文章目录
- Unity开发2D类银河恶魔城游戏学习笔记
- 前言
- 一、概述
- 二、基础攻击的改进
- (1)调整动画提升流畅度
- (2)解决攻击时滑动的问题
- (3)解决两次攻击间角色移动的问题
- (4)攻击间移动
- (5)添加攻击方向
- (6)加速操作(不需要做)
- 三、代码整理
- 总结 完整代码
- Player.cs
- PlayerPrimaryAttackState.cs
- PlayerIdleState.cs
前言
本文为Udemy课程The Ultimate Guide to Creating an RPG Game in Unity学习笔记,如有错误,欢迎指正。
本节进行角色基本攻击的改进。
对应b站视频:
【Unity教程】从0编程制作类银河恶魔城游戏P40
【Unity教程】从0编程制作类银河恶魔城游戏P41
一、概述
本节主要进行角色基本攻击的改进。
空闲状态到移动状态之间的转换添加条件 ! isBusy

本节改进了基本攻击的小问题,添加了攻击移动和攻击方向,提升了动画流畅度。在最后对代码进行了一些整理。整体结构如下:

二、基础攻击的改进
(1)调整动画提升流畅度
调整三个攻击动画的采样率,并且将最后一个攻击动画事件提前。



(2)解决攻击时滑动的问题
现在我们攻击时角色会来回滑动,因为我们的角色还保持着移动的速度但已经播放攻击动画了。

这里教程中的解决方式是在攻击时将角色速度置为0。
但我个人感觉这种处理方式会让攻击缺少一些灵活性。但如果想实现边移动边攻击就需要有另外的动画和另外的逻辑来实现,需要另行设计,所以在此先按教程中的实现。
此外,我们可以在进入时给stateTimer赋一个值,让角色停顿一下再攻击,做出惯性的效果。
在PlayerPrimaryAttack中修改:
//进入public override void Enter(){base.Enter();if (comboCounter > 2 || Time.time > lastTimeAttacked + comboWindow)comboCounter = 0;player.anim.SetInteger("comboCounter",comboCounter);stateTimer = 0.1f;}// 更新public override void Update(){base.Update();if (stateTimer < 0)rb.velocity = new Vector2(0, 0);if(triggerCalled)stateMachine.ChangeState(player.idleState);}

(3)解决两次攻击间角色移动的问题
我依然认为这种处理方法缺乏灵活性,但可以先学习一下他的处理方式。
如果需要解决这个问题,我们可以使用协程。我参照了以下文章的讲解:
进程、线程和协程之间的区别和联系
Unity 协程(Coroutine)原理与用法详解
Unity官方手册
协程是通过迭代器来实现功能的,通过关键字IEnumerator来定义。
启动协程:
StartCoroutine(IEnumerator routine:通过方法形式调用
StartCoroutine(string methodName,object values):带参数的通过方法名进行调用
停止携程:
StopCoroutine(string methodName:通过方法名(字符串)来进行
StopCoroutine(IEnumerator routine:通过方法形式来调用
StopCoroutine(Coroutine routine):通过指定的协程来关闭
yield方法:
yield return null; 暂停协程等待下一帧继续执行
yield return 0或其他数字; 暂停协程等待下一帧继续执行
yield return new WairForSeconds(时间); 等待规定时间后继续执行
yield return StartCoroutine(“协程方法名”); 开启一个协程(嵌套协程)
在Player中创建一个参数isBusy,并定义BusyFor函数
public bool isBusy { get; private set; }public IEnumerator BusyFor(float _seconds){isBusy = true;yield return new WaitForSeconds(_seconds);isBusy = false;}
在每次攻击结束时调用,在PlayerPriamaryAttack
//退出public override void Exit(){base.Exit();player.StartCoroutine("BusyFor", 0.15f);comboCounter++;lastTimeAttacked = Time.time;}
然后给空闲状态转到移动状态添加一个 ! Busy 的条件:
//更新public override void Update(){base.Update();//切换到移动状态if(xInput!=0 && !player.isBusy)stateMachine.ChangeState(player.moveState);}
在攻击期间即使我一直按着移动键,角色也不能移动了,效果如下:

(4)攻击间移动
给连击中的每一段设置一个位移。
在Player中添加变量攻击位移数组
[Header("Attack details")]public float[] attackMovement;
在开始攻击时设置位移
//进入public override void Enter(){base.Enter();if (comboCounter > 2 || Time.time > lastTimeAttacked + comboWindow)comboCounter = 0;player.anim.SetInteger("comboCounter",comboCounter);player.SetVelocity(player.attackMovement[comboCounter] * player.facingDir, rb.velocity.y);stateTimer = 0.1f;}
给数列赋值,调整数值直到你想要的效果


(5)添加攻击方向
现在的攻击还有一个小问题,在攻击后马上按相反方向键向角色身后攻击,我们会发现角色完全没有转向。
因为我们在攻击时始终是向着角色面向的方向,没有翻转。
我们在PlayerPrimaryAttack中添加这个功能:
//进入public override void Enter(){base.Enter();if (comboCounter > 2 || Time.time > lastTimeAttacked + comboWindow)comboCounter = 0;player.anim.SetInteger("comboCounter",comboCounter);float attackDir = player.facingDir;if (xInput != 0)attackDir = xInput;player.SetVelocity(player.attackMovement[comboCounter].x * attackDir, player.attackMovement[comboCounter].y);stateTimer = 0.1f;}
当没有输入时,向角色面向方向攻击;当有输入时,向输入方向攻击。现在角色可以迅速回身攻击了。

(6)加速操作(不需要做)
教程中顺便讲到了使所有动画加速的操作。这个操作可以实现不同武器不同攻速等操作,或者实现整体加速。
用player.anim.speed 进行实现,在进入攻击状态时加速,在退出时恢复原速。
//进入public override void Enter(){base.Enter();if (comboCounter > 2 || Time.time > lastTimeAttacked + comboWindow)comboCounter = 0;player.anim.SetInteger("comboCounter",comboCounter);player.anim.speed = 3.0f;player.SetVelocity(player.attackMovement[comboCounter].x * player.facingDir, player.attackMovement[comboCounter].y);stateTimer = 0.1f;}//退出public override void Exit(){base.Exit();player.StartCoroutine("BusyFor", 0.15f);player.anim.speed = 1.0f;comboCounter++;lastTimeAttacked = Time.time;}
为了效果明显,我调了三倍速。

三、代码整理
速度置零的操作,我们在Player中写一个函数ZeroVelocity()用来调用
//速度置零public void ZeroVelocity() => rb.velocity = new Vector2(0, 0);
在PlayerPrimaryAttack中改为调用函数
//PlayerPrimaryAttackState:基本攻击状态// 更新public override void Update(){base.Update();if (stateTimer < 0)player.ZeroVelocity();if(triggerCalled)stateMachine.ChangeState(player.idleState);}
Player中代码划分区域
速度设置
#region 速度设置//速度置零public void ZeroVelocity() => rb.velocity = new Vector2(0, 0);//设置速度public void SetVelocity(float _xVelocity, float _yVelocity){rb.velocity = new Vector2(_xVelocity, _yVelocity);FlipController(_xVelocity);}#endregion
翻转
#region 翻转//翻转实现public void Flip(){facingDir = -1 * facingDir;facingRight = !facingRight;transform.Rotate(0, 180, 0);}//翻转控制public void FlipController(float _x){if (_x > 0 && !facingRight)Flip();else if(_x < 0 && facingRight)Flip();}#endregion
碰撞
#region 碰撞//碰撞检测public bool isGroundDetected() => Physics2D.Raycast(groundCheck.position, Vector2.down, groundCheckDistance, whatIsGround);public bool isWallDetected() => Physics2D.Raycast(wallCheck.position,Vector2.right * facingDir,wallCheckDistance,whatIsGround);//绘制碰撞检测private void OnDrawGizmos(){Gizmos.DrawLine(groundCheck.position, new Vector3(groundCheck.position.x, groundCheck.position.y - groundCheckDistance));Gizmos.DrawLine(wallCheck.position, new Vector3(wallCheck.position.x+ wallCheckDistance, wallCheck.position.y));}#endregion

给PlayerPrimaryAttack改名为PlayerPrimaryAttackState
右键状态名->重命名->Enter


总结 完整代码
Player.cs
添加攻击位移变量
添加isBusy和协程
//Player:玩家
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class Player : MonoBehaviour
{[Header("Attack details")]public Vector2[] attackMovement;public bool isBusy { get; private set; }[Header("Move Info")]public float moveSpeed = 8f;public int facingDir { get; private set; } = 1;private bool facingRight = true;public float jumpForce = 12f;[Header("Dash Info")][SerializeField] private float dashCoolDown;private float dashUsageTimer;public float dashSpeed=25f;public float dashDuration=0.2f;public float dashDir { get; private set; }[Header("Collision Info")][SerializeField] private Transform groundCheck;[SerializeField] private float groundCheckDistance;[SerializeField] private Transform wallCheck;[SerializeField] private float wallCheckDistance;[SerializeField] private LayerMask whatIsGround;#region 组件public Animator anim { get; private set; }public Rigidbody2D rb { get; private set; }#endregion#region 状态public PlayerStateMachine StateMachine { get; private set; }public PlayerIdleState idleState { get; private set; }public PlayerMoveState moveState { get; private set; }public PlayerJumpState jumpState { get; private set; }public PlayerAirState airState { get; private set; }public PlayerDashState dashState { get; private set; }public PlayerWallSlideState wallSlideState { get; private set; }public PlayerWallJumpState wallJumpState { get; private set; }public PlayerPrimaryAttack primaryAttack { get; private set; } #endregion//创建对象private void Awake(){StateMachine = new PlayerStateMachine();idleState = new PlayerIdleState(StateMachine, this, "Idle");moveState = new PlayerMoveState(StateMachine, this, "Move");jumpState = new PlayerJumpState(StateMachine, this, "Jump");airState = new PlayerAirState(StateMachine, this, "Jump");dashState = new PlayerDashState(StateMachine, this, "Dash");wallSlideState = new PlayerWallSlideState(StateMachine, this, "WallSlide");wallJumpState = new PlayerWallJumpState(StateMachine, this, "Jump");primaryAttack = new PlayerPrimaryAttack(StateMachine, this, "Attack");anim = GetComponentInChildren<Animator>();rb = GetComponent<Rigidbody2D>();}// 设置初始状态private void Start(){StateMachine.Initialize(idleState);}// 更新private void Update(){StateMachine.currentState.Update();CheckForDashInput();}public IEnumerator BusyFor(float _seconds){isBusy = true;yield return new WaitForSeconds(_seconds);isBusy = false;}//设置触发器public void AnimationTrigger() => StateMachine.currentState.AnimationFinishTrigger();//检查冲刺输入public void CheckForDashInput(){dashUsageTimer -= Time.deltaTime;if (Input.GetKeyDown(KeyCode.LeftShift) && dashUsageTimer<0){dashUsageTimer = dashCoolDown;dashDir = Input.GetAxisRaw("Horizontal");if (dashDir == 0)dashDir = facingDir;StateMachine.ChangeState(dashState);}}#region 速度设置//速度置零public void ZeroVelocity() => rb.velocity = new Vector2(0, 0);//设置速度public void SetVelocity(float _xVelocity, float _yVelocity){rb.velocity = new Vector2(_xVelocity, _yVelocity);FlipController(_xVelocity);}#endregion#region 翻转//翻转实现public void Flip(){facingDir = -1 * facingDir;facingRight = !facingRight;transform.Rotate(0, 180, 0);}//翻转控制public void FlipController(float _x){if (_x > 0 && !facingRight)Flip();else if(_x < 0 && facingRight)Flip();}#endregion#region 碰撞//碰撞检测public bool isGroundDetected() => Physics2D.Raycast(groundCheck.position, Vector2.down, groundCheckDistance, whatIsGround);public bool isWallDetected() => Physics2D.Raycast(wallCheck.position,Vector2.right * facingDir,wallCheckDistance,whatIsGround);//绘制碰撞检测private void OnDrawGizmos(){Gizmos.DrawLine(groundCheck.position, new Vector3(groundCheck.position.x, groundCheck.position.y - groundCheckDistance));Gizmos.DrawLine(wallCheck.position, new Vector3(wallCheck.position.x+ wallCheckDistance, wallCheck.position.y));}#endregion
}
PlayerPrimaryAttackState.cs
攻击时速度置零
添加协程
添加攻击位移
添加攻击方向
//PlayerPrimaryAttackState:基本攻击状态
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class PlayerPrimaryAttackState : PlayerState
{private int comboCounter;private float lastTimeAttacked;private float comboWindow = 2;public PlayerPrimaryAttackState(PlayerStateMachine _stateMachine, Player _player, string _animBoolName) : base(_stateMachine, _player, _animBoolName){}//进入public override void Enter(){base.Enter();if (comboCounter > 2 || Time.time > lastTimeAttacked + comboWindow)comboCounter = 0;player.anim.SetInteger("comboCounter",comboCounter);float attackDir = player.facingDir;if (xInput != 0)attackDir = xInput;player.SetVelocity(player.attackMovement[comboCounter].x * attackDir, player.attackMovement[comboCounter].y);stateTimer = 0.1f;}//退出public override void Exit(){base.Exit();player.StartCoroutine("BusyFor", 0.15f);comboCounter++;lastTimeAttacked = Time.time;}// 更新public override void Update(){base.Update();if (stateTimer < 0)player.ZeroVelocity();if(triggerCalled)stateMachine.ChangeState(player.idleState);}
}
PlayerIdleState.cs
修改转到移动状态的条件
//PlayerIdleState:空闲状态
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR;public class PlayerIdleState : PlayerGroundedState
{//构造函数public PlayerIdleState(PlayerStateMachine _stateMachine, Player _player, string _animBoolName) : base(_stateMachine, _player, _animBoolName){}//进入public override void Enter(){base.Enter();player.SetVelocity(0, rb.velocity.y);}//退出public override void Exit(){base.Exit();}//更新public override void Update(){base.Update();//切换到移动状态if(xInput!=0 && !player.isBusy)stateMachine.ChangeState(player.moveState);}
}相关文章:
Unity教程(九)角色攻击的改进
Unity开发2D类银河恶魔城游戏学习笔记 Unity教程(零)Unity和VS的使用相关内容 Unity教程(一)开始学习状态机 Unity教程(二)角色移动的实现 Unity教程(三)角色跳跃的实现 Unity教程&…...
宠物空气净化器真的能除毛吗?有哪些选购技巧和品牌推荐修改版
夏日炎炎,有猫超甜。作为一名资深铲屎官,家里养有猫让我倍感幸福,夏天里有空调、有西瓜、有猫,这几个搭配在一起真的是超级爽。但在这么高温的夏天,家里养有宠物还是有不少烦恼的。比如家里的浮毛一直飘,似…...
Qt自定义注释
前言 是谁在Qt中编写代码,函数注释,类注释时,注释符号一个一个的敲? comment注释brief简洁的 Detailed详细的 第一步: 打开Qt 工具->选项->文本编辑器->片段 第二步: 点击添加 然后点击OK…...
【模电笔记】——信号的运算和处理电路(含电压比较器)
tips:本章节的笔记已经打包到word文档里啦,建议大家下载文章顶部资源(有时看不到是在审核中,等等就能下载了。手机端下载后里面的插图可能会乱,建议电脑下载,兼容性更好且易于观看),…...
Java之 equals()与==
目录 运算符用途:用于比较两个引用是否指向同一个对象。比较内容:比较的是内存地址(引用)。适用范围:适用于基本数据类型和对象引用 equals() 方法用途:用于比较两个对象的内容是否相同。比较内容…...
Ubuntu20.04 运行深蓝路径规划hw1
前言 环境: ubuntu 20.04 ; ROS版本: noetic; 问题 1、出现PCL报错:#error PCL requires C14 or above catkin_make 编译时,出现如下错误 解决: 在grid_path_searcher文件夹下面的CMakeLis…...
企业如何组建安全稳定的跨国通信网络
当企业在海外设有分公司时,如何建立一个安全且稳定的跨国通信网络是一个关键问题。为了确保跨国通信的安全和稳定性,可以考虑以下几种方案。 首先,可以在分公司之间搭建虚拟专用网络。虚拟专用网络通过对传输数据进行加密,保护通信…...
WordPress原创插件:Download-block-plugin下载按钮图标美化
WordPress原创插件:Download-block-plugin下载按钮图标美化 https://download.csdn.net/download/huayula/89632743...
前端【详解】缓存
HTTP 缓存 https://blog.csdn.net/weixin_41192489/article/details/136446539 CDN 缓存 CDN 全称 Content Delivery Network,即内容分发网络。 用户在浏览网站的时候,CDN会选择一个离用户最近的CDN边缘节点来响应用户的请求 CDN边缘节点的缓存机制与HTTP 缓存相同…...
P5821 【LK R-03】密码串匹配
[题目通道](【L&K R-03】密码串匹配 - 洛谷) 一道神题。 如果没有修改操作,翻转A数组或B数组后就是裸的FFT了 如果每次操作都暴力修改FFT时间复杂度显然爆炸 如果每次操作都不修改,记下修改序列,询问时加上修改序列的贡献,…...
httpx,一个网络请求的 Python 新宠儿
大家好!我是爱摸鱼的小鸿,关注我,收看每期的编程干货。 一个简单的库,也许能够开启我们的智慧之门, 一个普通的方法,也许能在危急时刻挽救我们于水深火热, 一个新颖的思维方式,也许能…...
计算机网络408考研 2014
1 计算机网络408考研2014年真题解析_哔哩哔哩_bilibili 1 111 1 11 1...
JavaScript 资源大全中文版
目录 JavaScript资源大全中文版 包管理器加载器组件管理器打包工具测试框架QA工具MVC 框架和库基于 Node 的 CMS 框架模板引擎文章和帖子数据可视化 时间轴电子表格 编辑器文档工具 文件函数式编程响应式编程数据结构日期字符串数字存储颜色国际化和本地化控制流路由安全性日志…...
如何获取能直接在浏览器打开的播放地址?
背景:需要在浏览器上直接打开设备的画面,但又不想二次开发 本文介绍一种极简的取流方式,不需要掌握前端开发知识,按照本文档拼接就能得到设备的播放地址 一、准备工作 1.将设备接入到萤石账号下。萤石设备接入指南:h…...
如何用 LangChain 实现一个Zero Shot智能决策器(附源码)
写在前面 最近一直在研究Agent和Tool的使用,今天给大家带来一篇何枝大佬(知乎何枝)的文章《如何用LangChain实现一个Zero Shot智能决策器》,并附上源码。 知乎:https://zhuanlan.zhihu.com/p/627333499LangChain是当…...
读完这本书,我终于搞懂了Transformer、BERT和GPT!【附PDF】
前言 《Transformer、BERT和GPT: 包括ChatGPT和提示工程》 是一本深入浅出地介绍自然语言处理领域前沿技术的专著,全书一共379页PDF,是截止到目前比较系统介绍NLP和GPT融合领域的书籍。 全书共十章,内容丰富,结构清晰,…...
仿RabbitMq简易消息队列基础篇(Muduo库的使用)
TOC Muduo库简介 Muduo由陈硕⼤佬开发,是⼀个基于⾮阻塞IO和事件驱动的C⾼并发TCP⽹络编程库。他是一款基于主从Reactor模型的网络库,其使用的线程模型是one loop per thread, 所谓 one loop per thread 指的是: 一个线程只能有一个事件循…...
.net SqlSugarHelper
NuGet安装: SqlSugarCore using SqlSugar; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks;namespace Namespace {public class SqlSugarHelper{public string _connectionString Custom…...
“AI能不能代替某某职业”,到底谁在破防?
前几天,公司在午间分享时谈到一个有趣的辩题:“AI能不能代替产品经理”,不仅双方辩手打了个你来我往,就连下面的吃瓜群众也进入红温状态。 “AI能不能代替xx”已经成为一个普遍的话题,在某乎上随手一刷就是不同的职业…...
智慧图书馆:构建高效视频智能管理方案,提升图书馆个性化服务
一、背景分析 随着信息技术的飞速发展,智慧图书馆作为现代公共文化服务的重要载体,正逐步从传统的纸质阅读空间向数字化、智能化方向转型。其中,视频智能管理方案作为智慧图书馆安全管理体系的重要组成部分,不仅能够有效提升图书…...
以下是对华为 HarmonyOS NETX 5属性动画(ArkTS)文档的结构化整理,通过层级标题、表格和代码块提升可读性:
一、属性动画概述NETX 作用:实现组件通用属性的渐变过渡效果,提升用户体验。支持属性:width、height、backgroundColor、opacity、scale、rotate、translate等。注意事项: 布局类属性(如宽高)变化时&#…...
8k长序列建模,蛋白质语言模型Prot42仅利用目标蛋白序列即可生成高亲和力结合剂
蛋白质结合剂(如抗体、抑制肽)在疾病诊断、成像分析及靶向药物递送等关键场景中发挥着不可替代的作用。传统上,高特异性蛋白质结合剂的开发高度依赖噬菌体展示、定向进化等实验技术,但这类方法普遍面临资源消耗巨大、研发周期冗长…...
vscode(仍待补充)
写于2025 6.9 主包将加入vscode这个更权威的圈子 vscode的基本使用 侧边栏 vscode还能连接ssh? debug时使用的launch文件 1.task.json {"tasks": [{"type": "cppbuild","label": "C/C: gcc.exe 生成活动文件"…...
关于nvm与node.js
1 安装nvm 安装过程中手动修改 nvm的安装路径, 以及修改 通过nvm安装node后正在使用的node的存放目录【这句话可能难以理解,但接着往下看你就了然了】 2 修改nvm中settings.txt文件配置 nvm安装成功后,通常在该文件中会出现以下配置&…...
【Redis技术进阶之路】「原理分析系列开篇」分析客户端和服务端网络诵信交互实现(服务端执行命令请求的过程 - 初始化服务器)
服务端执行命令请求的过程 【专栏简介】【技术大纲】【专栏目标】【目标人群】1. Redis爱好者与社区成员2. 后端开发和系统架构师3. 计算机专业的本科生及研究生 初始化服务器1. 初始化服务器状态结构初始化RedisServer变量 2. 加载相关系统配置和用户配置参数定制化配置参数案…...
2025 后端自学UNIAPP【项目实战:旅游项目】6、我的收藏页面
代码框架视图 1、先添加一个获取收藏景点的列表请求 【在文件my_api.js文件中添加】 // 引入公共的请求封装 import http from ./my_http.js// 登录接口(适配服务端返回 Token) export const login async (code, avatar) > {const res await http…...
Ascend NPU上适配Step-Audio模型
1 概述 1.1 简述 Step-Audio 是业界首个集语音理解与生成控制一体化的产品级开源实时语音对话系统,支持多语言对话(如 中文,英文,日语),语音情感(如 开心,悲伤)&#x…...
实现弹窗随键盘上移居中
实现弹窗随键盘上移的核心思路 在Android中,可以通过监听键盘的显示和隐藏事件,动态调整弹窗的位置。关键点在于获取键盘高度,并计算剩余屏幕空间以重新定位弹窗。 // 在Activity或Fragment中设置键盘监听 val rootView findViewById<V…...
优选算法第十二讲:队列 + 宽搜 优先级队列
优选算法第十二讲:队列 宽搜 && 优先级队列 1.N叉树的层序遍历2.二叉树的锯齿型层序遍历3.二叉树最大宽度4.在每个树行中找最大值5.优先级队列 -- 最后一块石头的重量6.数据流中的第K大元素7.前K个高频单词8.数据流的中位数 1.N叉树的层序遍历 2.二叉树的锯…...
【学习笔记】erase 删除顺序迭代器后迭代器失效的解决方案
目录 使用 erase 返回值继续迭代使用索引进行遍历 我们知道类似 vector 的顺序迭代器被删除后,迭代器会失效,因为顺序迭代器在内存中是连续存储的,元素删除后,后续元素会前移。 但一些场景中,我们又需要在执行删除操作…...
