news 2026/2/11 0:36:21

臭双非的技术学习之旅——C#与Unity结合篇(其二)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
臭双非的技术学习之旅——C#与Unity结合篇(其二)

来了来了,unity+C#的组合终于来了!

首先我们介绍一个对默认初始布局代码的更改

因为到了后期每个人都会产生属于自己专属的亦或是工程要求的模版。所以改变初始代码布局也是一个必备技能
这个虽然我们前期不咋需要,但可以交付于后面的小伙子,
1.对桌面上的unity应用程序右键打开文件所在文件夹
2.在文件夹中找到Date文件夹
3.在Date文件夹中找到Resourse文件夹
4.在resource文件夹中找到scripttample文件夹
5.在scripttample文件夹中找到81序号的文件,并以vs打开,
6.接下来就可以自定义代码布局了。
7.记得改完之后要保存。


介绍一下字段修饰

在unity编辑的时候,我们有些时候需要调节脚本里面的参数来达到我们预期的效果。
如果每次实例效果之后再去脚本里面改太过麻烦。
这是我们就可以采用字段修饰的方法,在unity中直接显示或者更改相关参数,这便是度脚本参数的字段修饰。
一般的形式是在参变量的上方加上[xxx],或者加上前缀。
上干货!

  • private int a=100;private私人的,那么在unity上就不会被显现
  • public int a=100;public 公开的,那么在unity上可以显现,并且可以更改,且更改值将成为最终值,脚本中只是做了初始化。
    这里我们解释一下public与private的另外一个重要功能区别,public在类中若是用于类的数据成员的修饰,那么在主程序中便可以直接进行修改,但若是在类的方法成员中对数据成员的值进行了限制,那么强行在外面非法修改会使该方法不被执行,造成一些牛马的问题。这个时候使用private就可以很好的解决这个问题
  • [range(1,100)]
    public int a=100;显现在unity中,并a的值可以在range中更改(全闭)
  • [HideInInspector]
    public int a=100;将能显现的进行隐藏
  • [SerializeField]
    private int a=100;将不能显示的显示出来;

介绍脚本生命周期的阶段知识点

注意这边介绍的都是相当于函数的东西
每个函数名都有他的功能,不可随意更改函数名!
具体的方法写在这些函数中!

初始阶段



###物理阶段


###逻辑阶段

我们的游戏逻辑主要放在update里面
而lateupdate可以放跟随作用的摄像机视角。
###输入事件

典型 的鼠标输入
###场景渲染

这个是个好东西,当摄像机照到时执行某个事件onbecamevisible
当摄像机没照到时执行某个事件
onbecameinvisible


各个类的功能理解

2024年3月5日08:27:52
把C#基础捋一遍后,我们得到了跟多的知识,现在再回过来看unity的内容,会比较清晰。
unity中要掌握的我觉得就是各个方法类的使用,了解它是干什么的,懂得如何去使用,这是学习的重点。
那么就开造吧!

  • Transform
  • GameObject
    用来创查找对象,可以在对象上添加组件,添加一个光源那就是手电筒
  • component
    组件类,管组件信息的。
  • Time
    这个是时间接口,调用时间的相关方法
    介绍TIme.deltatime,指的是渲染所花的时间,这保证了渲染的均衡性,因为每个机器的配置不同,渲染的速度也不同,乘上上每台机器特异的deltatime,我们就可以达到均衡的效果,
    介绍TIme.timescale,这个可以控制时间的流动!相当于The world!的效果,还可以减速,即达到慢动作。当他等于0时,方法fixedupdate直接被停止运行,而update不收影响。这个是好东西!
    然后还有其他好多好多!结合实际看来还是在联系中熟练比较ok

我们来做些小练习

如何制作小地图

由于小地图的制作需要联合uGUI的知识点。我们这里只是进行一些必要的C#代码解释

首先 厘清好思维,我们需要一个小地图, 需要的元素是 map的recttranslation 用于确定我们各个元素于小地图的相对位置,这里把所有的小地图上出现的活动元素归为map的子物体可以较好的实现, player transform,用于绑定map上的图标,与实际player的运动,有点映射的滋味。 Image item Image player_image 在这两个我们要理清楚 我们会预制一个item,这个item里面是我们想要的player图标,执行instantiate时他会在场景中创建一个item图像,之后我们把这个图像的信息赋值给player_image,对player_image的recttranslation进行修改,改变其在map上的位置来实现小地图上player移动功能。 private RectTransform rect; private Transform player_transform; private Image item; private Image player_image; // Start is called before the first frame update void Start() { item = Resources.Load<Image>("Image"); rect = GetComponent<RectTransform>(); player_transform = GameObject.FindWithTag("Player").transform; if (player_transform != null) { player_image = Instantiate(item); } } 这上面的是游戏初始化时的图标显现和相关信息的赋值 下面的是动态的图像渲染,每此update执行都会产生渲染一个新的图片。 // Update is called once per frame void Update() { player_image.rectTransform.sizeDelta = new Vector2(20,20); player_image.rectTransform.anchoredPosition = Vector2.zero; player_image.transform.SetParent(transform,false);设置父物体,这里的transform即map的transform,因为我们的脚本放在map上的,后面的bool值好像是世界坐标的是否。 player_image.sprite = Resources.Load<Sprite>("Texture/Py");因为我每帧都产生新的图片,所以要给每次的图片的精灵图元素进行赋值。 player_image.rectTransform.eulerAngles = new Vector3 (0,0,-player_transform.eulerAngles.y); 这里通过映射,把实际player的旋转角和图像的旋转联系起来。 }

查找所有敌人中血量最少的敌人

public class findTheMinus : MonoBehaviour { public void OnGUI() { if (GUILayout.Button("按我检测")) { enemy[] enemies = Object.FindObjectsOfType<enemy>(); enemy min = GetEnemy(enemies); min.GetComponent<MeshRenderer>().material.color = Color.red; } } public enemy GetEnemy(enemy[] all_the_enemy) { enemy enemy = all_the_enemy [0]; for(int i = 1; i < all_the_enemy.Length;i++) { if (enemy.HP >= all_the_enemy[i].HP) { enemy = all_the_enemy[i]; } } return enemy ; }

这个练习我们认识到系统的创建对象,理解类的含义确实还是有很多的帮助的,看看之前的project1,太乱了!
知道类的组成后,思路逐渐清晰。


##在父物体中寻找根据名称寻找子物体

public static Transform findyouchild(Transform father, string SonName) { Transform RightSon = father.Find(SonName); for (int i = 0; i < father.childCount; i++) { if (RightSon != null) { return RightSon; } else if (RightSon = null) { findyouchild(RightSon, SonName); } } return null; }

这是递归的方法,好像深度搜索。


根据名字搜索父对象中的子物体

public class findyouchild : MonoBehaviour { public static Transform Findyouchild(Transform father, string SonName) { Transform Rightson = father.Find(SonName); if (Rightson != null) { return Rightson; } int count_child_number=father.childCount; for (int i = 0; i < count_child_number; i++) { Transform Rightson1 = Findyouchild(father .GetChild(i),SonName); if(Rightson1 != null) { return Rightson1; } } return null; } void Update() { if (Input.GetKey(KeyCode.F)) { Transform rightson1 = Findyouchild(this.transform, "Cube (2)"); rightson1.GetComponent<MeshRenderer>().material.color = Color.green; } } }

时间的限制

我们在一些情况下,需要精确的秒计算来实现某个功能,而unity的是帧计算,0.02s这种,为了达到效果,我们需哟啊自己编写脚本来限定时间。
提供几个思路

  • 设立一个总计时器,time1=Time.time,还有一个下一秒限制器,time2=1
    若要实现一秒,即在每帧中判断,
    if(time1>time2)
    {
    这里写一秒时间节点的需求。
    time2=time1++;
    }
    这里的逻辑是利用帧的短时差,因为帧的时间为0.02级,那么我们可以通过整数的值差达到限制时间的效果。
  • 设立一个累加器,累加deltatime,当deltatime累加到1时,说明过了一秒,那么就执行你想要的操作,并在最后将deltatime归为0;
  • 还有一个就是在start 时调用

    直接解决。

倒计时的制作

这个我们要用到文本ui,注意这里组件的类!

public class TheCountDown : MonoBehaviour { TextMeshProUGUI textMeshPro; public int second=180; public float next_time=1; // Start is called before the first frame update void Start() { textMeshPro = this.GetComponent<TextMeshProUGUI>(); } // Update is called once per frame void Update() { if (Time.time >next_time) { second--; if (textMeshPro != null) { textMeshPro.text = string.Format("{0:d2}:{1:d2}", second / 60, second % 60); next_time = Time.time + 1; } } } }

这里练习了文本UI的基本操作

敌人固定路线巡逻

using System.Collections; using System.Collections.Generic; using Unity.VisualScripting; using UnityEngine; ///<summary> /// ///</summary> public class fixedwayline : MonoBehaviour { public Transform way_lines_root; public Transform[] waylines; public Transform[] waypoints; public int speed = 1; public int the_current_point=0; // Start is called before the first frame update void Start() { waylines = new Transform[way_lines_root.childCount]; for (int i = 0; i < way_lines_root.childCount; i++) { waylines[i] = way_lines_root.GetChild(i); } int the_random_line = Random.Range(0, waylines.Length);//得到随机数,并由此得到随机的路线。 Transform the_right_line = waylines[the_random_line];//这条随机出的路线被改名为正确的路线 the_right_line' waypoints = new Transform[the_right_line.childCount]; for (int i = 0; i < the_right_line.childCount; i++) { waypoints[i] = the_right_line.GetChild(i);//把这条路线的路线点放入waypoints数组中.我们就得到了含有所经路线位置的数组. } } // Update is called once per frame void Update() { find_way(); } public void look_the_point(Vector3 point) { this.transform.LookAt(point); } private void move_forword() { this.transform.Translate(0, 0, 5 * Time.deltaTime * speed, Space.Self); } private bool find_way() { if (waypoints == null || the_current_point >= waypoints.Length) { return false; } else { look_the_point(waypoints[the_current_point].position); move_forword(); if (Vector3.Distance(this.transform.position, waypoints[the_current_point].position) <= 0.5f) { the_current_point++; } return true; } } }

这里包含了路线的随机选择,向目标点的转向前进。

放大与缩小,瞄准镜!

public class TheGunSight : MonoBehaviour { Camera camera; bool open; public bool bigger_stste = false; public int[] the_bigger_level; public int level=0; // Start is called before the first frame update void Start() { camera = GetComponent<Camera>(); } // Update is called once per frame void Update() { if (Input.GetMouseButtonDown(0)) { level++; if (level < the_bigger_level.Length) { open = true; } else { open = false; } } if (!bigger_stste && open == true) { camera.fieldOfView = Mathf.Lerp(camera.fieldOfView, the_bigger_level[level], 0.1f); } if (open == false) { camera.fieldOfView = Mathf.Lerp(camera.fieldOfView, the_bigger_level[0], 0.1f); open = true; level = 0; } if (Input.GetMouseButtonDown(1)) { level = 0; camera.fieldOfView = Mathf.Lerp(camera.fieldOfView, the_bigger_level[level], 0.1f); } } }

这里我们学到了,我去寻找某个物体身上的组件,那么首先要声明一个该组件的对象,再将组件赋值给这个对象。之后再调用对象中的组件元素。


我们来介绍一些杂七杂八的知识点

在我们进行实操的时候我们可能会遇到一些迷茫的事情,就比如说,我们脚本要写改颜色,但我们不知道改颜色要在哪里改,在那个父级方法里面找。
这时候可以看看unity里面的星系,通常里面的顺序就可以按时出具体的方法在哪里
就比如说我们想要改颜色
那么就可以联想到渲染,进而想到材质,那么就可一顺腾摸瓜,找到材质的结构
this.GetComponent< MeshRenderer >().material.color = Color.green;
这里的this在不做其他修饰的情况下指的就是物体本身。
位置的变换也是如此


一个verygood的方法,invoke

  • invokerepeat(方法名,从第几秒开始,间隔的时间,可以让一个方法于什么时候开始调用,隔多少时间调用一次。
  • invoke(方法名,从第几秒开始)从第几秒开始调用方法。

关于一个数学函数lerp
对一个变量使用mathf.lerp方法时,可以让该变量实现从起始变量到中点变量由快到慢或由慢到快变化。
这个可以制造出一种渐变,就比瞄准镜的放大缩小过渡。
Mathf.Lerp(camera.fieldOfView, 20, 0.1f)
()中为起点,终点,变化速率。


据现在的已知信息,z的正半轴为正前方,x 的正半轴为正右方,运动正半轴为正上方。

进入牛马的重要部分:三维数学

介绍unity中的重要类:vector 向量

什么是向量?物体的位置就可以成为一个向量对象
他的理论知识点同数学上的知识点,
在C#中有三个方法可以求得向量的长度大小。
那向量的方向怎么描绘出来捏

Vector3 a1 = Camera.main.transform.position; Vector3 a2 = a1 /a1.magnitude; Vector3 a3 = a2.normalized ;

对于一个向量,描绘他的方向我们用与之方向相同的单位向量来表示,那么获得单位向量我们需要三步
第一,将该向量创建出来
第二。将该向量的大小变为1
第三,将该向量归一化,也叫标准化向量。
那这个玩意有什么用捏
向量,包含反向和大小,那么显而易见可以放到Translate方法中最为运动参数。一般我们要把向量进行归一化再放进去。

介绍角度弧度转化

根据已有的数学小知识,我们学习下如何用代码进行相应转化

  • 角转弧
    60–》pi/3==60*(pi/180)
    或者调用数学方法float degree=60*Mathf.Deg2Rad;
    这个还是挺好理解的deg即degree角度,2谐音同two即to,rad弧度。
  • 弧转角
    p1/3–>60度==p1/3*(180/p1)
    或者调用数学方法float rad = Mathf.PI / 3 * Mathf.Deg2Rad;

介绍向量求角度

我们在高中数学中学到的方法可以运用到这里来,面对实际问题灵活使用,数学终于可以为实践服务了!
介绍几个方法
在C#中,向量相乘并不是简单的中间加个乘号就可以了,要使用
vector方法。

点乘法

根据公式向量a*向量b=|a||b|cos(x),我们若是行求得ab之间的角度,那么就要求得x,那么x=arccos(向量a * 向量b)/|a||b|
之后这边的x一般表示方式为弧度,所以之后再来个弧转角就OK咯。
float a=Vector3.Dot (a1.normalized,a2.normalized);
float ha=Mathf.Acos(a)*Mathf.Rad2Deg;

叉乘法


使用这个课以判断两个物体的相对位置,在走边为+。右边即为负、通过这个可以克服点乘值域只有180-0的问题。


介绍欧拉角

这个玩意是表示旋转程度的概念,xyz,组成,分别表示在x,y,z轴上的旋转角度。
一个欧拉角存储在向量类里面,所以,看到vector3 a。可能不一定就是向量,还有可能是欧拉角
欧拉角没有方向和大小。
在对一个为物体的欧拉角做出改变时,以这样的形式操作。
Vector3 ol = this.transform.eulerAngles;
ol += new Vector3(1, 0, 0);
或者直接这么写
this .transform.eulerAngles += new Vector3(1,0,0) ;
他们表示,在x轴的旋转角度上加上1。
并且,(0,0,1)就是vector.forword。
但是,欧拉角存在一个大问题,叫作万向节死锁,x轴转到90度就转不动了。
所以欧拉角一般来用作简单的旋转,
所以为了解决这个牛马的问题,我们迎来了四元数

介绍四元数

四元数,顾名思义,他是通过四个数来确定物体的旋转,
根据公式

我们可以得到这四个数,那怎么用捏

确定旋转轴与旋转的角度,之后带入代码即可。
它可以根据任何一个向量作为轴,使得复杂度大打提高。
但我们不必再进行旋转操作时,不断的把这个代码写一遍,处了包装方法外,unity还给我们准备了方法。
a1.rotation= Quaternion.Euler(0,50,0);
以向量a1为轴,于y轴旋转50度。
两个四元数进行相乘为叠加。
比如我把a1.rotation=xxx,是把a1改到某个角度,而相乘就是说把a1进行当前位置上的叠加旋转。
而这种叠加的写法unity也提供了一种方法简便操作。
transform.Rotate(0, 0, 0);

欧拉角与四元数的转换

专研欧拉角

作为三维数学中的大头,我们必须把这个玩意怎么用,怎么用的方便,怎么去理解,怎么去思考等等搞清楚,不然将成为日后的诟病,把这个牛马狠狠的啃掉!

怎么去使用?

首先我们进行一波语法规范
若是想要进行某个物体的角度改变,使用欧拉角的话。
我们规定以下规范
this.transform.eulerAngles += new Vector3(1, 0, 0);
某物的欧拉角=之前的欧拉角加上新的欧拉角。
以上是欧拉角的基本用法
为了避免万向节死锁,我们继续搞四元数
还是规范语法
this.transform.rotation = Quaternion.Euler(40,0,0);
某物的旋转=我给出的四元数角度
this.transform.rotation*=Quaternion.Euler(40,0,0);
某物的旋转=我给出的四元数角度加上已有的四元数角度,注意,这里的加表示为*。
由上面的规范我们进而可以看出
欧拉角的计算要调出爱物体的欧拉角对象才能进行相应的计算
rotation是四元数对象,两个四元数相乘表示叠加。
而欧拉角与四元数的相互转换我们在上面也提到了。

复杂情景的向量角度运算

在基础的旋转指令结束后,真正困难的其实还是向量的理解。
向量,在unity其实是个坐标。万平米在数学中经常用坐标进行计算,但在这里就感觉很抽象
就比如向量a,向量b,弱项表示向量ab,即用b-a,但在unity中显现的是一个坐标,而所有坐标都是相对于世界原点的,这个b-a是一个坐标,我们把它命名为向量c
而这个向量c是这样的

它是从原点出发的,并且与我们理想的b-a等大同向。
想要达到理想的b-a,我们要加上向量a 的位置。
那这个向量到底是个坐标还是一个有向线段捏?
这个只是不同的理解角度。
我们可以从一些练习中去理解和掌握它。
我们可以思考下怎么进行一个向量的旋转,这是要改变坐标的。
把一个向量乘以四元数,并加上该向量的原初位置即可。
从这点来看,面对向量的位置角度改变,还是不要过多的去想坐标是怎么变化的,以空间有向线段的变换理解即可。
left= Quaternion.Euler(0, angle, 0)*c+plyer.position;

介绍一波比较OK的方法

这些都是vector3 的的方法成员

  • 直接求两向量之间的角度 angle(向量1,向量2);输出的是最小角度。
  • 限制向量的长度 clampMagnitude(向量,长度)。在某个向量后+上来起作用,当前面向量的长度小于规定值,该向量长度不变,当其长度大于规定长度时,将该向量长度化为规定值。
  • 产生一个投影向量 project(向量1,向量2)。返回一个向量1在向量2 上的投影向量。
  • 产生一个反射向量 reflect(向量1,向量2(平面法向量))
  • 进行助帧移动 moveTowards(起点,终点,速率),在这里向量理解为坐标比较合适。
  • 四元数轴角旋转 将一个无替代四元数,即rotation=quaternoin.angleaxis(角度,轴向)

专攻三维数学

在后面的实际开发过程中,我们老卡在这个三维数学上。
比如一个比较简单的需求
我们要摄像机在角色的一个球形面上进行移动。
这个真是不好写,这需要对三维数学的全面理解,与大量的案例练习才能独立解决此类问题。
所以我们就又回来了
2024年4月14日20:52:01
我们从头来全面的广泛的深刻的理解这个牛马的东西

向量

已知,向量在unity中就是表示为一个坐标。
我们可以获得她的方向与大小。
使用Translate(向量)可以使物体延该向量的方向移动相依的长度。
若要表示从从物体本身出发的一个向量,则加上自己的位置。
这里着重理解的应该是,你写下这个代码能干写什么,因为我们常常会把划线和向量想在一起,这对方法的解决与思考并不能起到关键性的效果。

关于UI项目

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/7 2:56:33

Scholar Inbox 订阅最新学术进展至邮箱

1. 注册账号可直接使用Google账号登陆用哪个邮箱注册&#xff0c;就会在哪个邮箱收到每天的最新论文精选2. 训练模型给模型提供启动数据——确认哪些文献与你的方向匹配、哪些文献不匹配&#xff0c;以便模型能够精准匹配你的研究方向scope。有两种方法&#xff1a;通过作者添加…

作者头像 李华
网站建设 2026/2/10 11:43:30

A.每日一题——3562. 折扣价交易股票的最大利润

题目链接&#xff1a;3562. 折扣价交易股票的最大利润&#xff08;困难&#xff09; 算法原理&#xff1a; 解法&#xff1a;01背包动态规划 297ms击败34.61% 时间复杂度O(N∗Budget) ①树形结构构建&#xff1a;将层级关系&#xff08;hierarchy&#xff09;转换为邻接表形式的…

作者头像 李华
网站建设 2026/2/9 23:03:44

圣默思 Teledyne DalsaFilr SWIR相机

Teledyne Dalsa&Filr SWIR相机 成像方案 什么是SWIR? 短波红外&#xff08;SWIR&#xff0c;一般定义为0.9 - 1.7μm波长范围内的光&#xff0c;但也可归入0.7 - 2.5μm波长范围&#xff09;成像使我们能够看到我们肉眼无法看到的物体。与物体本身发出的中波红外光…

作者头像 李华
网站建设 2026/2/4 20:01:20

Go 语言结构

Go 语言结构 概述 Go 语言,也称为 Golang,是由 Google 开发的一种静态强类型、编译型、并发型编程语言。自 2009 年发布以来,Go 语言以其简洁的语法、高效的并发处理能力和高性能而受到开发者的青睐。本文将深入探讨 Go 语言的各个结构特性,帮助读者更好地理解和应用 Go …

作者头像 李华
网站建设 2026/2/7 4:28:29

JavaScript for 循环详解

JavaScript for 循环详解 引言 在JavaScript编程中,循环是处理重复任务的重要工具。for循环是JavaScript中最常用的循环结构之一,它允许开发者重复执行一段代码,直到满足特定的条件。本文将详细介绍JavaScript中的for循环,包括其语法、使用场景以及注意事项。 for 循环的…

作者头像 李华