五、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();