关于编程基础急急忙慌的应用速成
伊底_1D
2022年06月27日 23:26
收录于文集
共16篇

业界传统

        本文算是为之后打算要搞的Shader和U3D Editor开发备忘录系列做一个铺垫,毕竟程序的概念一点都没有的话,直接开冲这两个东西,尤其是Editor,可能体验还是不会太好- -

        以下将会基于本人的实际应用与开发经历来系统性的讲述个人认为常用的编程概念及方法。本人主用U3D C#和shaderLab进行游戏方面的开发,所以下面也主要基于C#​的语法风格以及游戏方面的思路来讲。但程序语法的基本逻辑都一样,所以哪怕用别的语言也问题不大- -就是写法上会有点区别。

        因为本文意在让非科班出身的兴趣爱好者也能快速上手玩点代码,再加上本人水平有限,所以讲述的内容相较于正统课程会有大幅删减,且难免会缺乏深度、细节及性能上的考量。不过若是想要更进一步的讨论,或者有什么建议与补充,也欢迎在评论区理性讨论。

前言

        程序很大程度上就是对各种数字的处理。游戏那些看上去天花乱坠的效果本质上都是各种数字的变化,比如技能特效就是特效片整体的位移旋转缩放变化,特效片顶点的位置变化再加上贴图的像素位置、色值变化等;角色动作就是骨骼位置的变化按权重带动模型顶点的位置变化,并在关键帧之间进行坐标的插值。

        上面这些东西有些说法看不懂没关系- -总之先明确,程序就是对各种数字的运算就完事。

        那么废话不多说正式开始- -

一、数据类型

常用类型

        int——整型

        其实就是整数。

 

        float——浮点型

        其实就是小数。

 

        char——字符

        你键盘能打出来的字都是一个个字符,有些能打出来但看不到的比如回车也是字符。有个叫ASCII码的东西,差不多是一些单字符和数字之间的对应关系,并且可以互相转换,有时候有用。有兴趣可以自行度娘。

 

        bool——布尔型

        有true和false两个值,一般用作某些功能的开关或是标识记录。

 

        其他的诸如byte,uint,double,string,half还有后面的结构体和类等等,实际上就是精度,表示范围与上面不同或者是上面的数组形式(后面讲)或复合形式。单个数据情况下,一般上面这些就够用- -

变量声明

        在使用数据之前要“声明”。一般的声明格式就是【数据类型名】+【变量名】,经常还会在声明时顺手给数据一个初始值(也可以不给),像是下面这样。

代码块
text/x-csharp
自动换行
复制代码
int _Integer = 1;
float _Float = 3.14f;//C#中,float需要在结尾加f来转换类型
char _Char = '字';//单引号包夹表示为单字符,后面的字符串则是用双引号包夹
bool _Bool = true;
复制成功

        关于变量名的命名规范,一般是小写英文开头的一个能代表这个变量含义的单词,或者下划线开头(英文模式下shift+减号键),且不能与现有的int,float之类的数据类型名或类名重名(具体哪些字符能用那些不能请度娘)。

类型转换

        不同于数学计算,程序中不同类型的数字有一些对应的特性。比如对于两个int型数据,a =3,b=2,则a/b与b/a的实际输出结果为1和0,而不是数学中的1.5和0.66···。因为在程序中,同类型的变量做计算,其结果的数据类型也相同(在上面的例子中即结果为整型。结果会自动舍弃小数部分,而不是四舍五入)。

        当参与计算的数据类型不同,其转换规则是以其中精度最高的为优先。例如上面的例子中a/b结果为1,若是写成a/2.0f则会以float的精度将a自动转为float计算,且结果也为float,即1.5。

        上面的转换是自动的,或者说隐式转换,也可以手动进行强制转换。

代码块
text/x-csharp
自动换行
复制代码
//数值转换
int a = (int)3.14;//变为3
a = int.Parse("123");//变为123

//类型转换
Texture2D tex = new Object() as Texture2D;
tex = (Texture2D)new Object();
复制成功

        int.Parse是将输入字符串(后面讲)转换为整型的方法。但因为字符串可能包含非数字字符,所以这种写法可能会报错。(可以用int.TryParse方法代替)

        下面两行代码是将U3D中的Object类型转换为Texture2D类型的两种写法,一般用于父子类之间的转换。(涉及到面向对象的概念,本文不讲,但类的概念会在最后简单描述)

二、运算符

        编程中,除字符内容外务必切换到英文模式下输入。(虽说也支持中文变量名- -···但因为一些编码方式的因素可能会出问题,个人也就是在U3D工具开发的时候用枚举类型(之后讲)时偶尔会这么干)

代码格式

        ;——单句程序段后都需要以分号结尾。

        //——单行注释。本行代码中在//后的部分不参与程序计算。

        /*内容*/——选段注释。夹在中间的内容不参与程序计算。

        {代码段}——代表一个代码块。代码块中声明的变量只能在该代码块中使用。

计算

        +(加)-(减)*(乘)/(除)%(求余数)=(把等号右侧的值赋值给左侧)

        a++(等同于a = a + 1。减法同样适用)

        a += t(等同于a = a + t。减、乘、除、取余同样适用)

比较

        <(小于)>(大于)<=(小于等于)>=(大于等于)==(两边相等)!=(两边不等)

        比较运算整体等同于一个布尔值,例如 1 < 2 等同于true, 3 == 2 等同于false

逻辑运算

        &&(且)||(或)

        逻辑运算整体同样返回一个布尔值,例如(1 < 2) && (3 == 2)等同于false,(1 < 2) || (3 == 2)等同于true

三元运算

        条件 ? 方法1 : 方法2

        含义为,如果满足条件,执行方法1,否则执行方法2(可以用后面的if else代替,一般在条件和方法的代码结构都比较简单的时候偷懒装X用- -)

        例如以下两数取最小的代码,最终c的值是1

代码块
text/x-csharp
自动换行
复制代码
int a = 1, b = 2;
int c = a < b ? a : b;
复制成功

三、逻辑分支

单分支if语句

        即如果满足条件,则执行方法1,否则执行方法2。

代码块
text/x-csharp
自动换行
复制代码
if (条件)
{
  //方法1
}
else//不需要执行时可以不写
{
  //方法2
}
复制成功

多分支if语句

代码块
text/x-csharp
自动换行
复制代码
if (条件1)
{
  //方法1
}
else if (条件2)
{
  //方法2
}
···
else if (条件N-1)
{
  //方法N-1
}
else
{
	//方法N
}
复制成功

 switch语句

代码块
text/x-csharp
自动换行
复制代码
switch (输入值)
{
  case 比较值1://注意这里是冒号不是分号
    //方法1
    break;
  case 比较值2:
    //方法2
    break;
  ···
  case 比较值N:
    //方法N
    break;
}
复制成功

        对于if语句,输入条件是一个返回值为bool类型的代码段或方法(后面讲),switch语句则多是数字等有3个以上不同值的类型或方法,经常配合枚举类型(同样后面讲)使用。

四、多数据类型

        以数组为代表的多数据类型(我自己起的名字不要在外面瞎用- -)用于存储大量数据(数组元素可以是数,字符,类,甚至是数组本身),例如图像就可以看做一个存有色值信息的二维大数组。

        从数组开始的“复杂数据类型”(在C#中对应引用类型,它们在堆中开辟内存,本篇文章不多讲原理)的声明大多需要使用new关键字来初始化,其作用是为该变量开辟内存空间。

静态数组

        以下代码的意思便是为数组变量a开辟一个长度为n个int类型大小的内存空间。

代码块
text/x-csharp
自动换行
复制代码
int[] a = new int[n];
复制成功

        其中的n个元素默认都是0。若想指定每一个默认值可以这么写。

代码块
text/x-csharp
自动换行
复制代码
int[] a = new int[3] { 1, 2, 3 };
复制成功

        数组通过变量名[变量索引]的形式访问其中的某个元素。例如上面的结果便是

a[0] = 1,a[1] = 2, a[2] = 3。需要注意的是,数组的起始索引是0不是1,同理,最大索引是数组长度-1。其中的数组长度可以通过a.Length的方式获得。(这种变量名加个点的方式也是在后面讲类和结构体时常用的一种调用变量和方法的方式)

        需要注意的是,这种形式的数组在声明后便不可再更改长度,而只能修改其中的值。若是想换个长度只能new一个新的数组出来。不过也因为这种不便,在实际开发中更常用的“多数据类型”是下面的List<T>。

动态“数组”

代码块
text/x-csharp
自动换行
复制代码
int[] a = new int[3] { 1, 2, 3 };
List<int> b = new List<int>(a);
复制成功

        List称为泛型集合,可以理解成一个可以动态改变长度的数组,使用需在代码开头加上using System.Collections.Generic;(C#上面那些using是引用命名空间的意思。内置方法是写在不同命名空间里的,你想用就得引用对应的命名空间,其实就是个类似于地址门牌号的东西。电脑很傻,不写就找不到方法在哪,会报错- -不过VS也会自动提示你加上)。

        可以通过上面的方式将一个数组的值一一对应传递给List,也可以在括号里填数来设定List的初始长度。若括号里不填,则意味着创建一个初始长度为0的List。

        List访问元素的方式与数组相同,但访问其长度的方式为.Count。(.Count是当前包含的元素个数,还有个.Capacity是当前List的上限空间。从原理上讲,每当Count超过Capacity,List就会new一个新的出来,并且Capacity翻倍。所以在明确知道数组长度且不怎么变的情况下,建议使用常规数组以节省空间。更多原理有兴趣自行度娘)

        List通过.Add(新值)和.Remove(旧值)方法增加新值或删除已有值。这个过程中,List的长度和元素索引都会发生改变。除这两种方法,List还有一些如Insert之类的实用的内置方法,有需要请自行度娘- -

字典类型

代码块
text/x-csharp
自动换行
复制代码
Dictionary<char, float> a = new Dictionary<char, float>();
复制成功

        与List同命名空间,且内置方法都差不多。数据类型的格式为Dictionary<键, 值>,效果就是按键查值(a[键] = 值)。相对的,数组与List都是按索引查值(a[索引] = 值)。比如先使用a.Add('测', 2.71)方法往a里面塞一个2.71,那访问这个值的方式便是a['测'],即括号里输入的是键值的数据类型变量,而不一定是数字。

多维数组

        C#的静态多维数组有两种,以二维为例。

代码块
text/x-csharp
自动换行
复制代码
int[,] a = new int[2, 3] { { 1, 2, 3 }, { 4, 5, 6 } };
int[][] b = new int[2][];
b[0] = new int[2] { 1, 2 };
b[1] = new int[4] { 4, 5, 6, 7 };
复制成功

        第一种是声明了一个2行3列的数组,第二种则是声明了一个2行,但第一行2列,第二行4列的不等长数组。

        而对于动态则可以使用List或者Dictionary嵌套等方式实现。

代码块
text/x-csharp
自动换行
复制代码
List<List<int>> a = new List<List<int>>();
Dictionary<string, List<float>> b = new Dictionary<string, List<float>>();
复制成功

字符串

代码块
text/x-csharp
自动换行
复制代码
string str = "这里是测试用的字符串";
复制成功

        声明用双引号,实际上就是char型数组,但有很多好用的增删改查方面的内置方法以及一些自己的特性。

        例如字符串缓冲池的概念,以及每次更改内容都会在背后new一个新的字符串。所以如果处理不当(比如在百万次的循环中反复执行str += “延长”这样的操作)可能会让内存爆炸。一般这种操作推荐使用System.Text命名空间下的StringBuilder类型来实现可变字符串,以上各种有兴趣自行百度。

五、循环

        循环体中的代码段(即夹在下面示例中最外层大括号中的部分)会反复执行,直到不满足循环条件或遇到break时结束。

while循环

代码块
text/x-csharp
自动换行
复制代码
while (每次进入循环时的判断条件)
{
  if (跳出循环的条件)
  {
    //执行的方法
    break;//跳出循环,无视最外层的循环条件
  }
  else
  {
    //执行的方法
    continue;//继续循环,可省略
  }
}
复制成功

        利用break可以灵活跳出诸如while(true)等死循环的写法。if else只是为了演示break和continue,不一定写。

for循环

        最常用的循环方法,语句格式由for(循环变量; 继续循环的条件;每次循环后的操作)构成。

代码块
text/x-csharp
自动换行
复制代码
int[] a = new int[10] { -1, 2, 3, 6, 7, 99, 8, 190, -50, 10 };
int min = a[0];
for (int i = 0; i < a.Length; i++)
{
  if (a[i] < min)
  {
    min = a[i];
  }
}
复制成功

        以上代码的功能为在数组a中查找最小值,输出结果min = -50。

foreach循环

        灵活度最差的一种循环。多用于从头到尾查找遍历某些数据时使用,而不适合对数据进行修改。语句格式为foreach(数据类型  数组元素的代指名称  in关键字  要遍历的数组)。

代码块
text/x-csharp
自动换行
复制代码
int[] a = new int[10] { -1, 2, 3, 6, 7, 99, 8, 190, -50, 10 };
int min = a[0];
foreach (var item in a)//item代指当前遍历到的a中的某个元素
{
  if (item < a[0])
  {
    min = item;
  }
}
复制成功

        以上代码与for循环功能相同。其中,var为推断类型,即根据变量的实际类型自动判断属于哪种类型并进行操作,但显然存在性能问题,明确知道数据类型的情况下还是写具体的类型。

六、方法(函数)

        方法(以前也叫函数)是一段可以反复调用的代码块,声明由【方法返回类型】+【方法名】+【输入参数的类型和名称列表】构成,命名原则也是看了名字大概就知道干什么用的,但是大写开头。

        方法在形式上类似于数学中的函数(即y = f(x)。可能也是因为这个在一开始才叫函数?)。其意义在于方便反复使用同一段代码时的程序编写和修改,即但凡用了这个方法的地方,只需要写一行方法名与输入参数,而不需十几甚至几十行的复制粘贴,并且只要修改方法本身的代码,那么所有用到它的地方便都会同步修改,利于维护。

        讲到现在可以玩点稍微综合一些的例子了。下面的示例将“循环”一节中查找数组最小值的代码封装为方法并调用。最终达成的效果是,在10个长短不一的数组中分别查找最小值,并将这些最小值载入新数组,最后在新数组中再次查找最小值。

代码块
text/x-csharp
自动换行
复制代码
void Start()
{
  //数组初始化
  int[][] arrays = new int[10][];
  for (int i = 0; i < arrays.Length; i++)
  {
    arrays[i] = new int[Random.Range(1, 11)];//随机初始化数组长度,范围1~10
    for (int j = 0; j < arrays[i].Length; j++)
    {
      arrays[i][j] = Random.Range(-100, 101);//随机初始化数组元素值,范围-100~100
    }
  }
  //查找各数组中的最小值
  int[] mins = new int[arrays.Length];
  for (int i = 0; i < mins.Length; i++)
  {
    mins[i] = FindMin(arrays[i]);
  }

  //打印结果
  for (int i = 0; i < arrays.Length; i++)
  {
    string tempStr = "";
    foreach (int item in arrays[i])
    {
      tempStr += "    " + item;
    }
    print(tempStr + "——" + mins[i]);
  }

  //所有数字的最小值
  print(FindMin(mins));
}

int FindMin(int[] arrayIN)
{
  int min = arrayIN[0];
  for (int i = 0; i < arrayIN.Length; i++)
  {
    if (arrayIN[i] < min)
    {
      min = arrayIN[i];
    }
  }

  return min;//用return关键字输出返回值
}

void FindMin(out int min, int[] arrayIN)
{
  min = FindMin(arrayIN);
}
复制成功

输出结果

        针对查找最小值的方法FindMin,这里利用方法重载(重载是面向对象的概念,简单来说就是方法名一样,参数列表不一样的写法)写了个空返回值(也就是void修饰的方法,只计算而没有返回值)的同名方法,但效果是等价的。

        需要稍微说一嘴的是这个方法的参数列表中的out关键字。带out或ref修饰参数意为输出/输入参数,其意义在于,常规情况下对int这种值类型(C#中的值类型与引用类型的概念,除了结构体和枚举等,一般单数据类型都是值类型,而数组,类等则是引用类型。这里同样不细讲)进行方法传参时,其值在方法中的修改实际上改的是它的一份拷贝,而不会改其自身。但若是在前面加上ref或out则是以其自身来传参,值就会修改,所以这个重载方法才是有效的。

        对于ref和out,在调用方法时,参数前同样需要加上ref和out。二者的区别在于,ref需要在传参前有赋值,out不强制要求;out需要在方法体内有赋值,ref不强制要求。

七、复合数据类型

        复合数据类型这名字同样是我瞎起的,慎用- -···

        复合数据类型有点像是你把现有的一些数据类型按需要挑选组合一下而形成的一个自己造的新数据类型,其主要意义还是已经说烂了的增强代码可读性,维护性什么的。不过当你开发的东西规模越来越大的时候,可能就会发现这种分块处理的思想(即面向对象程序设计)确实会变得越发重要。

枚举

        实际上枚举就是把几个特定的int类型撮合到一起形成的“预设集合”,它可以把原本的数字包装成更容易被人理解含义的名称,并且限制使用者仅在有限的选项中进行选择。下面就是一个颜色的枚举类。

代码块
JavaScript
自动换行
复制代码
enum ColorEnum
{
  Red = 0,
  Green = 1,
  Blue = 2,
  Black = 3
}

ColorEnum color = ColorEnum.Red;
复制成功

        因为枚举类是你自己造的一个数据类型,所以要用他的话显然就需要像上面一样,造完了再声明出一个变量来,给初值的方式也是复合类型中常用的“点”。在此基础上,就可以像下面一样结合switch语句限制选择结果。

代码块
text/x-csharp
自动换行
复制代码
switch (color)
{
  case ColorEnum.Red:
    //方法1
    break;
  case ColorEnum.Green:
    //方法2
    break;
  case ColorEnum.Blue:
    //方法3
    break;
  case ColorEnum.Black:
    //方法4
    break;
}
复制成功

        以上枚举只支持单选,而枚举类型还能配合一些之前没讲的位运算符以及枚举值的设计来实现多选。因为本人目前也没怎么用过,所以在这里也不多说。有兴趣的可以自行度娘。

类

        类是复合类型中功能性最强的一种,同时也是面向对象程序设计的核心概念。在一个类中可以有变量,也可以有方法。粗浅的来说,一个类里包含的变量与方法应当是这个类所代表的事物所具有的一系列“特征”。例如设计一个猫的类,其中便应当包含猫的名字,品种,毛色等属性变量,同时包含“喵喵叫”,“抓老鼠”,“身体化为液态”等方法。(当然实际情况都不一定- -···具体问题具体分析)。

        在U3D中,每一个脚本都是单独的一个类(创建完一个新脚本打开你就会发现,前面用class关键字修饰)。

        下面是一个可能科班出身的同学几乎都写过类似代码的长方体类。

代码块
text/x-csharp
自动换行
复制代码
class Rectangle
{
  //成员变量
  private float length;//长
  private float width;//宽
  private float height;//高

  //构造函数,用于在外部初始化类的成员变量。与一般方法不同,不能有包括void在内的返回类型。若不写,系统会默认给一个没有内部操作的无参构造函数
  public Rectangle(float length, float width, float height)
  {
    this.length = length;//this关键字代表“自己”,用于和输入参数区分
    this.width = width;
    this.height = height;
  }

  //类方法
  //计算体积
  public float GetVolume()
  {
    return length * width * height;
  }

  //计算表面积
  public float GetSurface()
  {
    return 2 * (length * width + length * height + width * height);
  }
}

Rectangle rect = new Rectangle(1, 2, 3);//类的实例化
复制成功

        简单说一下访问权限修饰符public,private 和 protected(其实还有两种,但我没用过所以就不说了- -)。public就是在这个项目里哪哪都能调用,public修饰的变量或方法是可以被实例化的类“点”出来的;private则是仅在类自己内部可以调用,如果不写访问修饰符,在C#里默认是private;protected用的相对比较少,它是自己和自己的子类可以用,以后有机会说类继承什么再说吧- -

结构体

        个人几乎没怎么用过。表面上来看和类的使用都差不多,就是把class关键字改成struct,唯独知道的就是结构体是值类型,在栈内开辟空间;而类是引用类型,在堆内开辟空间。从这点上来看,可能比较小体量的“类”适合用结构体来代替,从而提高性能(栈区小,寻址快,处理也就快)。关于结构体我这里也就简单说这么些- -看个乐呵-_-