avatar

Unity--勇士传说笔记--Page1

主要是想记一些功能的实现思路,可能不是最优解。如果有更好的思路可以在评论交流一下

第一部分—场景

比例调整

这次选择的是2d像素风的素材,在导入的时候要调整一下 *Pixel Per Unit(每个单位所展示的像素量)*来填充画面,大部分素材绘制的时候都会使用88、16*16 、3232等比例,届时按需修改就行

(如下图所示,这一个格子就是一个单位)

素材切割

为了方便后续使用,需要用Sprite Editor把素材切开

*注:因为默认设置的原因,需要把Sprite Mode改成Multiple才能使用Sprite Editor

以人物举例,切的话可以按**格子数量(Cell Count)**切,然后这里的锚点需要改到脚底

对于场景来说就不能这样切了,需要根据格子大小(Cell Size)来把其切成小块来复用拼接

绘制

在场景中建好Tilemap直接把图扔进Tile Palette画就行,如果需要去绘制复杂的前景和背景,可以多建几层Tilemap,再通过调整图层(Sorting Layer)以达到想要的效果,数字较大的图层会覆盖较小的图层

规则瓦片(Rule Tile)

真画地图的时候不可能一点点选着画,因为还没画完人就升天了(x

于是就有了规则瓦片,配置好每张图的出现规则(通过判断上下左右有没有物体实现的),画的时候就能根据规则生成了,这里贴出地面的一部分作为演示

配好后拖到Tile Palette就可以用了

动态瓦片(Animated Tile)

这里是要做瀑布流动的效果,原理是通过图片切换来实现的,和上面一样配好图和顺序就行

第二部分—角色

刚体

人物刚体需要把Z轴冻结,不然会出现倒头就睡的情况

对于场景可以TimeMap Collider和Composite Collider搭配食用更佳,记得要把刚体类型改为Static,不改会导致平台掉落出去

玩家操作

1.按键

最新版Unity提供了新的Input System,可以直接配置按键去调用,省去了挺多麻烦

启用方法:点击顶栏的Edit-Project Settings-Player-Other Settings-Active Input Handling改为(Input System Package(New))

完成之后创建一个Input Action配置按键,搭配Player Input组件挂载到物体上就能使用了

(当然也可以让Input Action生成一个Script方便其他的Script来调用)

2.移动

实现逻辑如下,先读取按键输入的值

1
InputDirection = inputControl.Gameplay.Move.ReadValue<Vector2>();

通过读取的值,在对应的方向也施加一个速度即可

1
2
//涉及物理的部分要放到FixedUpdate中执行
rb.velocity = new Vector2(YourSpeed * Time.deltaTime * InputDirection.x, rb.velocity.y);

然后就是人物朝向的问题,毕竟你也不希望向另一侧移动时用的是太空步罢(x

这里可以判断输入进来的值是否大于零、小于零,对这两种情况分别赋一个1、-1的值,再将其赋给transform.localscale

1
transform.localScale = new Vector3(faceDir, 1, 1);

3.碰撞检测

玩家:

在人物中心点绘制一个范围来检测是否有需要碰撞的图层

1
2
3
4
5
6
public Vector2 bottomOffset;
public float checkRadius;
public LayerMask groundLayer;
public bool isGrounded;

isGrounded = Physics2D.OverlapCircle((Vector2)transform.position+bottomOffset, checkRadius, groundLayer);

范围如果拿不准的话可以绘制一下碰撞区域

1
2
3
4
private void OnDrawGizmosSelected()
{
Gizmos.DrawWireSphere((Vector2)transform.position + bottomOffset, checkRadius);
}

敌人:

对于敌人来说得添加两个Collider,一个用来站立于平台,另一个用来接受攻击,为避免人挤人走不动路,善用Collider的Layer Overrides(仅2022.3.20以上才有,其他版本只能另想办法了)

4.跳跃

给刚体施加一个向上瞬时的力就行

1
2
3
4
5
6
//判断是否在地面,否则不能进行跳跃
if(physicsCheck.isGrounded)
{
//括号里分别为:方向,力度,施加的方式,这里是瞬间的力
rb.AddForce(transform.up * jumpForce, ForceMode2D.Impulse)
}

人物动画

1.准备动画

整一个Animator Controller挂到物体身上,创建Clip即可,动画的快慢和Samples有关,可以适当调整。

2.准备变量

接着在Animator-Parameters中创建一些变量用于后面播放动画,数值的话可以在物体上挂个Script去组件里读取

这里仅展示获取到Rigidbody的速度然后赋给变量(不写全是因为太长了)

1
2
3
rb =GetComponent<Rigidbody2D>();

anim.SetFloat("velocityX", Mathf.Abs(rb.velocity.x));

3.设置过渡和条件

两个都搞好了,设置一下条件就能触发了。在Animator窗口中右键Clip,点”Make Transition”,再点另一个Clip就能完成过渡设置,以此类推即可。

然后就该设置条件了,点击下图的箭头,在面板中就能看到设置了,具体数值可以按需获取。

这里的Walk动画需要设置一个来回(不知道这样说对不对),Exit Time(可以提前退出动画)、Fixed Duration、Transition Duration(过渡),这三个不需要,因为这里需要立即切换动画

4.多动画切换

比如这回的跳跃动画,涉及准备、起跳、空中、下落等四个动画的切换,落地动画不包含在内是因为要单独做一个Clip。

先右键创建一个BlendTree把动画丢进去,Type、Parameters和数值看着改

Transition大概这样画,Jump接到Any State是因为任何情况都可以起跳,Land之后直接Exit,如果接到Idle的话,落地之后会顿一下很不连贯

人物属性

属性的话分为:最大生命值、当前生命值,攻击力、攻击范围、攻击频率,这两部分建议是分开写(虽说写一起也行)

人物受击

流程也很简短:

进入碰撞范围→获取对方组件中的受伤函数(无敌时间也在内执行)

1
2
3
4
5
public void OnTriggerStay2D(Collider2D other)
{
//使用了语法糖.?,如果对面没有Character组件,则不会执行TakeDamage
other.GetComponent<Character>()?.TakeDamage(this);
}

执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//受伤
public void TakeDamage(Attack attacker)
{
if (invincible)
{
return;
}
if (CurrentHealth - attacker.damage > 0)
{
CurrentHealth -= attacker.damage;
triggerInvincible();
}
else
{
CurrentHealth = 0;
}
}

无敌时间,这里用了一个叫计时器的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private void Update()
{
if (invincible)
{
invincibleCounter -= Time.deltaTime;
if (invincibleCounter <= 0)
{
invincible = false;
}
}
}

//无敌状态
public void triggerInvincible()
{
//这里必须不为真才行,不然会导致无敌时间无限续杯
if (!invincible)
{
invincible = true;
//重置无敌时间
invincibleCounter = invincibleTime;
}
}

**后续补充:在这一步时,已经给敌人也添加上了刚体,一定要仔细检查要排除的图层,比如我的Box是排除了玩家和敌人层,Capsule是敌人层,勾选错误可能会导致无法触发攻击

受击动画

先新建一个层级,并在设置中调整层级的权重和混合模式(这里选的是叠加)

具体动画的话,新建一个动画片段扔到animator的新层级里,点Add Property→Material Color 手动调整rgba的值去实现受伤变色的效果,别忘了设置触发条件。

另外,由于Update代码是单次执行,所以要把受伤触发拆分出来

执行额外事件

随着开发,可能某一块功能需要执行的事件非常的多,这时可以使用Unity Events来减少代码量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public UnityEvent<Transform> onTakeDamage;

public void TakeDamage(Attack attacker)
{
if (invincible)
{
return;
}
if (CurrentHealth - attacker.damage > 0)
{
CurrentHealth -= attacker.damage;
triggerInvincible();
// 触发注册的受伤事件
onTakeDamage?.Invoke(attacker.transform);
}
else
{
CurrentHealth = 0;
}
}

受击反弹

实现部分如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void FixedUpdate()
{
//不处于受伤状态时才能移动
if (!isHurt)
{
Move();
}
}

public void GetHurt(Transform attacker)
{
isHurt = true;
rb.velocity = Vector2.zero; //让人物停下来
//方向计算的话,用当前人物坐标的x,减去攻击者坐标的x
Vector2 dir = new Vector2((transform.position.x - attacker.position.x), 0).normalized;
//normalized数值归一化,因为距离的远近会导致数值大小不一样

rb.AddForce(dir * hurtForce, ForceMode2D.Impulse);
}

写好之后把方法注册到上面写的事件里即可

然后还需要给受伤动画挂在一个Script(直接在动画的Inspector中创建就能自动生成需要的方法),用于更新isHurt变量的状态,以解决受击后人物无法移动的情况

1
2
3
4
5
6
7
public class HurtAnimation : StateMachineBehaviour
{
override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
animator.GetComponent<PlayerController>().isHurt = false;
}
}

人物死亡

动画扔到受伤层连到Any State上,增加一个Param,然后取消Loop Time(不取消会导致动画重复播放),这块也还是用事件注册

1
2
3
4
5
public void PlayerDead()
{
isDead= true;
inputControl.Gameplay.Disable();
}

人物攻击

动画部分能说的点只有切换,这里一共是做了三段的动画。

触发条件是:isAttack的值为真&触发Trigger

而切换条件则是在动画未播放完之前按下按键,即切换下一段动画,反之则直接退出

接着新建一个按键映射用来写脚本

还是老样子先注册攻击函数,这样就能实现三段攻击,不用combo作判断是因为变化非常快,导致连续攻击的时候不能进入后面的动画

1
2
3
4
5
6
7
inputControl.Gameplay.Attack.started += PlayerAttack;

private void PlayerAttack(InputAction.CallbackContext context)
{
playerAnimation.PlayerAttack();
isAttack = true;
}

为了解决第三段攻击执行完毕后还能接着攻击的问题,需要让动画在结束时将isAttack改为false

1
2
3
4
override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
animator.GetComponent<PlayerController>().isAttack = false;
}

攻击判定

创建三个Object绑上Polygon Collider用于绑定三段攻击的判定范围

然后把碰撞体的形状调整为适合每段攻击的形状,比如:

至于怎么在合适的时间判定,可以在Animation窗口添加对应攻击的开启条件,接着打上关键帧即可

别忘了把Attack脚本挂上去,要不然没法打出伤害。

另外,还需要在碰撞体的Layer Overrides—Contact Capture Layer中只勾选Enemy层(或者在上面排除这一层也行),这样做是因为攻击范围的碰撞体可能会打到玩家的碰撞体,导致伤敌一千自损八百。

顺带把敌人的Capsule碰撞体优先级改高点,以让其优先接受碰撞。

走A部分

因为动画部分没有给对应的素材,可能导致实现效果不是很好(所以我选择保留这一部分,同时记一下禁用走A的思路)

先在角色的控制Script中,给角色移动添加相应的条件,再把对应刚体的材质加上摩擦力即可

1
2
3
4
5
6
7
8
private void FixedUpdate()
{
if (!isHurt&&!isAttack) //isAttack为false时才能进行移动
{
Move();
}
}

这样改完后,播放动画时,角色会往前位移一段距离,所以还需要在退出动画的脚本中添加在进入动画时要把isAttack改成true(这个就纯看个人喜好了)

1
2
3
4
override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
animator.GetComponent<PlayerController>().isAttack = true;
}

刚体摩擦力

主要是为了解决不同状态下的摩擦力之类的,举个例子:比如在跳跃时摩擦力过大的话,角色容易卡到墙里。

具体实现代码如下,挺简单的

1
2
3
4
public void StateCheck()
{
coll.sharedMaterial = physicsCheck.isGrounded ? normal : wall;
}

剩下的内容就放到第二篇写了(鸽王.arw)