前言
unity自学笔记
参考教程
project1
项目界面
第一个脚本
Debug.Log的使用
public class SL00 : MonoBehaviour
{
// Start is called before the first frame update
void Start() // Debug的使用,在console里返回参数
{
Debug.Log("** 开始测试 . .");
GameObject obj = this.gameObject;
string name = obj.name;
Debug.Log("** 物体名字:" + this.name); // this.name其实指向this.gameObject.name简化了gameObject(this其实也能省)
Transform tr = this.gameObject.transform; // localposition获取本地坐标
Vector3 pos = this.gameObject.transform.position; // 查看可知position的数据类型
Debug.Log("** 物体的位置:" + pos.ToString("F3")); // 将数值类型(通常是浮点数)转换为字符串,并指定格式化字符串。在这里,"F3"是格式化字符串
}
}
运行后:
第二个脚本
获取和修改物体位置数据
public class SL01 : MonoBehaviour
{
// Start is called before the first frame update
void Start() // 获取和修改物体位置数据
{
Debug.Log("** 开始测试:");
Transform tr1 = this.gameObject.transform; // localPosition获取本地坐标
Vector3 pos1 = this.gameObject.transform.localPosition; // 查看可知position的数据类型
Debug.Log("** 物体的局部位置:" + pos1.ToString("F3"));
this.transform.localPosition = new Vector3(1.5f, 2.5f, 0);
}
}
注意到Transform组件的参数都是local,相对于父物体的。
同时还发现unity先执行了SL01修改了位置,再执行SL00
第三个脚本
小车实现匀速直行,按shift加F键可以锁定视角
public class SL02 : MonoBehaviour
{
// Start is called before the first frame update
void Start() // 对应10.1帧更新,用Update获取动态的参数;以及10.2的移动物体
{
Application.targetFrameRate = 60; // 不支持固定帧率,但可以设定一个近似帧率
}
// Update is called once per frame
void Update() // 游戏引擎每更新一帧都会调用Update方法
{
Debug.Log("** 帧更新 Update:Time = " + Time.time); // 获取游戏时间(如果是Time.deltaTime获取的是上次更新时间差)
float speed = 50;
float distance = speed * Time.deltaTime; // 两帧间匀速小车移动的距离
Vector3 pos = this.transform.localPosition; // 直接 + 0.01f 实现物体移动,但非匀速,还会导致FPS越高速度越快
pos.z += distance; // 0.01f;
this.transform.localPosition = pos;
}
}
project2
项目界面
第一个脚本
public class SL00 : MonoBehaviour
{
GameObject flag;
// Start is called before the first frame update
void Start()
{
flag = GameObject.Find("红旗"); // 获取坐标
this.transform.LookAt(flag.transform); // 使z轴指向目标
Vector3 p1 = this.transform.position;
Vector3 p2 = flag.transform.position;
//Vector3 p = p2 - p1;
//float distance = p.magnitude; // 计算两个坐标间的距离
float distance = Vector3.Distance(p2, p1); // 等价于上面两行
Debug.Log("** 两个物体之间的距离:" + distance);
}
// Update is called once per frame
void Update()
{
Vector3 p1 = this.transform.position;
Vector3 p2 = flag.transform.position;
Vector3 p = p2 - p1;
float distance = p.magnitude;
if ( distance > 0.3f ) // 做停止判断
{
float speed = 2;
float move = speed * Time.deltaTime;
this.transform.Translate(0, 0, move, Space.Self); // Translate(dx, dy, dz, 世界或自身坐标系)
}
}
}
第二个脚本
public class SL01 : MonoBehaviour
{
float rotateSpeed = 30;
// Start is called before the first frame update
void Start()
{
Application.targetFrameRate = 60;
// transform.rotation 官方不建议使用
transform.localEulerAngles = new Vector3(0, 45, 0);
}
// Update is called once per frame
void Update()
{
Vector3 angles = this.transform.localEulerAngles;
angles.y += rotateSpeed * Time.deltaTime;
this.transform.localEulerAngles = angles;
// 等价于this.transform.Rotate(0, rotateSpeed * Time.deltaTime, 0, Space.self);
}
}
第三个脚本
挂在地球上的实现地球自转
public class SL02 : MonoBehaviour
{
float rotateSpeed = 30;
void Update()
{
this.transform.Rotate(0, rotateSpeed * Time.deltaTime, 0, Space.Self);
}
}
第四个脚本
挂在卫星上实现卫星绕地球旋转
public class SL03 : MonoBehaviour
{
float RotateSpeed = 60;
void Update()
{
Transform parent = this.transform.parent; // 如果脚本附加到卫星上,this.transform.parent将指向卫星的上一级卫星系统的Transform组件(B是A的子级)
parent.Rotate(0, RotateSpeed * Time.deltaTime, 0, Space.Self);
}
}
角度问题
注意到角度问题,先说结论:调节任意物体Inspector界面的rotation的x坐标和z坐标都是绕自身(也就是物体坐标系)旋转,当调整y坐标时是绕父物体的物体坐标系的y轴旋转。
但是在例如Rotate函数里的Space.Self和Space.World就是绕物体坐标系和绕惯性坐标系(包括y轴)。
另外,如果是采用
// localEulerAngles
Vector3 angle = transform.localEulerAngles;
Debug.Log(angle);
angle.y += 1;
transform.localEulerAngles = angle;
// eulerAngles
Vector3 angle = transform.eulerAngles;
Debug.Log(angle);
angle.y += 1;
transform.eulerAngles = angle;
进行调节,就会发现修改localEulerAngles等价于修改Inspector界面上的Rotation。
先在卫星上调节rotation.z,发现卫星绕自身z轴旋转
再在卫星上调节rotation.y
说明调节卫星的rotation.y不是绕自身的Local(物体坐标系)的y轴旋转,而是绕他的父物体卫星系统的Local(物体坐标系)来旋转的
使用上面的代码的LocalEulerAngles运行后会有个直观的结果
可以看到子物体和父物体的z轴方向也不一样,所以我们试试绕z转
Vector3 angle = transform.localEulerAngles;
Debug.Log(angle);
angle.z += 1;
transform.localEulerAngles = angle;
显然发现地球是绕自身z轴旋转而非父物体z轴
但是,这里在使用该代码实现绕x轴的时候会出现LocalEulerAngles的x坐标卡在89点多过不去,这里应该是万向锁的问题导致
采用四元数来避免万向锁问题
// 获取当前的旋转四元数
Quaternion currentRotation = transform.rotation;
// 计算旋转增量
float rotationAmount = rotateSpeed * Time.deltaTime;
// 创建一个围绕物体局部坐标系Y轴旋转的四元数
Quaternion deltaRotation = Quaternion.Euler(rotationAmount, 0, 0);
// 应用旋转增量
transform.rotation = currentRotation * deltaRotation;
万向锁介绍
project3
项目界面
第一个脚本
public class SL00 : MonoBehaviour
{
// 执行顺序
// 依次先执行每个脚本组件的awake方法,第一阶段初始化完毕
// 再把每一个脚本组件的Start方法执行一边,第二阶段初始化完毕 执行脚本的组件顺序随机
// 帧更新......
private void Awake() // 消息函数 Start只执行一次,Awake比Start先调用,同时在组件禁用的情况下Awake仍调用但start不会
{
Debug.Log("** Awake(), 初始化:");
}
// Start is called before the first frame update
void Start()
{
Debug.Log("** Start(), 初始化:");
}
// Update is called once per frame
void Update()
{
}
private void OnEnable() // 比Start快比Awake慢,在启用组件时会执行一次
{
Debug.Log("** OnEnable(), 初始化:");
}
private void OnDisable() // 在禁用组件时会执行一次
{
Debug.Log("** OnDisable(), 初始化:");
}
}
注意到第一行的Debug.Log来自MainLogic脚本的Awake(),虽然该脚本处于禁用状态,但仍会执行里面的Awake()方法
第二个脚本
public class SL01 : MonoBehaviour
{
[Tooltip("y轴方向角速度")] // 为public变量添加注释
public float rotateSpeed = 60;
void Update()
{
this.transform.Rotate(0, rotateSpeed * Time.deltaTime, 0, Space.Self);
}
}
第三个脚本
重复内容
第四个脚本
该脚本挂在风扇(1)上,实现按住鼠标左键就旋转,松开停止
public class SL03 : MonoBehaviour
{
float speed = 0;
void Update()
{
if ( Input.GetMouseButtonDown( 0 ) ) // 鼠标事件,按下执行一次,0左键,1右键,2滚轮
{
Debug.Log("** 鼠标按下");
speed = 180;
}
if ( Input.GetMouseButtonUp( 0 ) )
{
Debug.Log("** 鼠标抬起");
speed = 0;
}
this.transform.Rotate(0, speed * Time.deltaTime, 0, Space.Self);
if ( Input.GetMouseButton(0) ) // 鼠标状态,持续返回bool值
{
Debug.Log("** 鼠标按下ing");
}
else
{
Debug.Log("** 鼠标抬起ing");
}
}
}
第五个脚本
该脚本挂在小火车(1)上,实现该物体移动超出屏幕范围后自动停止
public class SL04 : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
Vector3 pos = this.transform.position;
Vector3 screenPos = Camera.main.WorldToScreenPoint(pos); // 世界坐标转为屏幕坐标
Debug.Log("** " + this.name + "屏幕位置为:" + pos);
}
// Update is called once per frame
void Update()
{
if(Input.GetMouseButtonDown(0))
{
Vector3 mousePos = Input.mousePosition; // 获取鼠标屏幕坐标
Debug.Log("** 鼠标位置为:" + mousePos);
}
Vector3 pos = this.transform.position;
Vector3 screenPos = Camera.main.WorldToScreenPoint(pos);
if (screenPos.y < 0 || screenPos.y > Screen.height)
{
Debug.Log("** " + this.name + "超出屏幕范围");
}
else
{
transform.Translate(0, 0, 2 * Time.deltaTime, Space.Self);
}
}
}
第六个脚本
重复内容
第七个脚本
通过脚本操控其他组件,该脚本挂在游戏主控上
public class SL06 : MonoBehaviour
{
// Update is called once per frame
void Update() // 获取组件,用代码改变组件参数
{
if (Input.GetMouseButtonDown(0))
{
PlayMusic();
}
}
void PlayMusic() // play on Awake按键游戏开始时自动播放音乐
{
AudioSource audio = this.GetComponent<AudioSource>(); // 获取组件
audio.mute = false; // 通过代码修改组件参数,mute 被设置为 true,则该音频源将被静音,不会播放声音
if(audio.isPlaying)
{
Debug.Log("* 停止播放");
audio.Stop();
}
else
{
Debug.Log("* 开始播放音乐");
audio.Play();
}
}
}
project4
项目界面
第一个脚本
该脚本实现父类子类的寻找
public class SL00 : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
Transform parent = transform.parent; // parent的类型是Transform
Debug.Log("** 父级:" + parent.name);
foreach (Transform a in this.transform.parent) // 在transform组件下遍历出来的就是他的子节点的transform组件
{ // 且可以发现transform前面的this也可以省
Debug.Log("** " + parent.name + "的子物体:" + a.name);
}
// 按索引获取子节点
Transform child = parent.transform.GetChild(0);
Debug.Log("** " + parent.name + "的第一个子物体:" + child.name);
// 按名称获取子节点
Transform child1 = parent.transform.Find("bb"); // 注意Find只能找子物体,找不到bbcc,需指定写成bb/bbcc,表示从bb下查找子物体
Debug.Log("** " + parent.name + "的第二个子物体:" + child1.name);
}
}
第二个脚本
public class SL01 : MonoBehaviour
{
void Update()
{
if(Input.GetMouseButtonDown(0))
{
Test();
}
}
void Test()
{
Transform parentNode = transform.Find("/111"); // 在根目录下面找其子级111
transform.SetParent(parentNode);
// transform.SetParent(null); 此代码可以将null设为父级,这样他就不是任何节点的子级
Transform item = transform.Find("aa");
if (item.gameObject.activeSelf) // 判断物体是否显示,该属性是gameObject下的
{
item.gameObject.SetActive(false); // 如果显示就是设置为不显示
}
else
{
item.gameObject.SetActive(true);
}
}
}
第三个脚本
获取别的物体的组件,脚本,并修改参数
public class MainLogic : MonoBehaviour
{
public AudioSource bgm; // 这个比bgmNode更直接一些,直接获取组件,GameObject是先获取节点,再用GetComponent获取该节点的组件
public GameObject bgmNode;
public GameObject fanNode;
void Start() // 获取别的节点下的组件
{
Application.targetFrameRate = 60;
bgm.Play();
// 等价于
// AudioSource audio = bgmNode.GetComponent<AudioSource>(); 用GetComponent获取该节点下的XX组件
// audio.Play();
}
void Update()
{
if(Input.GetMouseButtonDown(0))
{
DoWork();
}
}
void DoWork() // void前面什么都不加默认为private void,表示只有内部可调用的方法,public void表示可在外部调用
{
FanLogic fan = fanNode.GetComponent<FanLogic>();
fan.rotateSpeed = 180;
}
}
其中FanLogic为
public class FanLogic : MonoBehaviour
{
public float rotateSpeed;
void Update()
{
this.transform.Rotate(0, rotateSpeed * Time.deltaTime, 0, Space.Self);
}
}
第四个脚本
该脚本实现一个简易的俄罗斯方块的下落效果
当按下空格键后调用ChangeShape()实现方块的切换,同时设置isMoving为True启动TransForm(),使得方块向下移动,当到地面时停止下降并更改isMoving1禁用ChangeShape()
public class playerLogic : MonoBehaviour
{
int index = 0;
float speed = 0;
bool isMoving;
bool isMoving1 = true;
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
isMoving = true;
if(isMoving1)
ChangeShape();
}
if (isMoving)
{
TransForm();
}
}
void ChangeShape()
{
Transform child = transform.GetChild(index);
child.gameObject.SetActive(false);
index += 1;
if (index >= transform.childCount)
index = 0;
child = transform.GetChild(index);
child.gameObject.SetActive(true);
}
void TransForm()
{
Vector3 pos = this.transform.position;
if (pos.y >= 0)
{
speed = 3;
transform.Translate(0, -speed * Time.deltaTime, 0, Space.World);
}
else
{
isMoving1 = false;
}
}
}
project5
项目界面
汽车操控
控制汽车移动
public class moveLogic : MonoBehaviour
{
[Tooltip("指定速度向量")]
public Vector3 speed;
public float moveSpeed = 10.0f;
public float rotationSpeed = 100.0f;
// Start is called before the first frame update
void Update()
{
// transform.Translate(speed * Time.deltaTime, Space.World);
float horizontalInput = Input.GetAxis("Horizontal"); // 获取水平输入
float verticalInput = Input.GetAxis("Vertical"); // 获取垂直输入
// 根据输入控制汽车的转向和移动
transform.Rotate(0, horizontalInput * rotationSpeed * Time.deltaTime, 0);
transform.Translate(0, 0, verticalInput * moveSpeed * Time.deltaTime);
}
}
控制镜头跟随
public class cameraLogic : MonoBehaviour
{
public Transform target; // 汽车的Transform组件
public Vector3 offset = new Vector3(0, 1, 0); // 相机偏移,根据需要调整
void LateUpdate()
{
// 设置摄像机位置为汽车位置加上偏移
transform.position = target.position + offset;
transform.rotation = target.rotation;
}
}
LateUpdate()
函数会在每一帧的最后被调用。这使得它非常适合处理需要在其他对象的 Update()
函数执行后执行的逻辑。通常,LateUpdate()
用于处理与游戏对象之间的相互作用,例如相机跟随、角色控制等,以确保这些逻辑不会受到其他对象 Update()
函数的影响。如果你有两个脚本JS1、JS2,两个脚本中都有Update()函数, 在JS1中有 lateUpdate ,JS2中没有。那么 lateUpdate 函数会等待JS1、JS2两个脚本的Update()函数 都执行完后才执行。
定时调用
Invoke(func, delay),只调用一次
IvokeRepeating(func, delay, interval),循环调用 (在延时delay秒后开始,然后每隔interval秒调用func函数)
IsInvoking(func),是否正在调度
CancelInvoke(func),取消调用,从调用队列中移除
以下脚本挂在Sphere上
public class SphereLogic : MonoBehaviour
{
public float speed = 2;
// Start is called before the first frame update
void Start() // start只执行一次,但invokerepeating会随时间不断执行,(永不结束的方法)
{
Application.targetFrameRate = 60;
Debug.Log("* " + Time.time);
this.Invoke("DoSomeThing", 2); // invoke定时调用一次函数,这里定时2秒后执行方法
}
// Update is called once per frame
void Update()
{
if(Input.GetMouseButtonDown(1))
{
/* if(IsInvoking("Translate")) // isinvoking判断Translate是否在invoke队列里
{
CancelInvoke("Translate"); // 如果有就删掉旧进程,避免重复调用(重复调用效果可能会叠加)
}
InvokeRepeating("Translate", 0, Time.deltaTime); // 实现在点击一次鼠标后持续移动*/
// 可以这样写
if (!IsInvoking("Translate")) // !bool值取反
{
InvokeRepeating("Translate", 0, Time.deltaTime);
}
if (IsInvoking("Change"))
CancelInvoke("Change");
else
this.InvokeRepeating("Change", 2, 2); // 2秒后开始调用,之后每2秒执行一次
}
}
void Change()
{
speed = -speed;
}
void DoSomeThing()
{
Debug.Log("* " + Time.time);
}
void Translate()
{
this.transform.Translate(0, speed * Time.deltaTime, 0, Space.Self);
Debug.Log(Time.time);
}
}
里面还有一点点下面红绿灯的结果图
以下脚本挂在风扇上,用循环调用实现风扇旋转的加减速
public class FanLogic : MonoBehaviour
{
float rotateSpeed = 0;
public float MaxSpeed = 720;
bool SpeedUp = false;
// Start is called before the first frame update
void Start()
{
InvokeRepeating("Acc", 0, 0.2f);
}
// Update is called once per frame
void Update()
{
transform.Rotate(0, rotateSpeed * Time.deltaTime, 0, Space.Self);
if (Input.GetMouseButtonDown(2))
{
SpeedUp = !SpeedUp;
}
}
void Acc()
{
if (SpeedUp)
{
if(rotateSpeed < MaxSpeed)
rotateSpeed += 20;
}
else
{
if (rotateSpeed > 0)
rotateSpeed -= 20;
else
rotateSpeed = 0;
}
}
}
资源数组
就是数组的使用,该脚本挂在cube下的音乐盒上
public class musicBox : MonoBehaviour
{
public AudioClip[] audioClips; // 创建数组
// Start is called before the first frame update
void Start()
{
if(audioClips == null || audioClips.Length == 0)
{
Debug.Log("* 出错");
}
}
// Update is called once per frame
void Update()
{
if(Input.GetMouseButtonDown(0))
{
NextSong();
}
}
void NextSong() // 实现随机播放歌曲
{
int index = Random.Range(0, audioClips.Length);
AudioSource ad = GetComponent<AudioSource>();
ad.clip = audioClips[index];
ad.Play(); // Play是AudioSource的方法,而AudioSource一次只能装一个音频
Debug.Log("** 播放第" + index + "首:" + audioClips[index].name);
}
}
换汤不换药,挂在cube上
public class materialsChange : MonoBehaviour
{
public Material[] material;
int index = 0;
void Update()
{
if (Input.GetMouseButtonDown(0))
{
colorChange();
}
}
void colorChange()
{
MeshRenderer ad = GetComponent<MeshRenderer>();
ad.material = material[index];
index += 1;
if(index >= material.Length)
index = 0;
}
}
红绿灯
public class LightLogic : MonoBehaviour
{
[Tooltip("红、黄、绿颜色指定")]
public Material[] materials;
int index = 0;
// Start is called before the first frame update
void Start()
{
ChangeColor();
}
void ChangeColor()
{
Transform child = this.transform.GetChild(1);
MeshRenderer ad = child.gameObject.GetComponent<MeshRenderer>();
ad.material = materials[index];
if (index == 0)
{
Invoke("ChangeColor", 4);
}
if (index == 1)
{
Invoke("ChangeColor", 1);
}
if (index == 2)
{
Invoke("ChangeColor", 4);
}
if (index < 2)
index += 1;
else
index = 0;
}
}
project6
项目界面
火控系统(预制体的使用)
实现子弹的特性(有射程,有速度)
public class BulletLogic : MonoBehaviour // BulletLogic 记录并实现子弹的特性:飞行速度、射程
{
public float speed = 1;
public float maxDistance = 2000;
// Start is called before the first frame update
void Start()
{
float lifeTime = maxDistance / speed;
Invoke("SelfDestroy", lifeTime);
}
// Update is called once per frame
void Update()
{
Vector3 globalSpeed = new Vector3(0, 0, speed);
//Vector3 localSpeed = transform.InverseTransformVector(globalSpeed); 一些瞎写
transform.Translate(globalSpeed*Time.deltaTime, Space.Self);
}
void SelfDestroy()
{
Object.Destroy(this.gameObject);
}
}
火控系统脚本
public class FireLogic : MonoBehaviour // 火控代码,集合从炮台到子弹的所有参数
{
public GameObject bulletPrefab;
public Transform parent;
public Transform firePoint;
public Transform cannon;
public float bulletSpeed;
public float maxDistance;
public float rotateSpeed;
Vector3 eulerangle;
// Start is called before the first frame update
void Start()
{
Debug.Log(cannon.transform.eulerAngles);
}
// Update is called once per frame
void Update()
{
if (Input.GetMouseButtonDown(0))
{
TestFire();
}
float horizontalInput = Input.GetAxis("Horizontal");
// float verticalInput = Input.GetAxis("Vertical");
//loat currentXRotation = cannon.transform.eulerAngles.x;
//if (currentXRotation >= -30 && currentXRotation <= 30 ) // 有问题,因为无论如何都会超出一点点,所以这样判断会导致控制不了
//{ // 所以要拆开来,向上抬过头就禁用向上抬,但能用向下转
// cannon.Rotate(-verticalInput * rotateSpeed * Time.deltaTime, horizontalInput * rotateSpeed * Time.deltaTime, 0, Space.World);
//}
cannon.transform.Rotate(0, horizontalInput * rotateSpeed * Time.deltaTime, 0, Space.World);
float delta = rotateSpeed * Time.deltaTime;
if(Input.GetKey(KeyCode.S))
{
if (eulerangle.x < 30)
eulerangle.x += delta;
}
if (Input.GetKey(KeyCode.W))
{
if (eulerangle.x > -30)
eulerangle.x -= delta;
}
Vector3 term = cannon.transform.eulerAngles; // 利用term暂存canon的欧拉角,实现保存上面绕y轴的旋转,
term.x = eulerangle.x; // 然后用eulerangle记录上下旋转的变化量
cannon.transform.eulerAngles = term;
Debug.Log("*" + eulerangle); // x坐标显示的是-30到30
// Debug.Log(currentXRotation); 欧拉角日志显示的是0到30,360到330
Debug.Log("**" + cannon.transform.eulerAngles);
}
void TestFire()
{
GameObject Bullet = Object.Instantiate(bulletPrefab, parent);
Bullet.transform.position = firePoint.position; // 因为不在同一父节点下面,所以使用世界坐标
Transform paoTa = transform.Find("/炮塔");
Bullet.transform.eulerAngles = paoTa.eulerAngles;
BulletLogic script = Bullet.GetComponent<BulletLogic>();
script.speed = this.bulletSpeed;
script.maxDistance = maxDistance;
}
}