【Archive】Make Your First Game

角色控制,包括移动与多段跳

朝向

1
2
3
4
float forward = Input.GetAxisRaw("Horizontal");// 返回-1,0,1
if (forward != 0) {
transform.localScale = new Vector3(forward, 1, 1);
}

移动

1
2
3
4
5
6
7
8
9
10
11
public float speed;
private Rigidbody2D body;
void Start(){
body = GetComponent<Rigidbody2D>();
}
void FixedUpDate(){
float horizontalMove = Input.GetAxis("Horizontal");
body.velocity =
new Vector2(horizontalMove * speed * Time.fixedDeltaTime, body.velocity.y);
// Input.GetAxis()返回-1f至1f的浮点,这么做会使角色移动产生“脚滑”的现象
}

跳跃

以跳跃作为核心玩法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public float jumpForce;
public int finalJumpCount;// 用于重置jumpCount
public Transform footPoint;// 用于检测脚是否与地面接触
public LayerMask ground;// 代表地面Layer

private int jumpCount;// 代表玩家当前可跳跃的次数
private bool jumpPressed = false, isJumped = false;
private float time = -1f;// 用于确保jumpPressed随时处于正确状态的计时器
void Update() {
// 在Update中确保能敏感地接收到起跳请求,再去FixedUpDate中进行Rigidbody相关的运算
if (Input.GetButtonDown("Jump") && jumpCount > 0) {
jumpPressed = true;
time = Time.time + 0.05f;
}
if (time >= 0 && time < Time.time) {
// 经过一个短暂的计时后重置jumpPressed
jumpPressed = false;
time = -1f;
}
}
void FixedUpdate(){
if (Physics2D.OverlapCircle(footPoint.position, 0.1f, ground)) {
// 落地时重置跳跃相关的参数,而且要避免刚起跳时OverlapCircle检测到地面
jumpCount = finalJumpCount;
isJumped = false;
}
if (jumpPressed) {
// 跳跃
if (Physics2D.OverlapCircle(footPoint.position, 0.3f, ground)) {
// 地面起跳
body.velocity = new Vector2(body.velocity.x, jumpForce);
jumpCount--;
jumpPressed = false;
isJumped = true;// 代表经过跳跃离开地面
}
else if (isJumped) {
// 空中起跳
body.velocity = new Vector2(body.velocity.x, jumpForce);
jumpCount--;
jumpPressed = false;
}
// 剩余情况为不经跳跃离开地面且没有消灭敌人/吃到樱桃时请求跳跃,不予起跳
}
}

玩家与收集物的交互,与敌人的交互。

收集品、简单的UI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using UnityEngine.UI;

public Text diamondCount;
private int diamond = 0;

private void OnTriggerEnter2D(Collider2D collision) {
if (collision.CompareTag("Cherry")) {
Destroy(collision.gameObject);
jumpCount++;
isJumped = true;
// 无论怎样离开地面,吃到樱桃后解锁跳跃条件
}
if (collision.CompareTag("Diamond")) {;
Destroy(collision.gameObject);
diamond++;
diamondCount.text = diamond.ToString();
}
}

敌人

父类

首先创建所有敌人类的父类 Enemy,其中播放死亡动画和摧毁对象的方法对所有敌人来说都是通用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Enemy : MonoBehaviour
{
protected Animator animator;
protected Rigidbody2D rb;

private int deathID;
protected virtual void Start() {
animator = GetComponent<Animator>();
rb = GetComponent<Rigidbody2D>();
deathID = Animator.StringToHash("Death");
}
public void Death() {
GetComponent<Collider2D>().enabled = false;
rb.constraints = RigidbodyConstraints2D.FreezeAll;
// 沿所有轴冻结旋转和移动,保证Enemy不至于一边被玩家一脚踹飞一边播放死亡动画
animator.SetTrigger(deathID);
}

// 在Death动画的结尾调用
public void Disappear() {
Destroy(gameObject);
}
}

Frog类

Frog 需要在一定范围内以跳跃的方式移动,这个坐标由他的两个子物体 leftPoint 和 rightPoint 提供。使用动画事件方便地使函数在正确的时机被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class EnemyFrog : Enemy
{
public Transform leftPoint, rightPoint;
public float speed;
public float jumpForce;
public LayerMask ground;
public bool forwardLeft;

private Rigidbody2D body;
private Collider2D collisionBox;
private float leftX, rightX;
private int jumpingID, fallingID;
protected override void Start() {
base.Start();
body = GetComponent<Rigidbody2D>();
collisionBox = GetComponent<Collider2D>();
leftX = leftPoint.position.x;
rightX = rightPoint.position.x;
Destroy(leftPoint.gameObject);
Destroy(rightPoint.gameObject);
// 获取移动范围坐标后即可摧毁标志对象
jumpingID = Animator.StringToHash("Jumping");
fallingID = Animator.StringToHash("Falling");
}
void Update() {
if (animator.GetBool(jumpingID) && body.velocity.y <= 0) {
animator.SetBool(jumpingID, false);
animator.SetBool(fallingID, true);
}
if (animator.GetBool(fallingID) && collisionBox.IsTouchingLayers(ground)) {
animator.SetBool(fallingID, false);
body.velocity = new Vector2(0, 0);
}
}
void movement() {// 在Idel动画的结尾调用
if (forwardLeft) {// 朝左
if (transform.position.x <= leftX) {// 需要转身
forwardLeft = false;
transform.localScale = new Vector3(-1, 1, 1);
}
body.velocity = new Vector2(-transform.localScale.x * speed, jumpForce);
animator.SetBool(jumpingID, true);
}
else {// 朝右
if (transform.position.x >= rightX) {// 需要转身
forwardLeft = true;
transform.localScale = new Vector3(1, 1, 1);
}
body.velocity = new Vector2(-transform.localScale.x * speed, jumpForce);
animator.SetBool(jumpingID, true);
}
}
}

PlayerController类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private void OnCollisionEnter2D(Collision2D collision) {
if (collision.gameObject.CompareTag("Enemy")) {// 触敌
if (animator.GetBool(fallingID) &&
transform.position.y - collision.transform.position.y > 0.35f) {

// 保证触敌时两者处于一个相对垂直的位置
Enemy enemy = collision.gameObject.GetComponent<Enemy>();
enemy.Death();
body.velocity = new Vector2(body.velocity.x, jumpForce);
jumpCount = finalJumpCount;
isJumped = true;
// 无论怎样离开地面,消灭敌人后解锁跳跃条件
}
else {// 受伤
isHurt = true;// 用于切换动画以及屏蔽受伤状态下移动相关的输入
if (transform.position.x <= collision.transform.position.x) {// 右侧触敌
body.velocity = new Vector2(-10f, body.velocity.y + jumpForce * 0.7f);
}
else {// 左侧接敌
body.velocity = new Vector2(10f, body.velocity.y + jumpForce * 0.7f);
}
}
}
}

对话框、运动透视、菜单中调节音量。

对话框提示

对话框的渐入渐出和移动由录制动画实现,通用的 Dialog 类挂载在一个 Trigger 上,负责动画的切换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Dialog : MonoBehaviour
{
public GameObject dialog;

private Animator animator;
private int enterID,exitID;
private void Start() {
animator = dialog.GetComponent<Animator>();
enterID = Animator.StringToHash("Enter");
exitID = Animator.StringToHash("Exit");
}
private void OnTriggerStay2D(Collider2D collision) {
if (collision.CompareTag("Player") && !dialog.activeSelf) {
dialog.SetActive(true);
animator.SetBool(enterID, true);
animator.SetBool(exitID, false);
}
}
private void OnTriggerExit2D(Collider2D collision) {
if (collision.CompareTag("Player")) {
animator.SetBool(enterID, false);
animator.SetBool(exitID, true);
Invoke("setActiceFalse", 0.1f);//等待退出动画结束
}
}
private void setActiceFalse() {
dialog.SetActive(false);
}
}

运动透视

将这个脚本挂载在某一层背景上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Parallax : MonoBehaviour
{
public new Transform camera;
public float moveRate;
public bool lockY;

private float bgStartX, bgStartY;
private float cmStartX, cmStartY;
void Start(){
bgStartX = transform.position.x;
bgStartY = transform.position.y;

cmStartX = camera.position.x;
cmStartY = camera.position.y;
}
void Update(){
float difX = camera.position.x - cmStartX;
float difY = camera.position.y - cmStartY;

if (lockY) {
transform.position = new Vector2(bgStartX + difX * moveRate,
transform.position.y);
}
else {
transform.position =
new Vector2(bgStartX + difX, bgStartY + difY) * moveRate;
}
}
}

音量调节

将所有 Source 输出到一个 Mixer 内,若想更新 Mixer 的属性(比如音量),需先将音频组检视面板中的属性暴露出来再 Set。再将该函数交由一 SliderUI 调用,动态地传入值即可。

1
2
3
public void SetAudioVolume(float volume) {
audioMixer.SetFloat("MainVolume", volume);
}

相关链接

M_Studio
Input
Rigidbody2D
Physics2D
RigidbodyConstraints2D
timeScale