Mrli
别装作很努力,
因为结局不会陪你演戏。
Contacts:
QQ博客园

Unity游戏开发学习

2021/12/25 Unity
Word count: 3,898 | Reading time: 17min

组件

RigidBody

刚体,会给物体增加重力

spring joint:

弹簧关节,可以连接两个刚体,使得两个物体能够像弹簧一样运动。

Box Collision

增加碰撞检测

图片渲染器

Sprite精灵,游戏开发中指一张图片

  • Sprite Render 图片渲染器,是用来渲染图片的组件
1
2
3
4
5
6
7
8
9
10
11
// 随机生成怪物头像
void createMonster(){
float x = Random.Range(-2, 2);
float y = 5;
GameObject monster = Instantiate(MonsterPrefab);
monster.transform.position = new Vector3(x, y, 0);

int index = Random.Range(0, images.Length);
SpriteRender render = monster.GetComponent<SpriteRender>();
render.sprite = this.images[index]
}

所以图片对象其实是, Empty Gameobject+Sprite Render组件构成的

注:UI下的Image不是Sprite

API:

Transform

Translate()

根据 translation 的方向和距离移动变换。如果 relativeTo 被省略或设置为 Space.Self,则会相对于变换的本地轴来应用该移动。(在场景视图中选择对象时显示的 X、Y 和 Z 轴。) 如果 relativeToSpace.World,则相对于世界坐标系应用该移动。

Rotate

使用 Transform.Rotate 以各种方式旋转 GameObjects。通常以欧拉角而不是四元数提供旋转。

获得键鼠操作

获取鼠标:

1
2
3
4
Input.GetMouseButton(int)
int = 0时,获取鼠标左键
int = 1时,获取鼠标右键
int = 2时,获取鼠标中键

获取方向键盘:

1
2
3
4
Input.Getkey(KeyCode.UPArrow) 上键
Input.GetKey(KeyCode.DownArrow) 下键
Input.GetKey(KeyCode.LeftArrow) 左键
Input.GetKey(KeyCode.RightArrow) 右键

获得对象上的组件

  • 每个组件上都有transform组件,因此在scripts脚本中可以通过transform.positiontransform.rotation来获得位置和朝向。
  • 获得对象下的刚体组件: Rigidbody rd = bullet.GetComponent<Rigidbody>();

镜头跟随——移动摄像头位置

  • Camera.main.transform.position = Vector3.Lerp( Camera.main.transform.position, new Vector3(Mathf.Clamp(posX, 0, 15), Camera.main.transform.position.y, Camera.main.transform.position.y,), smooth* Time.deltaTime)

注:被挂载的脚本也算个组件,其名称为脚本名称,如下GetComponent<Bird>的Bird为挂载的Bird.cs脚本

1
collision.transform.GetComponent<Bird>().Hurt();

获得脚本挂载的对象以及上级对象

1
2
3
4
5
6
7
8
9
// 在脚本中transform为脚本挂载对象的transform组件
transform
// 如果要获得 脚本挂载对象 的上级对象的transform(如果有的话)
transform.parent
transform.root
// 如果要进一步获得gameObject的话,通过 transform.parent.gameObject来获得

// 获得父级对象后进一步
transform.parent.getComponent<Text>()

有获得父对象就有获得子对象

1
2
3
for (int i = 0; i < transform.childCount; i ++ ){
transform.GetChild(i).GetComponent<Collider2D>().enabled = false;
}

脚本获得对象:

  1. public Text TimeText将对象通过public暴露,然后手动拖拽指定

  2. private Text TimeText指定为private,然后Aware()TimeText = GameObject.Find("TimeText").GetComponent<Text>();

    1.无法查找隐藏对象

    • 隐藏对象包括查找路径的任何一个父节点隐藏(active=false)
    • ▲:解决方案,可以先让它设置为active=true,然后在start中找到后设置为active=false,再在用的时候设置为true

    2.如果查找不在最上层,建议合理使用路径查找,路径查找是把双刃剑

    **优点1:**解决查找中可能出现的重名问题。
    **优点2:**如果有完全的路径,减少查找范围,减少查找时间。

    注:与GameObject.Find相似的api还有GameObject.FindWithTag

  3. Transform.Find

    1.可以查找隐藏对象
    2.支持路径查找
    3.查找隐藏对象的前提是transform所在的根节点必须可见,即active=true

实际开发中会将功能预制体放到一个可见的GameObject目录下,将这个GameObject目录作为查找根节点,下面的所有对象(隐藏、非隐藏)都可以查找到。

SetActive和enable区别

相同点:

  1. gameObject.SetActive函数控制的是上图复选框1是否勾选,不勾选,那么隐藏,勾选即显示;
  2. enabled属性控制的是复选框2是否勾选,显隐规则同上;

不同点:

  1. SetActive是针对元素对象gameObject的;enabled是针对gameObject下的某个component的
  2. 如果UI的复选框1默认不勾选, 无论复选框2是否勾选, 那么当UI显示时, MyImage.cs的所有函数都不会执行, 这就说明没有加载此脚本到内存;
  3. 如果UI的复选框1默认勾选,复选框2默认不勾选,那么当UI显示时, MyImage.cs的Awake函数会执行,其他函数均不会执行,这就说明此脚本会加载到内存中, 除了执行Awake函数外,其他函数均不执行.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// enable
// 通过拖拉加入
public List<Bird> birds;
birds[i].enabled = true;
birds[i].sp.enabled = true;

// 这边是Image UI
public GameObject win;
public GameObject lose;
//输了
lose.SetActive(true);
win.SetActive(true);

// UI ==> canvas下的UI其实也是GameObject
button.SetActive(false);

本地持久化保存与读取的类

PlayerPrefs: 工作原理非常简单,以键值对的形式将数据保存在文件中,然后程序可以根据这个名称取出上次保存的数值。

Playerprefs静态方法

  • SetFloat(),SetInt(),SetString()` 写入数据
  • GetFloat(),GetInt(),GetString() 读取数据
  • DeleteKey(),DeleteAll() 删除数据
  • HasKey("SS") 检查数据,是否有该键
  • Save()

创建和销毁prefab对象

1
2
3
4
5
6
7
8
9
10
11
12
13
// 通过预制体来动态创建实例对象, Instantiate参数有很多, 具体见api文档
GameObject bullet = Instantiate(myPrefab)
GameObject go = Instantiate(score, transform.position + new Vector3(0,0.5f,0), Quaternion.identity);

bullet.transform.position = transform.position + new Vector3(0, 1f, 0)

// 销毁预制体 ==> 绑定在子弹prefab上的脚本
transform.Translate(0, step, 0, Space.Self)
Vector3 sp = Camera.main.WorldToScreen(transform.position)
if (sp.y > Screen.height){
Destroy(this.gameObject, 1.5f) // 延时1.5s后
// 即GameObject.Destroy
}

场景切换

Unity游戏开发中,单个Scene解决所有问题似乎不可能,那么多个Scene之间的切换是必然存在。如果仅仅是切换,似乎什么都好说,但是在场景比较大的时候不想让玩家等待加载或者说场景与场景之间想通过一些画面、动画表现出一些让玩家期待的东西,大家就要去认真考虑。这篇文章主要介绍两种增加切换中如何播放画面或者动画等等,提高玩家的浸入感,当然你也可以做成无缝的场景

1
2
3
4
5
6
7
8
9
10
11
12
// 异步切换场景
// AsyncOperation async = Application.LoadLevelAsync("MyBigLevel");
// 同步切换
// Application.LoadLevel("Middle");

using UnityEngine.SceneManagement;

public void Retry() {
Time.timeScale = 1;
// 大多数情况下使用的方法
UnityEngine.SceneManagement.SceneManager.LoadScene(2);
}

重玩功能:可以通过切换到当前场景来实现,由于切换会把切之前的元素销毁掉,并重新建新的元素,因此相当于重玩

游戏暂停

设置Time.timeScale = 0 将会暂停所有和帧率有关的事情。这些主要是指所有的物理事件和依赖时间的函数、刚体力和速度等,而且FixedUpdate会受到影响,会被暂停(不是Update),即timeScale =0 时将不会调用FixedUpdate函数了。

(1)Time.timeScale = 0可以暂停游戏,Time.timeScale = 1恢复正常,但这是作用于整个游戏的设置,不单单是当前场景,记得在需要的时候重置回Time.timeScale = 1。当然也可以使用Time.timeScale来做游戏的1倍、2倍整体加速。

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

private void Awake()
{
// 获得 gameObject自身的动画组件
anim = GetComponent<Animator>();
}

public void Retry() {
Time.timeScale = 1;
UnityEngine.SceneManagement.SceneManager.LoadScene(2);
}

/// <summary>
/// 点击了pause按钮
/// </summary>
public void Pause()
{
//1、播放pause动画
anim.SetBool("isPause", true);
button.SetActive(false);


// ▲ 这边可以看到除了设置动态和Time.timeScale以外还对其他做了修改
if (GameManager._instance.birds.Count > 0) {
if (GameManager._instance.birds[0].isReleased == false) { //没有飞出
GameManager._instance.birds[0].canMove = false;
}
}
}

/// <summary>
/// 点击了继续按钮
/// </summary>
public void Resume() {
//1、播放resume动画
Time.timeScale = 1;
anim.SetBool("isPause", false);

if (GameManager._instance.birds.Count > 0)
{
if (GameManager._instance.birds[0].isReleased == false)
{ //没有飞出
GameManager._instance.birds[0].canMove = true;
}
}
}

public void Home()
{
Time.timeScale = 1;
UnityEngine.SceneManagement.SceneManager.LoadScene(1);
}

碰撞检测

碰撞体的编辑

点εEdit collider进行编辑…,规定可碰撞的范围和形状
常见的形状:

  • 方形 Box Collider2D
  • 圆形 Circle collider2D
  • 不规则边缘 Edge Collider2D
  • 胶囊形状 Capsule Collider2D
  1. 带有collider组件的对象脚本,将组件的IsTrigger属性勾选上

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 开始接触
    void OnTriggerEnter(Collider collider) {
    Debug.Log("开始接触");
    }

    // 接触结束
    void OnTriggerExit(Collider collider) {
    Debug.Log("接触结束");
    }

    // 接触持续中
    void OnTriggerStay(Collider collider) {
    Debug.Log("接触持续中");
    }
  2. 带有collider组件的对象脚本,发生碰撞时会执行如下钩子函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 碰撞开始
    void OnCollisionEnter(Collision collision) {
    // 销毁当前游戏物体
    Destroy(this.gameObject);
    }

    // 碰撞结束
    void OnCollisionExit(Collision collision) {

    }

    // 碰撞持续中
    void OnCollisionStay(Collision collision) {

    }

两者的区别在于,将对象collider组件的isTriiger属性勾上后,游戏物体发生接触的时候就不会有碰撞的效果了,而是会直接穿过去,即碰撞逻辑需要我们自己手写了。

【值得注意的是,触发器回调的这三个方法的参数都是Collider类型,表示的就是被碰撞的游戏物体的触发器组件对象。】

1
2
3
4
5
6
7
// 获得被碰撞物体的name
var name = collision.collider.name;
// 获得被碰撞物体的tag
var tag = collision.collider.tag;

// 感觉更好记
collision.transform.GetComponent<Bird>().Hurt();

Resources.Load动态加载资源

使用这种方式加载资源,首先需要下Asset目录下创建一个名为Resources的文件夹,这个命名是U3D规定的方式,然后把资源文件放进去,Cube放在Resource中的Prebs中,而Sphere放在Resources跟目录下

1
2
3
4
5
private string cubePath = "Prebs/MyCubePreb";
private string spherePath = "MySpherePreb";

Object cubePreb = Resources.Load(cubePath, typeof(GameObject));
Object spherePreb = Resources.Load(spherePath, typeof(GameObject));

DontDestroyOnLoad

新场景的负载会破坏所有当前场景对象。调用Object.DontDestroyOnLoad在级别加载期间保存对象。如果目标对象是组件或游戏对象,Unity还将保留Transform的所有子对象。对象.DontDestroyOnLoad不返回值。使用typeof操作符更改参数类型。

建议把需要DontDestroyOnLoad的游戏对象放到一个在游戏逻辑中不会返回的一个场景,比如说放到登录时加载的那个场景,,,或者代码使用一个静态变量做标志,(static bool isHave )是否DontDestroyOnLoad过,若DontDestroyOnLoad就将其赋值为True,

想要找到这个游戏物体的话,搜索Find 是可以找到的,或者使用标签的形式FindGameObjectWithTag

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
// 登陆页面的Canvas挂载的必启动文件
public class Main : MonoBehaviour {
public GameObject ClientSystem;

public
// Use this for initialization

//180,49;
void Start () {
// 在场景切换的时候不销毁ClientSystem对象
DontDestroyOnLoad(ClientSystem);
}

// Update is called once per frame
void Update () {
}

}

// ClientSystem上挂载的服务器通信脚本
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Net;
using System.Net.Sockets;
using System.Text;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
using System.Threading;

public class Client : MonoBehaviour
{
public GameObject LoadCavas;
public GameObject Ball;
public GameObject StartBackGround;
public GameObject InputText;

float rota = 1.6f;
bool isBallRota = false;

string HeartBeatMsg = "Hello";
string otherNameMsg = "otherName";
string HookMsg = "Hook";
string ExitMsg = "exit";

string SendMsg = "";
string RecvMsg = "";

const string ip = "127.0.0.1";
const int port = 9999;
IPEndPoint ipe = new IPEndPoint(IPAddress.Parse(ip), port);
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

private void Start()
{
LoadCavas.SetActive(false);

}

private void Update()
{
//Loading的皮卡丘动画
if (isBallRota && Ball != null)
{
Ball.transform.Rotate(Vector3.forward, rota);
}
//处理收到的消息
if (RecvMsg != "")
{
MsgHandle();
}
//用SendMsg保存要发送的消息
SendMsg = GetSendMsg();
}

void LoadAnimate(bool Start)
{
if (Start)
{
LoadCavas.SetActive(true);
isBallRota = true;
StartBackGround.GetComponent<Image>().sprite = null;
StartBackGround.GetComponent<Image>().color = new Color(0, 0, 0, 0);
InputText.GetComponent<InputField>().text = "Matching....";
}
else
{
if (LoadCavas != null)
{
LoadCavas.SetActive(false);
isBallRota = false;
StartBackGround.GetComponent<Image>().sprite = Resources.Load<Sprite>("Button");
StartBackGround.GetComponent<Image>().color = Color.white;
InputText.GetComponent<InputField>().text = PlayerPrefs.GetString("UserName");
}
}
}

public void Link()
{

LoadAnimate(true);

socket.Connect(ipe);
name = PlayerPrefs.GetString("UserName");
byte[] nameB = Encoding.ASCII.GetBytes(name);
socket.Send(nameB);
string recvStr = "";
byte[] recvBytes = new byte[1024];
int bytes;
bytes = socket.Receive(recvBytes, recvBytes.Length, 0);//从服务器端接受返回信息
recvStr += Encoding.ASCII.GetString(recvBytes, 0, bytes);
Debug.Log("client get message" + recvStr);
Thread thread = new Thread(Communicate);
thread.Start();

}

private void OnApplicationQuit()
{
if (socket.Connected)
{
socket.Send(Encoding.ASCII.GetBytes(ExitMsg));
byte[] recvBytes = new byte[1024];
int bytes;
bytes = socket.Receive(recvBytes, recvBytes.Length, 0);
socket.Close();
}
}

private void Communicate()
{
while (socket.Connected)
{
byte[] heartBeatStrB = Encoding.ASCII.GetBytes(SendMsg);
socket.Send(heartBeatStrB);
string recvStr = "";
byte[] recvBytes = new byte[1024];
int bytes;
bytes = socket.Receive(recvBytes, recvBytes.Length, 0);//从服务器端接受返回信息
recvStr += Encoding.ASCII.GetString(recvBytes, 0, bytes);
RecvMsg = recvStr;
Thread.Sleep(100);
}
}

void MsgHandle()
{
//先重置收到的消息
string msg = RecvMsg;
RecvMsg = "";
if (msg.StartsWith(otherNameMsg))
{
LoadAnimate(false);
Variable.OtherName = msg.Remove(otherNameMsg.Length);

}
else if (msg == HookMsg)
{
Variable.isOtherHookDown = true;
}
else if (msg == "Left")
{
isBallRota = false;
Variable.isSelfLeft = true;
SceneManager.LoadScene("Game");
}
else if (msg == "Right")
{
isBallRota = false;
Variable.isSelfLeft = false;
SceneManager.LoadScene("Game");
}
else if (msg == "exit")
{
SceneManager.LoadScene("Main");
LoadAnimate(false);
InputText.GetComponent<InputField>().text = "Mathing Failed QAQ";

}
}

string GetSendMsg()
{
string msg = HeartBeatMsg;
if (Variable.isExit)
{
msg = ExitMsg;
Variable.isExit = false;
}
else
{
if (Variable.isSelfHookDown)
{
msg = HookMsg;
Variable.isSelfHookDown = false;
}
}
return msg;
}
}

问题:

渲染顺序:

简单总结一下,

  1. 决定Sprite render的渲染关系的层级顺序是:

    1. Camera: 不同的Camera的Depth
    2. sorting layer: 相同Camera下的不同SortingLayer
    3. sortingorder: 相同SortingLayer下的不同Z轴/Order in Layer
  2. 改变控件之间的层级关系
    (1)同一canvas下:
    改变控件transform的SiblingIndex,
    transform.GetSiblingIndex();
    transform.SetSiblingIndex(int index); //index值越大,越后渲染,层级越大,越显示在前面
    (2)不同Canvas下:
    设置Canvas下的Sort Order //Sort Order值越大,越后渲染,层级越大,越显示在前面

学习参考视频:

官方API文档

免费的素材网站

Author: Mrli

Link: https://nymrli.top/2021/12/16/Unity游戏开发学习/

Copyright: All articles in this blog are licensed under CC BY-NC-SA 3.0 unless stating additionally.

< PreviousPost
Python中关于图片的操作
NextPost >
软件架构课程学习笔记
CATALOG
  1. 1. 组件
    1. 1.1. RigidBody
    2. 1.2. spring joint:
    3. 1.3. Box Collision
    4. 1.4. 图片渲染器
  2. 2. API:
    1. 2.1. Transform
      1. 2.1.1. Translate()
      2. 2.1.2. Rotate
    2. 2.2. 获得键鼠操作
    3. 2.3. 获得对象上的组件
    4. 2.4. 获得脚本挂载的对象以及上级对象
    5. 2.5. 脚本获得对象:
    6. 2.6. SetActive和enable区别
    7. 2.7. 本地持久化保存与读取的类
    8. 2.8. 创建和销毁prefab对象
    9. 2.9. 场景切换
    10. 2.10. 游戏暂停
    11. 2.11. 碰撞检测
      1. 2.11.1. 碰撞体的编辑
    12. 2.12. Resources.Load动态加载资源
    13. 2.13. DontDestroyOnLoad
  3. 3. 问题:
    1. 3.1. 渲染顺序:
  4. 4. 学习参考视频:
  5. 5. 官方API文档
    1. 5.1. 免费的素材网站