Unity学习笔记(03):UnityC#模块化开发、基本数据类型、权限修饰符、const&readonly


五、Unity C#编程

游戏运行模式

  • 程序首先初始化
  • 然后进入一个while(true)循环 检查是否有消息(包括鼠标事件等)
    若有消息 则处理后 然后计算 绘制场景
    程序处在这么一个大循环中 不断检查是否有事件 若有则处理

帧频

在while循环中 游戏会有一秒循环的次数 比如CPU可以一秒绘制80次画面
人对于画面的流畅感若到了60 其实已经非常流畅了
帧频若达到60 则可以不用继续提升了 若继续提升 其实也感觉不出来 而且会更加消耗CPU
因此 在绘制的时候可以看时间是否到达 若还没到 则sleep
1/60=0.0166秒 但比如只有0.01秒就全部处理完了 那么可以休眠0.0066秒 休眠是为了节约CPU

因此 在while中 有:

  • 事件处理(包括各种事件)
  • 绘制场景
  • 检测是否需要休眠(维持帧频在60左右)

若CPU比较低端 那么绘制速度会变慢 此时while会不断地绘制 就不会循环了

FPS

FPS有两个概念

  • 1、帧频 (Frames Per Second)
  • 2、第一人称射击 (First Person Shoot)

?组件的代码入口

每个节点都有多个组件
因此 组件是经常面对的开发模式

  • 当组件被挂载到节点的时候 会调用组件的一个函数:Awake
  • 当节点在while循环里 刷新前 会调用Start
  • 当节点在while循环里 要处理的时候 会调用Update
    每个while循环要处理的时候都会调用每个组件的Update

模块化开发 & 代码模块

实际上 组件成了很多入口的模块
因此 其实是根据Unity逻辑来开发模块

给Unity写代码 实际上是给Unity写代码模块

开发

先在Project的scripts里右键 -> Create -> C# Script 以创建一个C#代码块
在这里插入图片描述
此时的代码是一个组件 组件只有挂载到节点上才会在Unity的while里循环被调用
在Hierarchy右键 -> Create Empty 创建一个根节点
点击Add Component添加组件
在这里插入图片描述
点击Scripts 然后选择脚本即可:
在这里插入图片描述
双击Project里的脚本图标 即可打开Visual Studio编辑器
在这里插入图片描述
自动生成的代码:

using UnityEngine;
using System.Collections;

public class game_scene : MonoBehaviour {

	// Use this for initialization
	void Start () {
	
	}
	
	// Update is called once per frame
	void Update () {
	
	}
}

改一下:

using UnityEngine;
using System.Collections;

// game_scene组件类继承于Unity提供的基类MonoBehaviour
public class game_scene : MonoBehaviour {

    // 组件实例加载的时候调用
    void Awake()
    {

    }

	// Use this for initialization
    // 组件实例在第一次Update之前调用
	void Start () {
	
	}
	
	// Update is called once per frame
    // 游戏每次刷新的时候调用
	void Update () {
	
	}

    // 物理引擎每次固定刷新的时候调用(与帧频无关)
    // 主要用于物理计算
    void FixedUpdate()
    {

    }
}

其中 MonoBehaviour就是Unity的代码模块/组件 是组件的基本规则

public class game_scene : MonoBehaviour

game_scene : MonoBehaviour代表game_scene继承/扩展自MonoBehaviour 所有组件都必须基层与MonoBehavior 必须遵守这个规则
在一个脚本里有且只能有一个类继承自MonoBehavior 且该类名必须与脚本文件名保持一致

Awake Start Update 和 FixedUpdate都是很重要的接口 他们是在Unity上开发组件代码的入口
(也类似于其它编程语言或框架的生命周期)

FixedUpdate是Unity提供的固定的机制
Update帧频是随时在变的 是浮动的 是实时的 只是维持在60上下
而FixedUpdate是以固定的频率来调用的 根据当前CPU的帧频给出一个固定的频率

在继承了MonoBehaviour之后 就具备了MonoBehaviour的所有特性
正因如此 当game_scene组件实例化之后加到节点上 Unity的while循环才能调用到诸如Awake Start Update 和 FixedUpdate的基本函数

还有个OnGUI接口
Unity提供了一种GUI(Graphic User Interface 界面)元素的绘制机制 这就是OnGUI
比如在游戏里要显示昵称等2D文字 Unity提供OnGUI 当绘制3D物体了 要将其变为2D成像 然后会调用OnGUI接口 此时 即可绘制GUI元素
并不是生成一个GUI节点 而是绘制(draw)出GUI元素
OnGUI在每次的刷新(Update)的时候都会被调用

// 绘制2D元素的入口的时候调用 例如玩家的昵称和血量条
voidOnGUI()
{

}

一个组件可以挂载多个脚本

使用Debug.Log()打印Debug日志输出语句

组件实例化

定义了类只是一个描述 而并不是一个实例 class只是组件的类型
要将类创建为实例才行

挂载的并不是类的本身 而是该类的类型的实例 因此 挂载多个并不会冲突

在添加组件的时候 创建了该组件类的对象实例
然后在gameobject对象中保存了该组件的实例

?Unity C#基本数据类型

由于是Unity C# 所以和C#其实还是有一些细微差别的

程序包含数据代码 数据是在运行过程中产生的
这些都是存放在内存中的
内存存储的最小单位是字节
1字节=8比特(bit)

  • 整数 / 1字节
    • sbyte 带符号的整数 / 1字节 (需要多拿出1bit来表示符号位)
    • byte 不带符号的整数 / 1字节
    • short 带符号的整数 / 2字节
    • ushort 不带符号的整数 / 2字节
    • int 带符号的整数 / 4字节
    • uint 不带符号的整数 / 4字节
    • long 带符号的整数 / 8字节
    • ulong 不带符号的整数 / 8字节
  • 浮点数
    • float / 4字节
    • double / 8字节
  • 逻辑
    • true
    • false
  • 字符 / 2字节(16位Unicode字符)
  • 复杂类型的引用变量(用于表示一个变量 指向另一个复杂的对象 保存着对该变量的引用)
    • 若在64位的.net那就是64bit/8字节
    • 若在32位的.net那就是32bit/4字节
    • String字符串也是一个复杂类型
    • 类可能有多个成员 因此叫做复杂对象

Unity C#权限修饰符

  • public 类型成员的修饰符
  • private 类型成员的修饰符(默认)
  • protected 类型成员的修饰符
  • internal 类型成员的修饰符(默认)

用public修饰的类可以在外部使用 用internal修饰的类只能在类的内部使用
类型成员分为两个:一个是数据成员(不属于类 只属于类的实例) 另一个是类的方法成员
类的实例会拥有数据成员 而每个实例的数据成员不一定相同 但是方法成员必定都是相同的 因为都是由这个类所产生的
比如 都是人 都会走路跑跳 但是每个人的头发或者身体都是其自己的

// 定义一个类
publicclass Person
{
    // 数据成员的定义
    private string name;
    private int age;
    private int gender;

    // 成员函数的定义
    public string eat(int age)
    {
        return "asd";
    }
}

类的实例化

类也需要实例化
类的实例就是类的数据成员所需要的内存体

C#内存模型

C#的内存模型里面共有四块区域
分别是:

  • 代码段 用于存放函数指令常量字符串 所有实例共用
  • 还有个数据段 用于存放全局变量(static静态变量)
  • 还有个 用于存放new出来的复杂对象 当没有任何一个引用变量指向该new出的内存时即被回收
  • 还有个 用于存放局部变量 函数返回变量即被回收

堆和栈会在一定情况下被回收 代码段和数据段是常驻内存不回收的
堆和栈中的是程序运行才会产生的 代码段和数据段是程序一加载首先加载的

out关键字

out修饰的参数 在函数内可直接修改该变量的值
out可以理解为将修改后的值带出来 有些类似于引用和指针的概念

由于是在函数里另外new了一个对象 因此地址是不同的 带出来的是在函数内new出来的对象
但若不加out的话 即使在函数内将传入的对象改了 那么在外面的也不会受影响
这就是加不加out的区别

Person p=new Person();
p.age=10;
create_person(out p);
Debug.Log(p.age); // 12

void create_person(out Person p)
{
	p=new Person();
	p.age=12;
}

继承

在C#中 使用:来继承
比如 Son继承Parent

public class Son : Parent
{

}

在继承的时候 若基类/父类为internal 那么子类也必须为internal
若基类/父类为public 那么子类可以为public 也可以为internal

调用顺序

this.xxx()调用的是自己的函数
继承后 子类成员函数调用时的查找方式是先从当前类中找
若找到则调用自己的 若未找到 则往基类找 直到找到为止

base关键字

若当前类和基类有同名函数 那么base.xxx()调用的是基类的函数
base只能在类的内部使用

虚函数

为同时管理多个成员 提出了虚函数的概念

  • 基类的引用变量保存子类的实例
  • 为方便管理 需在基类定义几个接口以统一管理
    基类定义几个函数接口 子类自己重载 然后有不同的实现

子类继承了基类 若调用方法 那么会调用子类的该方法
此时 若想要调用父类的该同名方法 那么可以用虚函数(virtual关键字)

public virtual void sayHello()
{
	Debug.Log("Hello");
}

virtual表示该函数为一个虚函数 若遇到该函数为虚函数 那么会去基于该实例查找基类是否为virtual
若为virtual 则会查找子类是否重载该虚函数
此时 还需要在子类的函数上用override关键字显式地定义重写 代表重写了基类的同名虚函数

public override void sayHello()
{
	Debug.Log("Hello World");
}

若有 则调用子类的函数
——“我不知道外界会传入什么样的实例 但我依然能够分情况调用不同的函数 执行各自的逻辑处理”

?const & readonly

const常量

const常量全局唯一 只有一个
const修饰的是类的成员变量
它是在编译的时候就确定的常量

readonly只读

每个实例都会有一个readonly只读变量
它是在实例化的时候确定的常量
readonly的变量只有一次修改的机会 在对象构造的时候 在构造函数里修改

?名称空间

在代码中有可能会使用同样的名字
若名字出现重复 则会产生冲突

因此 要使用名称空间 名称空间带有自己的烙印
代码全都写到该名称空间中 这样 及时代码中出现了同样的名称 但名称空间不同 因此不会出现冲突
namespace关键字来定义名称空间

namespace my_namespace
{
    class Person
    {
        int gender;
    }
}

my_namespace.Person p = new my_namespace.Person();

简写 / 省略名称空间

每次前面都要加上名称空间 过于麻烦
此时 可以using 名称空间 往搜索范围中添加该名称空间 然后使用时即可省略名称空间了

流程:首先去当前名称空间查找 若找不到 去using的名称空间里查找 直至找着为止
在using的全部名称空间里都找不到 则会报错

// 往搜索范围中添加该名称空间
using my_namespace;

namespace my_namespace
{
    class Person
    {
        int gender;
    }
}

Person p = new Person();