2014-11-14 22:22:15 imdaixixi 阅读数 667
本日志,随时更新

今天学了最基础的打印“hello world”
首先,创建C#脚本(Assets-create-C# Script)
可重命名自己需要的名字,双击打开,会弹出u3d默认编辑器

打开后的界面

在void Start()里输入代码

using UnityEngine;

using System.Collections;


public class hello : MonoBehaviour {


// Use this for initialization

void Start () {

print ("hello world");

}

// Update is called once per frame

void Update () {

}

}

保存,把脚本拖拽到游戏对象上,或主照相机

运行该场景即可看到“hello world”



2014-01-04 10:32:55 cjc_hoderxx123 阅读数 1468

1.

这次出现这个错误是我在做毕业大设计的时候,用一个SoundManager脚本来统一管理游戏音效文件,所有的文件初始化放在Start()函数里;在PlayerControl脚本里面的Start()函数里调用了音效脚本管理的一个播放文件方法,导致出现这种错误。

注释:assigned是分配的意思。

后来我把音效管理脚本的音效初始化放在了Awake(),让其初始化优先级高于调用脚本的优先级,程序正常运行。

2.2013-11-29

今天老师要交期末小组作业,事发突然,我赶快修改了一些小bug,然后开始导出PC平台的文件,但是怎么多导不出来。出现了下列类似的问题:

编译项目时报错: 
错误提示: 
An asset is marked as dont save, but is included in the 
build:Asset:'Assets/resources/peidiangui2.prefab'UnityEditor.HostView:OnGUI() 
原因: 
peidiangui2.prefab这个文件有问题: 
解决方法: 
删除“peidiangui2.prefab”,重新新建一个prefab,即可

2019-04-03 16:19:15 Michaelia_hu 阅读数 139

主要介绍Unity 3D脚本编译流程,JIT编译、AOT编译、如何将项目发布到Android与IOS上。

一、Unity 3D脚本编译流程

我们都知道,用高级编程语言写成的源代码并不能直接被机器识别并运行,而是通过编译成原生码,也就是二进制机器码,才能运行。Unity使用的三种脚本语言编写的代码,则需要经过两次编译,第一次,将源代码编译成.Net的dll文件,第二次是运行时,进行JIT编译,从CIL编译成机器码(IOS系统除外)。

1. 第一次编译

这一次编译的实际作用是语法检查以及“正确代码”分析,并输出对源代码行为的表述代码。Unity会利用Mono将所有脚本编译成.Net的dll文件,此时生成的是CIL代码中间语言。  因为CIL代码需要运行时来控制它的执行,所以也称为托管模块,托管模块包括CIL代码和metadata元数据

metadata元数据:描述模块定义了什么,如类型与成员,有时也会描述引用了哪些内容。这样的优点时在第二次编译时不需要额外生成原生的C/C++头文件与库文件支持,直接利用CIL与metadata就可以获得编译时的全部信息。

但是其实这一次编译后得到并非托管模块,而是一些文件名以Assembly开头的dll文件,实际上,运行时是通过这些dll文件,也就是程序集和托管模块接触的。程序集是重用、安全性以及版本控制的最小单元。所以,编译器会将托管模块转化为程序集。程序集包括PE32或PE32+文件与所有元数据表manifest清单。

为什么要引入程序集呢?因为程序集可重用,可以将常用的类型,如hero,solider等类型放在一个程序集中,不常用的,如每年的节日活动放到另一个程序集中。

因为没有直接编译成二进制文件,所以这一步的速度很快。

而在编译时,必须按照某种顺序才能正确地编译,不然这个脚本被编译的时候,它引用的脚本还未被编译,则会报错。在Unity中有一些保留文件夹会被先编译,Unity的编译顺序为:

a. Standard Assets文件夹:这个文件夹最先编译,里面会放一些Unity自带的组件,如3rd Person Controller等。

a. Pro Standard Assets文件夹,这个文件夹紧接着上一个编译,但是存放的是专业版才有的功能。

a. Plugins文件夹:存放一些可以在游戏脚本中访问的原生插件,它只能放在Assets文件夹的根目录下。

以上三个文件夹中的脚本都不会调用其他文件夹中的脚本,所以被优先编译不会报错,但是它们可以通过GameObject.SendMessage()和其他脚本通信。

b. Editor文件夹:该文件夹在项目发布时不会被打包,只能在编辑器中被编译,主要实现一些对编辑器的自定义功能。如果Editor文件夹位于以上三个文件夹中,则接下来编译这个文件夹。

c. 接下来编译开发者自己写的不在Editor文件夹中的脚本文件。

d. 最后编译不在特殊文件夹下的Editor文件夹中的脚本。

2. 第二次编译

(1)JIT编译

JIT(just-in-time)编译,也就是即时编译,是在运行时的环境中发生的编译,游戏启动后,首先将Mono运行时载入内存,然后加载所需的程序集,然后调用入口方法。接下来就是对游戏脚本进行JIT编译了。

在运行一个方法之前,Mono运行时会检测出这个方法中需要的对象,为它们分配内部数据结构,并且调用的对象方法有个记录项,Mono运行时将这些记录项放在JITCompiler的未编档函数中,之后在运行到相关函数时,会调用JITCompiler函数来进行JIT编译,将CIL编译成原生代码。这里需要注意的是,如果一个方法中多次调用了另一个方法,则只会在第一次调用时需要编译,之后会直接从内存块中读取之前编译好的该方法的原生码。

虽然这种把编译拆成两段的编译方式比直接从高级语言翻译成机器码更加低效,但是它也有一些好处:

a. 支持多平台,因为先编译成了CIL,就利用了CIL的跨平台能力。

b. JIT会对原生代码进行性能优化。

其实经过Mono的优化,JIT即时编译虽然对运行效率有影响,但是影响却不大。

因为IOS封了内存(或者说是堆)的可执行权,这相当于变相封锁了JIT这种编译方式,所以不能进行热更新。

(2) AOT提前编译

不是在运行时编译脚本。AOT并非将所有的CIL都编译完成,它和JIT并非对立,使用AOT会减少JIT的工作量。

(3) Full-AOT

这种编译方式主要是为了适应ios系统,它保证在运行时不进行任何JIT编译。但是有一些限制:

a. 不支持泛型虚方法

b. 不允许动态生成代码因此无法使用System.Reflection.Emit,也就无法动态创建类。无法使用DLR或基于DLR的任何语言。

c. 不支持对泛型类的P\Invoke

d. 目前不能使用反射中的Property.SetInfo给非空类型赋值。

e. 值类型作为Dictionary的Key时会有问题。

不要混淆了反射与System.Reflection.Emit,所有反射与相关API均可用。

这种编译的优点有:

a. 减少了启动时间

b. 有利于内存共享以节约内存(如果一个程序集被同时被多个进程加载,就可以通过内存映射的方式同时映射到多个进程的地址空间)

 

 

 

 

 

 

 

 

 

 

 

 

 

2017-12-19 10:27:23 pursue16 阅读数 1457

通过查阅资料,看到unity3d的通信有以下几种方式:
1.利用在脚本A中定义对外接口函数,然后在脚本B中找到A所在的对象,再找到脚本A,进而调用里面的对外接口函数,这个方法在我的《unity3D NGUI中button响应事件实现》这篇文章中有仔细说明,可以参考。
2.还有一个是利用SendMessage的方法来实现脚本互相通信。
首先在A脚本中编写对外接口函数,如下所示:

using UnityEngine;  
using System.Collections;  

  public class A : MonoBehaviour {  
  public void Show_nunber(int n)  

      {  

      print("num: " + n );  

      }  
}  

然后在脚本B中利用SendMessage机制调用脚本A里面的Show_number函数。如下:

using UnityEngine;  
using System.Collections;  

 public class B : MonoBehaviour {  

 public GameObject Obj;//A脚本绑定在一个物体上的时候,再把脚本B拖拽到这个GameObject  
  void Start ()   
    {  

   Obj.SendMessage("Show_number","10");//相当于调用脚本B里面的函数,第一个参数是函数名,第二个传递的整型参数  

  }  

这样就可以实现两个不同对象上的脚本相互通信了。

以上引用于博客:

http://blog.csdn.net/u012805027/article/details/17102393

但是我后来发现,我在最开始想要实现的通信,并不是这种“复杂”的传递参数。有一种更简单的方法,直接调用public类的函数就可以了。
比如,有一个串口通信类
public class SerialPortReciever : MonoBehaviour{}
在另一个类中直接实例化一个对象

public class Anima_Control : MonoBehaviour {
    public SerialPortReciever Obj;//  
    ……
if (Obj.R1 == 2)
        {
            animation["first"].speed = 1f;
            animation.Play("first");
            ……
        }
    }

其实通信没有那么复杂。

2013-08-21 18:32:12 kfqcome 阅读数 19765

一 创建和使用脚本

1 概述

GameObject的行为都是被附加到其上面的组件控制,脚本本质上也是一个组件。

在unity中创建一个脚本,默认内容如下:

using UnityEngine;
using System.Collections;

public class MainPlayer : MonoBehaviour {

// Use this for initialization
void Start () {

}

// Update is called once per frame
void Update () {

}
}



一个脚本通过实现一个派生自”MonoBehaviour”的类来与unity的内部工作机制建立联系。可以将新创建的组件类型的类作为一个蓝图,该类作为一个新类型的组件被附加到游戏对象。每次将一个脚本组件附加到游戏对象,都会创建一个该蓝图定义的对象的实例。创建的脚本文件的文件名必须与里面的类名相同,这样才能将其附加到游戏对象上。


需要注意到的主要事情是,该类中定义的两个函数。Update函数处理游戏对象的帧更新相关的操作。这个可能包括移动,触发动作以及对用户输入的反馈,基本上在游戏过程期间需要处理的任何事情都可以在这里面处理。Start函数在游戏开始前被unity调用(例如,在Update被第一次调用之前),因而是一个进行初始化操作的理想位置。


可能会疑惑为什么不把初始化操作放在类的构造函数中,这是因为对象的构造是由编辑器处理的,在游戏开始的时候并不如想象中那样会发生。如果为一个脚本组件定义构造器,它会和unity的正常操作发生干扰从而导致一些问题。


脚本被创建之后是处于不激活状态的,只有将它的一个实例附加到一个游戏对象之后代码才会被激活。同时,一个游戏对象的一个类型的组件只能有一个,也就是可以附加到游戏对象的脚本组件不能超过一个。


2 变量

就是指的类的成员变量,只不过在unity这里将成员变量设为公有的时候,将其附加到游戏对象后,可以在游戏对象的监视面板中的脚本组件那栏里面看到该公有变量,也就是说可以在编辑器里面直接对该公有变量进行赋值,同时在debug状态下也可以在面板中看到它的值。

unity在编辑器中显示类的成员变量的时候,在类名中遇到(不是第一个字母的)大写字母的时候,会在该字母前加入一个空格,例如在类里面定义个公有成员变量名为”TestVar”,在编辑器中显示的变量名为”Test Var”,中间加了一个空格,但是这只是一个unity显示变量的方式,真正在代码中访问这个变量仍然应该使用”TestVar”这个名字。


3 事件函数

unity中的脚本并不像传统意义上的程序,即在一个循环中连续的运行直到完成任务后再退出,而是unity通过调用定义在脚本内的某个函数,间断的将控制权交给一个脚本。一旦一个函数完成执行,控制权被交还给unity。这些函数就是所知的事件函数,因为它们由unity调用用于响应发生在游戏过程中的事件。unity支持的所有事件都定义在MonoBehaviour这个里面,可以在该类的参考文档中看到一系列的事件。下面介绍几个最常见、最重要的事件。

<1> 常规更新事件(Regular Update Events)

一个游戏更像一个在线生成动画帧的动画。游戏编程中的一个关键观念就是在每帧渲染前对游戏对象的位置、状态和行为进行改变。Update函数是unity中放置这类代码的主要地方,它在每帧被渲染前、同时也在动画被计算前调用。

update事件函数

void Update() {
float distance = speed * Time.deltaTime * Input.GetAxis("Horizontal");
transform.Translate(Vector3.right * speed);
}

物体引擎也以一个和帧渲染类似的方式每隔一个离散时间段进行更新。另一个叫FixedUpdate的函数在每次物理更新之前调用。由于物理更新和帧更新并不使用相同的频率更新,如果将上面的代码放到FixedUpdate函数而不是Update函数的话,可以得到更加精确的结果(FixedUpdate会以一个比Update更加稳定的帧率运行)。

void FixedUpdate() {
Vector3 force = transform.forward * driveForce * Input.GetAxis("Vertical");
rigidbody.AddForce(force);
}

同时在场景中所有对象的Update和FixedUpdate调用之后,以及所有的动画计算之后仍然可能有需要进行一些额外的改变。一个例子就是在一个摄像头需要监视一个目标对象的时候,对摄像机朝向的调整必须在目标对象已经移动之后。另一个例子是在在需要用脚本代码来覆盖动画效果的场合(比如,使角色的头看向场景中的目标对象)。LateUpdate函数可以用于这些场合。


<2>  初始化事件

在游戏中的任何更新函数之前调用初始化代码是经常用到的,初始化函数Start在物体的第一帧更新或者物理更新之前被调用。Awake函数在场景被加载的时候,场景中的每个对象上的该函数都会被调用,所有的Awake函数都会在第一个Start函数(有好多对象的话就有好多个Start函数)调用之前被调用。这就意味着Start函数中的代码也可以使用发生在Awake阶段里面的初始化后的数据。


<3> GUI事件

unity有一个用于渲染GUI控件的系统,这些GUI控件处于场景之上并且响应鼠标事件。这个代码处理起来和常规的帧更新有些不大一样,因而它需要放到OnGUI函数中,周期性的被调用。

void OnGUI() {
GUI.Label(labelRect, "Game Over");
}

同时当发生鼠标单击场景中的游戏对象的时候可以检测这些消息。这个可以用于调整武器或者显示当前在鼠标下面的角色信息。一系列的OnMouseXXX事件函数可以用于和用户的鼠标行为交互。


<4>物理事件

物理引擎会通过调用物体上的事件函数来报告发生的与另一物体的碰撞事件。OnCollisionEnter、OnCollisionStay和OnCollisionExit函数分别在刚接触,持续和离开(broken)的时候调用。当碰撞器被配置成触发器的时候发生碰撞后对应的OnCollisionEnter、OnCollisionStay和OnCollisionExit会被调用。假如在物理更新期间有多于一个接触被检测到这些函数可能会被多次按顺序调用(即调用完一轮再来一轮)。


二 控制游戏对象


在unity中,可以在监视器面板中修改物体的组件属性,但是更多的时候,需要使用脚本来进行这些操作。


1 访问组件

最常见的一个情形是需要使用脚本访问附加到相同游戏对象上的另一个组件(当前脚本就是一个组件,其他的组件也就是另一个组件了)。在引言中提到过,一个组件实质上是一个类的实例,因而首先需要做的是获取想要操作的组件实例的引用。这个通过GetComponent函数来实现。典型的,可能会想要将一个组件赋值给一个变量,如下代码所示

void Start () {
Rigidbody rb = GetComponent<Rigidbody>();
}

一旦获取了组件实例的引用,就可以对它的属性进行想要的操作,同时也可以调用它之上的一些功能函数。

如果想要访问另一个的脚本文件,也可以使用GetComponent,只需使用脚本的类名作为该函数的组件类型参数(因为脚本本来就也是一个组件)。

如果想要去获取一个并没有添加到当前游戏对象的组件,GetComponent函数会返回null,如果试图去改变一个null对象上的任何值,将会发生null引用错误。

由于一些组件类型经常使用,unity提供了一些内置的变量来访问它们,例如可以使用下面的代码

void Start () {
transform.position = Vector3.zero;
}


而不用使用GetComponent去获得Transform组件,所有的内置组件变量列表在MonoBehaviour类的参考手册里面有。


2 访问其他对象

虽然游戏对象有的时候都是各自处理,使用代码进行跟踪其他物体是常有的事。例如,一个追赶的敌人可能需要知道玩家的位置,unity提供了一系列不同的方法来获取其他对象,各适合不同的场合。


<1> 将对象链接到变量

最直接的办法是将一个游戏对象添加到脚本的公有成员变量上,直接在编辑器中将需要访问的游戏对象拖到对应脚本组件的那个公有成员变量上,unity会自动根据变量的类型将添加的游戏对象中相同的组件类型映射到该变量。

例如将一个游戏对象拖给一个Transform的成员变量,就会自动的将游戏对象的Transform组件和该变量映射起来。

直接将对象和变量链接起来在处理需要有永久链接的对象的时候是最有用的方法。同时也可以使用一个数组变量和几个相同类型的对象链接起来,但是这种链接必须在unity编辑器中完成,而不能在运行时进行。通常使用下面的两种方法来在运行时定位对象。


<2> 查找子物体

有的时候,一个游戏场景中可能会用到很多同一类型的对象,例如敌人、路点(waypoints)和障碍物。这些对象在游戏中需要由一个特定的脚本来监视和响应。这个时候使用变量来链接这些对象太过麻烦不好操作。对于这种情况,通常更好的方法是将一系列的对象添加到一个父对象下面,这些子对象可以通过使用父对象的Transfrom组件来获得。

public class WaypointManager : MonoBehaviour {
public Transform waypoints;

void Start() {
waypoints = new Transform[transform.childCount];
int i = 0;

for (Transform t in transform) {
waypoints[i++] = t;
}
}
}



同时也可以使用Tranfrom.Find来查找某个具体的子对象。使用Transform来进行对象查找操作是因为每一个游戏对象都有Transfrom组件。


<3> 通过名称或标签访问对象

只要有一些信息,在层级场景中的任何位置定位到该游戏对象是可能的。单个对象可以通过GameObject.Find函数进行查找。如下:

GameObject player;

void Start() {
player = GameObject.Find("MainHeroCharacter");
}


某个对象或者一系列的对象也可以分别通过GameObject.FindWithTag和GameObject.FindObjectsWidthTag函数进行定位。


<4>查找特定类型的对象

staticObjectFindObjectOfType(Type type)

返回指定类型对象中的第一个活动的加载对象

需要注意的是这个函数很慢(可能是由于要在整个场景中进行遍历),不推荐每一帧都使用这个函数,在大多数情况下可以使用单件模式

例如

Camera cam = FindObjectOfType(typeof(Camera)) as Camera;

由于该函数返回的类型是Object,所以需要使用as进行一下强制转换。


static Object[] FindObjectsOfType(Type type);

返回指定类型的加载活动对象的列表,速度也慢

HingeJoint[] hinges = FindObjectsOfType(typeof(HingeJoint)) as HingeJoint[];



三 创建和销毁对象

在运行时创建和销毁对象是常有的事。在unity中,可以使用Instantiate函数对现有的一个对象做一个拷贝来创建一个新的游戏对象。

public GameObject enemy;

void Start() {
for (int i = 0; i < 5; i++) {
Instantiate(enemy);
}
}

值得注意的是用于进行拷贝的对象并不一定需要放置在场景中。更普遍的做法是将一个预设(Prefab)拖到脚本的对应公有成员变量上,实例化的时候直接对这个成员变量进行实例化即可。


同时也有一个Destroy函数在帧更新函数完成后或设定的一个延时时间后销毁一个对象。

void OnCollisionEnter(otherObj: Collision) {
if (otherObj == "Missile") {
Destroy(gameObject,.5f);
}
}

注意到Destroy函数可以销毁单独的组件而不对游戏对象本身产生影响,一个通常范的错误是

Destroy(this);

这句代码仅仅效果脚本组件,而不会销毁该脚本所附加在的对象


四 协程(Coroutines)

当调用一个函数的时候,它会在返回前运行到结束位置。

根据前面的知识可以知道,几乎所有的函数都由unity在帧更新的时候调用一次,然后到下一帧后再调用一次,那这个时候在函数中写一些循环体类的代码想要达到产生动画的效果可能会失败。如下

void Fade() {
for (float f = 1f; f <= 0; f -= 0.1f) {
Color c = renderer.material.color;
c.alpha = f;
renderer.material.color = c;
}
}


这是想要产生一个透明渐变的效果,但是由于Fade函数会在一个frame update时间片中全部执行完,达不到渐变效果。要达到这个效果,可以在帧更新函数中进行处理即可。但是,对于这类情形更方便的做法是使用coroutine。

一个coroutine就像一个可以暂停执行并将控制权返回给unity的函数,但是在下一帧的时候又可以在它停止的位置继续执行。在C#中,这样声明一个coroutine:

IEnumerator Fade() {
for (float f = 1f; f <= 0; f -= 0.1f) {
Color c = renderer.material.color;
c.alpha = f;
renderer.material.color = c;
yield return;
}
}


实质上它是一个返回类型为IEnumerator的函数,同时在函数体中增加了yield return这句代码。yield return这行就是会在执行的时候暂停、在下一帧的时候恢复执行的位置。要启动coroutine,需要使用StartCorutine函数。

void Update() {
if (Input.GetKeyDown("f")) {
StartCoroutine("Fade");
}
}


默认的情况下,一个coroutine在它暂停后的下一帧恢复,但是也可以使用WaitFroSeconds来引入一个延时。

IEnumerator Fade() {
for (float f = 1f; f <= 0; f -= 0.1f) {
Color c = renderer.material.color;
c.alpha = f;
renderer.material.color = c;
yield return new WaitForSeconds(.1f);
}
}


这个可以用于产生一个随时间变化的效果,同时也是一个用于进行优化的有效方法。游戏中的许多人物需要周期性的执行,最常用的做法是将它们包含在Update函数中。但是Update函数通常每秒会调用多次。当有的任务并不需要这么频繁的被调用的时候,可以将它放在一个coroutine中按一定时间进行调用,而不是每一帧都调用。一个这样的例子就是用于在游戏中如果有敌人接近的话就提示玩家,代码如下

function ProximityCheck() {
for (int i = 0; i < enemies.Length; i++) {
if (Vector3.Distance(transform.position, enemies[i].transform.position) < dangerDistance) {
return true;
}
}

return false;
}
IEnumerator DoCheck() {
for(;;) {
ProximityCheck;
yield return new WaitForSeconds(.1f);
}
}


当有很多敌人的时候,使用coroutine0.1秒执行一次靠近检查,可以减少大量的计算量。


没有更多推荐了,返回首页