Unity 3D

开发游戏

游戏引擎与API

常用游戏引擎有:Unity,UE4,Cocos,laya,白鹭等。
常用开发API有:DirectX,OpenGL,Vulkan等。

人员分工

程序:

  • 服务端
  • 客户端
    • UI 实现
    • 业务逻辑
    • 动画实现
    • 计费SDK
  • 工具开发
  • 运营数据系统

美工:

  • 原画
  • 3D建筑
  • 3D角色
  • 动画
  • 特效
  • 地形编辑
  • UI 界面
  • 灯光
  • 场景
  • TA:即懂美术也懂程序,写Shader的

策划:

  • 系统策划
  • 剧情策划
  • 数值策划
  • 关卡策划
  • 任务策划
  • 执行策划:盯着程序员的

Unity 操作

界面介绍

项目:由场景构成
场景 Sence:由游戏对象构成(看见的对象,光源,摄像机等)
游戏对象:由组件构成
组件:拥有多个属性

菜单栏:

  • File:场景操作和项目操作。
  • Edit:复制粘贴等
    • Duplicate:复制,包括Copy与Paste
    • Prefrence:首选项
  • Assets:资源
    • Create:创建
    • Import:导入资源包
  • GameObject:游戏对象
    • Create
    • 2D Object
    • 3D Object
    • Effects
    • Light
    • Audio
    • Video
    • UI
    • Camera
  • Component:
    • Mesh
    • Effects
    • Physics
    • Physics 2D
    • Navigation
    • Audio
    • Video
    • Rendering
    • Layout
    • Playables
    • AR
    • Miscellaneous
    • Analytics
    • UI
    • Scripts
    • Events
  • Window:面板控制
  • Help
    • Scripting Reference:手册

Layout:布局方式
Project:项目面板,管理所有文件
Scene:场景面板,列出场景中的所有对象
Game:游戏面板,玩家看到的面板,由摄像机拍摄
Hierarchy:层级,管理场景中的所有游戏对象
Inspector:显示游戏对象上的组件及组件属性
Layers:

  • 层,用于管理文件组。
  • 系统保留8个层,共有32个层。

坐标系

Untiy 使用左手系。

ISO:正交视图
Persp:透视视图

场景操作

鼠标操作:

  • 旋转:鼠标右键 / Alt + 左键
  • 缩放:Alt + 鼠标右键 / 滚轮
  • 平移:鼠标中键 / 上方手
  • 锁:禁止旋转
  • 选中多个:Shift + 点选

上方工具栏(快捷键):

  • 平移场景 Q
  • 移动物体 W
  • 旋转物体 E
  • 缩放物体 R
  • 2D缩放物体 T
  • Center / Pivot:坐标轴位置 Z
  • Global / Local:切换坐标系 X

快捷键:

  • V:顶点捕捉
  • Ctrl + D:复制
  • Ctrl:角度捕捉

Unity 资源

项目目录

Assets:管理资源文件

  • Animations
  • Animators
  • Audio
  • Done
  • Fonts
  • Gizmos
  • Materials
  • Models
  • Prefabs
  • Scenes
  • Scripts
  • Shaders
  • Textures
    特殊
  • Standard Assets:内置优先加载
  • Editor:扩展编辑器
  • Plugins:插件Dll
  • Resources:资源文件

材质 Material

可以配置颜色,图片。
可以添加着色器。

着色器

Diffuse:Base贴图。
Bumped Diffuse:Base贴图和法线贴图。
Bumped Specular:再加入高光材质。

纹理贴图

尺寸需要被2整除(256*256)。

可以创建法线贴图。
Texture Type:贴图类型
MaxSize:纹理取样尺寸。

Substance Designer:一款材质制作工具。

GUI 纹理显示:

1
2
3
4
function OnGUI(){}  // GUI 启动
Resources.Load() // 加载Resources文件夹
Resources.LoadAll()
GUI.DrawTexutre()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private var oneTexture:Texture2D;
private var allTexutre:Object[];
function OnGUI(){
if(GUI.Button(Rect(0,20100,60), "loadOne")){
if(oneTexutre == null){
oneTexture = Resources.Load("one/Grass");
}
}
if(GUI.Button(Rect(0,20100,60), "loadAll")){
if(allTexutre == null){
allTexutre = Resources.Load("all");
}
}
if(allTexutre!=null){
for(var i=0;i<allTexture.Length;i++){
GUI.DrawTexture(Rect(110+i*120,140,120,120), allTexutre[i],ScaleMode.StretchToFill,true,0);
}
}
}

Moive纹理,是一个动画纹理。导入后转为OGG格式。

模型 FBS

模型一般有FBS,OBJ格式,一般使用FBS。
如果导出的是3DMAX等文件,可以用Blend,Untiy转换为FBX。

编辑时尽量让z轴朝向前方,以便以后程序控制。

对于动画,可以导出一个动画,再在Untiy中分割成多个动画;也可以导出为多个文件。但是多文件时命名要遵循模型@动画命名规则。

Model:

  • Meshes:缩放因子 0.01,MeshCompression 网格压缩,ReadWriteEnable 运行时修改,OptimizeMesh 优化,GenerateColliders 碰撞(对固定物体较好),SwapUV,GenerateLightMapUV
  • Normals & Tangents:法线切线(输入,计算),SplitTangents 分割法线
  • Materials:导入材质,材质命名,材质搜索
    Rig:动画类型,人/物。
    Animations:动画如何分割。

模型:

  • 组成部分
  • 网格
  • 动画

控制模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Move: MonoBehaviour{
void Update(){
float horizontal = Input.GetAxis("Horizontal");
float vertical = Input.GetAxis("Vertical");
Vector3 direction = new Vector3(horizontal, 0, vertical);
if(direction!=Vector3.zero){
transform.GetComponent<Animation>().CrossFade("walk");
transform.rotation = Quaternion.LookRotation(direction);
transform.Translate(Vector3.forward * 1 * Time.deltaTime);
}else{
transform.GetComponent<Animation>().CrossFade("idle");
}
}

void OnCollisionEnter(Collision collision){
if(collision.gameObject.name == "Plane"){
return;
}
transform.GetComponient<Animation>().CrossFade("bite");
collision.transofrm.GetComponent<Animation>("die");
Destroy(collision.gameObject, 5f);
}
}

声音

是否为3D声音:有距离感。
载入方式。

音源组件:发出声音的物体。
声音监听组件:听到声音的物体。

预设体 Prefabs

用来批量管理游戏对象。
也可以批量添加预设物体。

可以将游戏对以文件形式存储起来:从层级面板拖入项目目录即可。

Select:快速找到该预设体。
Revert:还原为预设体中的设置。
Apply:将修改后的设置放入预设体。

游戏对象

层级关系:
子物体的Transform是相对于父物体的

基本对象:
Cube
Sphere
Plane:由多个三角形构成
Quad:由2个三角形构成
Terrain:地形

Tag:属于某一个类。
Layer:属于某一个层。

搜索对象:在层级面板双击 / 单击,在场景中按 F

对象组件:
Transform:位置 / 旋转 / 缩放
Mesh Filter:决定游戏对象的形状
Mesh Rendeer:决定游戏对象的外观展示

  • Material:材质

刚体碰撞组件

Rigidbody:刚体组件,模拟物理效果

  • Mass:质量
  • Drag:拉力,阻力
  • Angular Drag:旋转拉力
  • Use Gravity:重力
  • Is Kinematic:开启运动学,不受力
  • Interpolate:插值
  • Collision Detection:碰撞检测
  • Constraints:约束
    Collider:碰撞体组件
  • 用于碰撞检测
  • 可以编辑碰撞体
  • is trigger
  • material:物理材质
    • dynamic friction:动态摩擦力
    • static friction:静态摩擦力
    • bounciness:弹力
    • friction combine:组合摩擦力
    • bounce combine:组合弹力
  • center
  • size

地形

一种游戏对象。

Terrain:地形

  • Terrain:地形修改器
    • Brush:
      • Brush Size:大小
      • Opacity:硬度
    • 地形
      • 抬高
      • 恒高
    • 地形纹理
      • 可添加多个纹理,作为笔刷
    • 种树种草
    • 放水

灯光

灯光可以是游戏对象,也可以是组件。

灯光类型:

  • Directional Light:影响所有物体
  • Point
  • Spot
  • Area

项目设置->Player,可以在不同平台上设置渲染路径。
Vertex List
Forward
Deferred Lighting

灯光类型:点,聚光灯Spot,方向灯,区域灯光。
阴影类型。
灯光耀斑:Draw Halo
渲染重要性:Render Mode

摄像机

ClearFlag:天空盒,实体色,深度信息(多摄像机叠加)等。
投影方式。
前后截面。
视口尺寸。
深度:深度大的会叠加到深度小的摄像机上。

脚本

GameObject包含的组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
tranform // 位移
rigibody // 刚体
camera
light
render
animation
audio
其他
// 或写为
this.gameObject.transform
// 其他组件
other.GetComponent("filename")
other.GetComponent(filename)
other.GetComponent<filename>()
// 搜索
GameObject.FindGameObjectWithTag("tag")
// 消息传递
gameObject.SendMessageUpwards("ApplyDamage", 5.0);

Time对象,记录各种间隔时间。

协程

1
2
3
4
5
6
7
8
9
10
void Start(){
// 启动一个协程,异步调用
StartCoroutine(Routine());
}
IEnumerator Routine(){
// 等待协程操作
yield return new WaitForSeconds(1f);
// 执行剩余任务
}

GUI

所有GUI相关都在OnGUI中实现。
常用控件:

  • Label
  • Button:点击后,返回True
  • RepeatButton:按下后持续执行。
  • TextField:返回string
  • Toggle:开关
  • HorizontalSlider:滑块
    复合控件:
  • Toolbar
  • SelectionGrid

GUI.changed:GUI更新
GUI.skin:GUI风格

定义组:

  • GUI.BeginGroup()
  • GUI.EndGroup()

GUILayout.Button
GUILayout.Box

HUD

角色屏幕上固定的界面。

Add Component,GUITexture,然后调整位置大小。

特效

使用粒子系统添加特效。
OpenEditor:打开独立编辑器。
+->Show All Modules:显示所有特效
基本参数:

  • Duration:粒子持续时间
  • Loging:循环
  • Prewarm:预热,一般不需要
  • Start XXX:启动设置,可以设置曲线(随机数范围)
  • Gravity Multiplier:重力
  • Inherit Velocity:继承速度,没用
  • Simulation Space:本地坐标,世界坐标,粒子是否随着发射器移动
  • Play On Awake:是否初始激活
  • Max Particles:最大粒子数
    Emission:
  • Rate:发射速率 / 距离
  • Bursts:爆炸
    Shape:
  • Shape:形状
  • Angle:角度
  • Radius:半径
  • Length:长度
  • EmitFromShell:发射面
  • Random Direction:随机方向
    XXX Over LifeTime:速度,限制速度,力,颜色,大小,旋转
  • XYZ:速度方向
  • Space:坐标
  • Dampen:阻尼
  • Separate Axis:锁定轴
    XXX By Speed:颜色,大小,旋转
    External Forces:力学,不用
    Collision:碰撞,消耗资源
  • Planes:碰撞面板
  • Scale Plane
  • Dampen:阻尼
  • Bounce:摩擦
  • Lifetime Less:减少的寿命
  • Min Kill Speed
  • Particle Radius:碰撞位置偏移
  • Send Collision Message:发送碰撞信息
    Sub Emitters:下一个粒子系统
  • Birth
  • Collision
  • Death
    Texture Sheet Animation:贴图UV动画
  • Tiles
  • Animation
  • Frame Over TIme
  • Cycles
    Render
  • Render Mode:板,拉伸,平行,垂直,模型
  • Normal Direction
  • Material:材质
  • Sort Mode
  • Sorting Fudge:优先渲染
  • Case Shadows
  • Receive Shadows
  • Max Particles Size:最大大小

角色

CharacterController:角色控制器
CharacterMotor:角色引擎

动画

Mecanim

射线碰撞

Raycast

资源包

导出:在项目目录 Export Package

导入:在项目目录 Import Package

  • 自定义资源包
  • 内置资源包(Standard Asserts)

Asset Store

在线资源库。

高通 AR

首先在高通AR上下载资SDK(需要注册)。

将下载的SDK资源包导入到Unity中。如果中途提示升级,不需要升级。

配置License Key,要到官网申请License Key,并将其配置到项目中。

在Develop -> License Manager中申请(Development)。

点击创建的项目,进入项目详情,可以看到License Key。复制Key,进入项目目录,在Resources->VuforiaConfiguration中加入Key。

之后上传图片:将要扫描的图片上传到高通,进入Target Manager -> Database,创建数据库,Device,然后进入数据库,Add Target,Single Image,上传图片。

之后下载数据库并导入项目,Download Database,Unity,将下载的数据库导入到项目。

在项目中,首先删掉 Main Camera,并导入AR Camera。Vuforia Prefabs AR Camera,和 Image Target。

再配置Image Target,在其插件中配置Database,Image Target,高和宽,

再配置 VuforiaConfiguration,Datasets,Load并Active。

放置要出现的虚拟物体,作为Image Target的子物体。

C Sharp

字符串是引用类型。字符串操作:

1
2
string str = $"this is {box}";
string str = @"C:\Program files\";

控制台输入输出

1
2
3
string str = Console.ReadLine("{0}", box);
Console.WriteLine("{0}", box);
Console.ReadKey();

遍历数组

1
2
// 无法修改元素
foreach(int x in array) { }

param关键字:用来修饰形参

1
2
3
4
// 调用
method(1, 2, 3);
// 定义
method(param int[] array) { }

引用,ref可以不赋值,但是out必须赋值,且out默认没有值,不可以直接使用。

1
2
3
4
5
6
7
// 传入引用
method(ref a);
method(ref int a) { }

// 传入引用
method(out a);
method(out int a) { }

类继承后,重写父类方法,可以用new;虚函数virtual可以重写override。

类和结构体的区别是,类在堆上,结构体在栈上(值类型)。
结构体必须是有参构造方法,并要给所有字段赋值。

类的默认类型是internal,在项目内部可见。

Sealed:密封,不允许继承,可以用于修饰类和方法。

命名空间中只有类,结构体,枚举,接口。

运算符重载

1
2
3
public static Point operator +(Point p1, Point p2) {
// ...
}

抽象类abstract类

接口Interface:一系列的规范,接口实现必须是public

委托可以直接赋值

1
TestDelegate oneDelegate = TestMethod;

匿名函数

1
2
TestDelegateX method = delegate(){};
TestDelegateX method = () => {}; // 参数 => 方法体

用委托可以实现回调。

泛型,泛型不可继承

1
class Person<T> { }

集合:是容器,变长的,类型可不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ArrayList list = new ArrayList();
list.Add(1);
list.Add("abc");
list.AddRange(new ICollection()); //ICollection可以是集合,数组等
list.Remove(); // 删掉第一个匹配项
list.RemoveAt(); // 删除某个
list.RemoveRange(); // 批量删除,下标与长度
list.SetRange(0, new int[]{1, 2, 3});
list.IndexOf();
list.LastIndexOf();
list.BinarySearch(); // 二分查找
list.Sort();
list.Reverse();
list.Contains();
list.Insert(0, "a");
list.InsertRange(0, int[]{1, 2});

实现ICompareable可以实现排序。

1
CompareTo <0 this<obj

List:

1
2
3
4
5
list.RemoveAll(Predicate<in T>(T obj)); // 删除匹配项
list.RemoveAll(name => name=="abc");
Exists(name => name=="abc") // 判断存在
FindXXX(name => name=="abc")
GetRange() // 获取子集

其他类型:
Stack
Queue
Hashtable:键值对,内部按照Key的Hash排序
Dictionary:字典,必须指定类型

1
2
3
4
5
6
Hashtable table = new Hashtable();
table.Add("key", "value");
foreach(DictionaryEntry entry in table){
entry.Key;
entry.Value;
}
1
2
3
4
5
6
Dictionary<string, string> dic = new Dictionary<string, string>();
dic.Add("key", "value");
foreach(KeyValuePair<string, string> pair in dic){
pair.Key;
pair.Value;
}

正则表达式:

1
2
3
4
5
6
7
using System.Text.RegularExpressions;

public class Test{
public void Test(){
bool result = Regex.IsMatch("a string", "^[1-9]\\d{4,10}$");
}
}

^: 字符串开头
$: 字符串结尾
[]: 匹配括号中的一位字符
[^]: 匹配括号以外的一位字符
+: 匹配1次或多次
*: 匹配0次或多次
?: 匹配0次或1次
{m}: 恰好出现m次
{m,}: 至少出现m次
{m,n}: 出现m到n次
\d: [0-9]
.: 匹配任意字符
(): 分组,返回Match,多个分组

异常处理:如果Catch异常有继承关系,父类应放到最后。

反射:可以通过类名,字段来实例化对象,操作类成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 已有的定义
class Type{
FieldInfo,
MethodInfo,
Constructor,
}

// 实例化对象
Type t = Type.GetType("Space.Person"); // 通过类名获取类型
object obj = Activator.CreateInstance(t); // 只能是 public 构造方法
object obj = Activator.CreateInstance(t, true); // private, internal 也可以
object obj = Activator.CreateInstance(t, para1, para2); // 有参构造
object obj = Activator.CreateInstance(t, BindingFlags.NonPublic|BindingFlags.Instance, null, new object[]{1,2}, null); // 有参其他权限构造

// 访问属性
FileInfo name = t.GetField("name");
FileInfo name = t.GetField("name", BindingFlags.NonPublic|BindingFlags.Instance);
name.SetValue(obj, "abc"); // 静态成员,则用null
object x = name.GetValue(obj);

// 访问方法
MethodInfo method = t.GetMethod("Show", BindingFlags.NonPublic|BindingFlags.Instance);
method.Infoke(null, null); // 第一个是谁在调用,第二个是调用的参数列表

MethodInfo method = t.GetMethod("Show", BindingFlags.NonPublic|BindingFlags.Instance, null, new Type[]{typeof(string), typeof(double)}, null); // 有重载的方法,Type中为参数类型列表
object result = method.Infoke(obj, new object[]{"1", 2.0}); // 第一个是谁在调用,第二个是调用的参数列表

Mono脚本的生命周期

编辑器

Reset:当脚本被附加或重置时执行。

初始化

Awake:初始化后的第一步操作,整个声明周期只执行一次。
OnEnable:启动后的操作,可以多次执行。
Start:当第一帧启动前执行一次。

物理引擎

从物理引擎开始,每一帧都要执行。
物理引擎部分一帧可能执行多次。
物理引擎是个非常消耗资源的部分。

FixedUpdate:按固定时间间隔执行。
yield WaitForFixedUpdate
OnTriggerXXX
OnCollisionXXX

Input 事件

OnMouseXXX:响应设备事件。

游戏逻辑

Update
yield null
yield WWW
yield WaitForSeconds
yield StartCoroutine
内部动画更新。
LateUpdate

场景渲染

OnWillRenderObject:渲染前
OnPreCull
OnBecameVisible
OnBecamingVisible
OnPreRender
OnRenderObject
OnPostRender
OnRenderImage

Gizmos 渲染

OnDrawGizmos

GUI 渲染

OnGUI:可循环多次,但不建议使用

帧结束

yield WaitForEndOfFrame

暂停

OnApplicationPause

关闭

OnDisable

销毁

OnApplicationQuit
OnDelete
OnDestory

程序设计

设计原则

单一职责

对象的职能只有一个。

开闭原则

对象应该对于扩展是开放的,但是对于修改是封闭的。

依赖倒置原则

首先了解需求,画图表示;写出代码。

接口隔离原则

接口里面的单一原则,也就是接口只负责一个功能。

合成复用原则

父类解决共有问题,子类解决特有问题。

是什么的问题改为有什么的问题。

能用现有的类,就不要创建新类。

迪米特法则

最少知识原则:尽量不牵扯到其他的类

设计模式

单例模式

整个软件的声明周期,有且只有一个实例。

拖拽一次,实例化一次。

用在组织框架管理者身上:WWW管理器,UI管理器,音效管理器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 只能拖拽挂载,不能 new
public class Single: MonoBehaviour{
public static Single instance;
void Awake(){
instance = this;
}
void Start(){

}
}
// 另外一种方法
public class Single{
private static Single instance;
public static Single Instance{
get{
if(instance == null){
instance = new Single;
}
return instance;
}
}
}

工厂模式

交给它任务,它自动生产,最后返回一个产品。

不关心生产过程,用于创建新的对象。

例如生产金币,怪物。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class Factory{
Sprite[] allSprite;
Transform parents;

public Factory(){
Initial();
}

public void Initial(){
allSprite = Resources.LoadAll<Sprite>("res");
// 场景中唯一的物体,才做标记,灯光,摄像机
parents = GameObject.FindGameObjectWithTag("MainCanvas").transform; // 深度优先遍历,效率低
}

public GameObject CreateImage(int index, Vector3 pos){
GameObject obj = new GameObject();
obj.name = index.ToString();
// 设置物体 Parents
obj.transform.SetParent(parents, false);
// 设置在 Parents 下的位置
obj.transform.SetAsSiblingIndex(2);
obj.transform.SetAsLastSibling();
obj.transform.SetAsFirstSibling();
obj.transform.localPosition = pos;

Image img = obj.AddComponent<Image>();
index = index % allSprite.Length;
img.sprite = allSprite[index];
return obj;
}
}

public class UseFactory: MonoBehaviour{
Factory factory;

void Start(){
factory = new Factory();
}

void Update(){
int cnt = 0;
if(Input.GetKeyDown(KeyCode.Space)){
facotry.CreateImage(++cnt, Vector3.one * cnt * 50);
}
}
}

观察者模式

不断地询问。

例如:播放一个动作,在1s的时候播放音效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Obstor: MonoBehaviour{
Animation animal;
float timeCount = 0;

void Start(){
animal = GetComponent<Animation>();

}

public void PlayAction(){
animal.Play("Action");
timeCount = 0;
}

// 尽量轻量级
void Update(){
if(!animal.isPlaying){
// 判断动画是否完成
}
timeCount += Time.deltaTime;
if(timeCount > 1.0f){
timeCount = 0;
// 判断是否到某个时间节点
}
}
}

代理模式

使用回调(代理)的方式调用对方。B留出回调指针,M给B的回调指针加上A的方法。当触发到某一事件时,B调用回调函数,就会间接调用A。

例如:按钮事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

using UnityEngine.UI;
using UnityEngine.Events;

public class UseFactory: MonoBehaviour{
Factory factory;
void Start(){
factory = new Factory();
Button = btn = transform.GetComponent<Button>();
// 传入回调函数
// 分配的空间在堆上
btn.onClick.AddListener(new UnityAciton(BtnOnClick));
// 分配的空间在栈上
btn.onClick.AddListener(BtnOnClick);
}

public void BtnOnClick(){
// 按钮事件
Debug.Log("Btn Clicked");
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

public class Master{
Worker worker;
public void CallWorker(){
worker.Task();
}
public void OnWorkerCall(){
Debug.Log("Work Finish");
}
}

public class Worker{
public delete void TaskPointer();

public TaskPointer taskPointer;
public void Task(){
taskPointer();
}
}

public class UseDelegate: MonoBehaviour{
void Start(){
Master master = new Master();
Worker worker = new Worker();
// Master 调用 Worker
master.CallWorker();

// Worker 调用 Master
worker.taskPointer += master.OnWorkerCall;
}
}

策略者模式

决策制可以根据不同对象的输入得到不同的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class AbsBase{
public flaot pay;
public virtual void CalculateTex(){
// ...
}
}

public class AbsPerson: AbsBase{
public override void CalculateTex(){
base.CalculateTex();
pay *= 0.08f;
}
}

public class AbsCompany: AbsBase{
public overrie void CalculateTex(){
base.CalculateTex();
pay *= 0.12f;
}
}

public void TestAbstrator: MonoBehaviour{
public void CalculateTex(AbsBase bs){
bs.CalculateTex();
}

// 根据不同的对象,自动选择不同的计算方法,得到不同的结果
void Start(){
AbsPerson person = new AbsPerson();
CalculateTex(person);

AbsCompany company = new AbsCompany();
CalculateTex(company);
}
}

建造者模式

将一个大的东西拆分成小的东西。

例如:MVC框架。

由顶部的UI Manager管理下层的对象。对模块的管理,用单例模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 不对外提供Manager,Worker,只提供Master
// Master是单例模式
public class Master{
public static Master instance;
public static Master Instance{
get{
if(instance == null){
instance = new Master();
}
return instance;
}
}
public void CreateTask(){
Manager manager = new Manager();
manager.ResolveTask();
}
}

public class Manager(){
public void ResolveTask(){
Worker worker = new Worker();
worker.ExecuteTask();
}
}

public class Worker(){
public void ExecuteTask(){

}
}

中介者模式

防止类直接互相影响。解决耦合性问题。

例如:怪物和玩家之间的互相攻击。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class MiddleBase{
public float hp;
public virtual void reduce(float bias){
hp -= bias;
}

}

public class MiddleFirst: MiddleBase{
public override void reduce(float bias){
hp -= bias * 1.3f;
}
}

public class MiddleSecond: MiddleBase{
public override void reduce(float bias){
hp -= bias * 0.7f;
}
}
}

public class Main: MonoBehaviour{
public void CalculateHP(MiddleBase first, MiddleBase second){
first.reduce(1);
}

public void Update(){
MiddleFirst first = new MiddleFirst();
MiddleSecond second = new MiddleSecond();

CalculateHP(first, second);
CalculateHP(second, first);
}
}

外观模式

将各种不同类型的东西放在一起,聚合,形成特定的功能。

例如:红绿灯,日期时间控件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class UI: MonoBehaviour{
private GameObject Red;
private GameObject Blue;
private bool stateRed = false;

public void Start(){
Button btn = GetComponent<Button>();
btn.onClick.AddListener(OnClick);
}

public void OnClick(){
if(stateRed){
Red.SetActive(true);
Blue.SetActive(false);
}else{
Red.SetActive(false);
Blue.SetActive(true);
}
stateRed = !stateRed;
}
}

组合模式

将相同类型的东西组合一起,形成特有功能。

例如:构成一辆车的轮胎,框架,发动机等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class CarBase{
public virtual void Forward(){

}

public virtual void Backword(){

}
}

public class Wheel: CarBase{
public override void Forward();
public override void Backword();
}

public class Engine: CarBase{
public override void Forward();
public override void Backword();
}

public class Main: MonoBehaviour{
List<CarBase> car;
void Start(){
car = new List<CarBase>();
car.Add(new Engine());
car.Add(new Wheel());
}

void Update(){
for(int i = 0; i < car.Count; i++){
car[i].Forward();
}
}
}

状态者模式

FSM 有限状态机或命令者模式。

可以用来制作动画。

模型文件:
Obj 只有模型(顶点,法线,切线),没有动画。
Fbx 带动画和模型
Dae 只有模型

这里用Fbx文件。

模型系统:
Scale Factor:缩放因子,Unity以米为单位,因此需要缩放。
Mesh Compression
Read Write Enable
Optimize Mesh:优化,采用四边形
Import BlendShapes

Normals:法线,Import,模型自带法线
Tangents:切线(切线:右向量,副切线:前向量),Import,自带

Materials:导入材质

动画系统:
关节动画:骨骼(子级在父级的坐标 Transform)动画。

  • MeshFilter 筛选定点
  • MeshRender 渲染选出的的定点
    蒙皮动画:皮肤动画。
  • SkinMeshRender 将模型传递给GPU
    顶点动画:对模型的顶点做动画。

Rig
动画类型:

  • Legacy 关节动画,通过代码控制
  • Generic 状态机动画,非人形动画,动画不能通用
  • Humanoid 状态机动画,人形动画,可以复用动画
    • OrcAvatar:保存人物动画的映射

一般模型,动画分开两个文件保存。

动画文件,一般都是原地动画,也有带位移的动画。

动画事件通过代码实现。

fbx和meta文件一起放入目录。

控制器代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class OrcCtrl: MonoBehaviour{
CharacterController charCtrl;
Animator animator;

void OnAnimatorMove(){
// 有重力,参数为动画片段的位移
charCtrl.SimpleMove(animator.deltaPosition);
charCtrl.SimpleMove(animator.deltaRotation);
charCtrl.SimpleMove(animator.rootPosition);
// 没有重力,参数为向前移动
charCtrl.Move(Vector3.forward);
}

void Start(){
charCtrl = GetComponent<CharacterController>();
animator = GetComponent<Animator>();
}
}

Shader

开发语言和工具

开发语言:
HLSL:专用于Direct3D和XNA。
Cg:C for Graphics,由NVIDIA开发
GLSL:用于OpenGL
ShaderLab:Unity使用,类似CgFX和Direct3D的效果框架。

开发工具:
FX Composer
Render Monkey
MonoDevelop:Unity使用
第三方IDE

网络

基本介绍

UDP:传输顺序不一定。
RUDP:保证顺序一定的UDP,发送数据后等待却认。
TCP:消耗资源较大,有拥塞控制。

MMORPG:多人在线游戏,状态同步,实时同步状态(发送状态而不是位置)到所有客户端。网络延迟时,每3-5秒同步一次。网络掉线时,重连,可以用Unity 的API判断。
Moba:采用RUDP,使用网络帧同步,50ms - 60ms 一次。使用Lock Step技术,服务器等待所有结果,一台机器卡,所有机器都卡。使用乐观锁,不等待客户端,只下发。
ARPG:角色扮演
PVP:对战
PVE:打怪
SLG:策略游戏

创建连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
void Start(){
Socket server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPEndPoint endPoint = new IPEndPoint(IPAddress.Parse("192.168.1.1"), 8000);
IPEndPoint endPoint = new IPEndPoint(IPAddress.Any, 8000);
server.Bind(endPoint);
Thread thread = new Thread(() => {
while(true){
try{
server.BeginAccept((IAsyncResult callback) => {
// 有连接 会回调这里
Socket client = server.EndAccept(callback);
}, null); // 异步,参数
}catch(Exception e){
Debug.Log(e.ToString());
}
Thread.Sleep(50);
}
});
thread.Start();
}
// 传输数据,转换二进制流或JSON
void Use(){
WebCamDevice cam = WebCamTexture.devices[0];
WebCamTexture tex = new WebCamTexture(cam.name, 300, 400, 10); // 尺寸,帧率
transform.GetComponent<Renderer>().material.mainTexture = tex;
tex.Play();
}
byte[] GetBytes(){
Texture2D raw = new Texture2D(300, 400);
for(int i=0;i...){
for(){
Color c = tex.GetPixel(j, i);
raw.SetPixel(j, i, c);
}
}
return raw.EncodeToJPG();
}
void Send(){
UDPSocket send = new UDPSocket(port, size, new UDPSocket.RecvDelegate(()=>{
send.SendBytes(GetBytes(), "ip");
}));
}
// API
Buffer.BlockCopy();
// 线程传数据
// 主 到 辅:主线程锁住,变换
// 辅 到 主:recvBuffer锁住,接收数据

接收后更新M层数据,在主线程。

示例模块

一个游戏一般包含:

  • Audio 音效
  • WWW 网络链接/本地资源文件
  • DB 数据库
  • Effect 特效
  • UI 用户界面
  • Animation 动画(FSM 有限自动机)
    • Player 玩家
    • NPC 机器人

网络传输模块

例如进入游戏之后,先检查游戏更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
public class TestHttp: MonoBehaviour{
// GET 请求
public IEnumerator SendGet(string url){
WWW www = new WWW(url);
// 协程,将任务分片加载
yield return www;

// 判断是否下载错误
if(string.IsNullOrEmpty(www.error)){
// 没有错误
Debug.Log(www.txt);
}
}

// POST 请求
public IEnumerator SendPost(string url, WWWForm form){
WWW www = new WWW(url, form);
// 协程,将任务分片加载
yield return www;

// 判断是否下载错误
if(string.IsNullOrEmpty(www.error)){
// 没有错误
Debug.Log(www.txt);
}
}

// 下载本地文件
public IEnumerator DownloadLocalFile(string url){
WWW www = new WWW(url, form);
// 协程,将任务分片加载
yield return www;

// 判断是否下载错误
if(string.IsNullOrEmpty(www.error)){
// 没有错误
Debug.Log(www.txt);
}
}

public string InitUrl(string url){
// iOS: "file://"
// Android: "jar:file://"
// PC: "file:///"
if(Application.platform == RuntimePlatform.WindowsEditor
|| Application.platform == RuntimePlatform.WindowsPlayer){
url = "file:///" + url;
}else if(Application.platform == RuntimePlatform.Android){
url = "jar:file://" + url;
}else{
url = "file://" + url;
}
return url;
}

void Start(){
string url = "";
// 启动协程
StartCoroutine(SendGet(url));

WWWForm form = new WWWForm();
form.AddField("arg", "value");
StartCoroutine(SendPost(url, form));

// Application.dataPath: 应用路径/Asserts/,一般只读,访问工程,只在PC上
// Application.persistentDataPath: 缓存路径,用文件系统可读可写
// Android:SD 卡
// iOS:Document
// Android:User/AppData/Local...
// Application.streamingAssetsPath: 应用路径/Asserts/StreamingAsserts,该路径下所有文件会被打包到实机上(打包后的assets路径下,只能用www下载,不可写)
string filepath = Application.dataPath + "/filename.txt";
StartCoroutine(DownloadLocalFile(InitUrl(filepath)))
}
}

封装模块:

下载任务基类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class WWWItem{
public virtual void BeginDownload();
public virtual void DownlaodFinish(WWW www);
public virtual void DownloadError(WWWItem item);

private string url;
public string Url{
get {return url;}
set {url = value;}
}
public IEnumerator Download(){
BeginDownload();
WWW www = new WWW(Url);
yield return www;
if(string.IsNullOrEmpty(www.error)){
DownlaodFinish(www);
}else{
DownloadError(this);
}
}
}

下载队列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class WWWHelper: MonoBehaviour{
public static WWWHelper Instance;

Queue<WWWItem> allTask;
void Start(){
Instance = this;
allTask = new Queue<WWWItem>();
}

public void AddTask(WWWItem item){
allTask.Enqueue(item);
if(allTask.Count == 1){
StartCoroutine(DownloadItems());
}
}

public IEnumerator DownloadItems(){
while(allTask.Count > 0){
WWWItem item = allTask.Dqeueue();
yield return item.Download();
}
}
}

下载TXT文件模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class WWWTxt: WWWItem{
public WWWText(string url){
url = InitUrl(url);
}

public string InitUrl(string url){
if(Application.platform == RuntimePlatform.WindowsEditor
|| Application.platform == RuntimePlatform.WindowsPlayer){
url = "file:///" + url;
}else if(Application.platform == RuntimePlatform.Android){
url = "jar:file://" + url;
}else{
url = "file://" + url;
}
return url;
}

public override void BeginDownload(){
Debug.Log("Start ...");
}
public override void DownlaodFinish(WWW www){
Debug.Log(www.txt);
}
public override void DownloadError(WWWItem item){
WWWHelper.Instance.AddTast(item);
}
}

使用:

1
2
3
string url = Application.dataPath + "/filename";
WWWTxt txtTask = new WWWTxt(url);
WWWHelper.Instance.AddTask(txtTask);

音效模块

需要监听者,声源。

Audio Listener:相当于摄像机,唯一。

Audio Source:可以播放Audio Clip,非常占用空间,继承MonoBehavior,属性较多。

因此:
- 应该使用有限个Audio Source播放所有Clips(对象池)

Audio 模块:

  • SourceManager:管理AudioSoruce对象,寻找空闲的对象,播放Clip
    • 拿出空闲的Audio Source
    • 释放空闲的Audio Source
    • 播放或停止播放
  • AudioSoruce:3个
  • ClipsManager:管理Clip对象
    • 有名字作为索引
    • 通过配置文件读取
    • 加载到内存
  • Clip:无数个
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
public class AudioManger: MonoBehaviour{
public static AudioManger Instance;

private SourceManager sourceManager;
private ClipManager clipManager;

void Start(){
Instance = this;
sourceManager = new SourceManager(gameObject);
clipManager = new ClipManager();
}

void Play(string audioName){
AudioSource source = sourceManage.GetFreeAudio();
SingleClip singleClip = clipManager.FindClipByName(audioName);
singleClip.Play(source);
}

void Stop(string audioName){
sourceManager.Stop(audioName);
}
}

public class SourceManager{
List<AudioSource> allSources;
GameObject owner;

public SourceManager(GameObject own){
owner = own;
InitAudioSourceList();
}

public void InitAudioSourceList(){
allSources = new List<AudioSource>();
for(int i=0;i<3;i++){
AudioSource source = owner.AddComponent<AudioSource>();
allSources.Add(source);
}
}

public AudioSource GetFreeAudio(){
if(int i=0;i<allSources.Count;i++){
if(!allSources[i].isPlaying){
return allSources[i];
}
}
AudioSource source = owner.AddComponent<AudioSouce>();
allSources.Add(source);
return source;
}

public void ReleaseFreeAudio(){
int cnt = 0;
List<AudioSource> releaseSources = new List<AudioSource>();
for(int i=0;i<allSources.Count;i++){
if(!allSources[i].isPlaying){
cnt++;
if(cnt > 3){
releaseSources.Add(allSources[i]);
}
}
}
for(i=0;i<releaseSources.Count;i++){
allSources.Remove(releaseSources[i]);
GameObject.Destroy(releaseSources[i]);
}
releaseSources.Clear();
releaseSources = null;
}

public void Stop(string name){
for(int i=0;i<allSources.Count;i++){
if(allSources[i].isPlaying &&
allSources[i].clip.name.Equals(name)){
allSources[i].Stop();
}
}
}
}



public class ClipManager{
// 从配置文件加载
string[] clipName = null;
string[] clipFile = null;
int clipCount = 0;

SingleClip[] allSingleClip;

public ClipManager(){
ReadConfigFile();
LoadClips();
}

public void ReadConfigFile(){
var fileAddress = Application.streamingAssertsPath + "/AudioConfig.txt";
FileInfo info = new FileInfo(fileAddress);
if(info.Exists){
with()
StreamReader sr = new StreamReader(fileAddress);
string lineCountStr = sr.ReadLine();
int lineCount = 0;
if(int.TryParse(lineCountStr, out lineCount)){
clipName = new string[lineCount];
clipFile = new string[lineCount];
for(int i=0;i<lineCount;i++){
string[] line = sr.ReadLine().Split("\t".ToCharArray());
clipName[i] = line[0];
clipFile[i] = line[1];
}
}else{
// Read Error
}
}
}

public void LoadClips(){
allSingleClip = new SingleClip[clipCount];
for(int i=0;i<clipCount;i++){
AudioClip clip = Resources.Load<AudioClip>(clipFile[i]);
SingleClip sclip = new SingleClip(clip);
allSingleClip[i] = sclip;
}
}

public SingleClip FindClipByName(string name){
for(int i=0;i<clipCount;i++){
if(clipName[i] == name){
return allSingleClip[i];
}
}
return null;
}
}

配置文件AudioConfig.txt

1
2
3
4
3
ClipButton ClipButton.mp3
River River.mp3
UIMusic UIMusic.mp3

Clip 存储:

1
2
3
4
5
6
7
8
9
10
11
public class SingleClip{
AudioClip clip;
public SingleClip(AudioClip tclip){
clip = tclip;
}

public void Play(AudioSource source){
source.clip = clip;
source.play();
}
}

UI 模块

从效果图入手,切图成为碎图。
使用Texture Packer打大图:

Data Format:Unity Texture2D
Texture format:PNG
Image format:RGBA8888: 每个通道8bit。

Geometry
Max Size:2048*2048 (1024*1024)

其他保持默认,然后将碎图文件夹拖入其中,得到拼图(tpsheet)

在Unity中,加载插件codeandweb.com放入根目录。

之后根据拼图拼UI,以Panel为单位。

使用时,使用九宫格拉伸。

最后写代码:

Controller:
UIManager管理所有Panel及其子控件。
Panel包含部分子控件。
子控件消息通信通过UIManager直接查询对方。
子控件主动报告数据。

子控件设计:实现UIBehaviour,实现向UIManager注册自己
UIManager:被注册后,以Panel为单位划分,挂载子控件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
public class UIBehaviour: MonoBehaviour{
void Awake(){
var panel = transform.GetComponentInParent<UIBase>();
UIManager.Instance.RegistGameObject(panel, transform.name, gameObject)
}

public void AddButtonListener(UnityAction action){
Button btn = transform.GetComponent<Button>();
if(btn != null){
btn.onClick.AddListener(action);
}
}

public void AddDragInterface(UnityAction<BaseEventData> action){
EventTrrigger trigger = gameObject.GetComponent<EventTrigger>();

if(trigger == null){
trigger = gameObject.AddComponent<EventTrigger>();
}

EventTrigger.Entry entry = EventTrigger.Entry();
entry.eventID = EventTriggerType.Drag;
entry.callback = new EventTrigger.TriggerEvent();
entry.callback.AddListener(action);
trigger.triggers.Add(entry);
}
}

public class UIBase: MonoBehaviour{
void Awake(){
Transform[] allChd = transform.GetComponentsInChildren<Transform>();
for(int i=0;i<allChd.Lenght;i++){
if(allChd[i].name.EndsWith("_N")){
allChd[i].gameObject.AddComponent<UIBehaviour>();
}
}
}

void OnDestory(){
UIManager.Instance.UnRegistPanel(transform.name);
}

public GameObject GetWidget(string widget){
return UIManager.Instance.GetWidget(transform.name, widget);
}

public UIBehaviour GetBehaviour(string widget){
GameObject obj = GetWidget(widget);
if(obj != null){
return obj.GetComponent<UIBehaviour>();
}
return null;
}

public void AddButtonListener(string widget, UnityAction action){
UIBehaviour bvr = GetBehaviour(widget);
if(bvr != null){
bvr.AddButtonListener(action);
}
}

public void AddDrag(string widget, UnityAction<BaseEventData> action){
UIBehaviour bvr = GetBehaviour(widget);
if(bvr != null){
bvr.AddDragInterface(action);
}
}
}

public class UIManager: MonoBehaviour{
public static UIManager Instance;
// 第一级表示Panel,第二级表示子控件
Dictionary<string, Dictionary<string, GameObject>> allWidgets;

public void RegistGameObject(string panel, string widget, GameObject obj){
if(allWidgets.ContainsKey(panel)){
allWidgets[panel] = new Dictionary<string, Dictionary<string, GameObject>();
}
allWidgets[panel].Add(widget, obj)
}

public void UnRegistGameObject(string panel, string widget, GameObject obj){
if(allWidgets.ContainsKey(panel)){
if(allWidgets[panel].ContainsKey(widget)){
allWidgets[panel].Remove(widget);
}
}
}

public void UnRegistPanel(string panel){
if(allWidgets.ContainsKey(panel)){
allWidgets[panel].Clear();
allWidgets[panel] = null;
}
}

public GameObject GetWidget(string panel, string widget){
if(allWidgets.ContainsKey(panel)){
return allWidgets[panel][widget];
}
return null;
}

void Awake(){
Instance = this;
allWidgets = new Dictionary<string, Dictionary<string, GameObject>>();
}
}

public class Regist: UIBase{
RegistModel registModel;
public void OnClick(){
// ...
}

void Start(){
AddButtonListener("btn_N", OnClick);
}
}

Model:

1
2
3
4
public class RegistModel{
public string username;
public string password;
}

资源加载:
在Loading界面,要将资源加载到内存。用的时候再实例化。

在列表类的控件,会在子控件加载多个同类型的子单元控件。因此需要批量加载初始化。

现在让子控件称为UISubManager,来管理下级单位。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class UISubManager: MonoBehaviour{
Dictionary<string, GameObject> allChd;

void Awake(){
UIBase bs = transform.GetComponetInParent<UIBase>();
UIManager.Instance.RegistGameObject(,)

Transform[] chd = transform.GetComponentsInChildren<Transform>();

for(int i=0;i<chd.Length;i++){
if(chd[i].name.EndsWith("_S")){
allChd.Add(chd[i].name, chd[i]);
}
}
}

public Transform GetChidTransform(string widget){
return allChd[widget];
}

void OnDestory(){
if(allChd != null){
allChd.Clear();
allChd = null;
}
}
}

摇杆 EasyTouch

首先获取鼠标到圆心的距离,与方向。
考虑移动范围:圆内任意移动,不可超出圆。

小地图

可以使用相机,修改属性:
Viewport Rect:显示在屏幕上的位置和大小
Depth:相机前后关系

但是以上方法渲染两次,消耗资源。

因此可以使用等比例映射,也就是用图片做小地图。

创建一张图片,使其(锚点)对齐到右上角。

然后让物体的位置映射到小地图上。

物体相对于地形的位置,按照比例缩小到小地图。

GL

GL.PushMatrix()
位置变换 GL.MultMatrix(transform.localToWorldMatrix)
正交投影 GL.LoadOrtho()
画线 GL.Begin(GL.LINES)
也可以画三角形,多边形等。
颜色 GL.Color(Color.red)
位置 GL.Vertex3(0.1f, 0.1f, 0)
结束 GL.End()
GL.PopMatrix()

小地图上的摄像机可视区域

得到相机的可视区域,映射到小地图上面。

将相机的四个角对应到世界位置上:从相机的四个角发出4条射线到地面,得到4个交点。再映射到小地图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class CameraView: MonoBehaviour{
void Update(){
Ray first = Camera.main.ViewportPointToRay(Vector3.zero);
Ray second = Camera.main.ViewportPointToRay(Vector3.right);
Ray third = Camera.main.ViewportPointToRay(Vector3.one);
Ray forth = Camera.main.ViewportPointToRay(Vector3.up);

RaycastHit hitout;
if(Physics.Raycast(first, out hitout, 1000)){
if(hitout.transform.CompareTag("Ground")){
// 得到世界坐标
hitout.point
}
}
}
}