Unity3D研究院之一张图彻底了解Unity脚本的生命周期


 

以前没好好看Unity的脚本手册,原来写的是这么的全。。。尤其起yield  www  协同 这部分看了这张图彻底的懂了。。。

http://docs.unity3d.com/Manual/ExecutionOrder.html



我们可以做一个小实验来测试一下: 
在Hierarchy视图中创建三个游戏对象,在Project视图中创建三条脚本,如下图所示,然后按照顺序将脚本绑定到对应的游戏对象上: 
struct 

三条脚本的代码完全一样,只是做了一点名称上的区分: 

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
using  UnityEngine;
using  System.Collections;
public  class  Scring0 : MonoBehaviour
{
     void  Awake()
     {
         Debug.Log( "Script0 ======= Awake" );
     }
 
     bool  isUpdate = false ;
     void  Update()
     {
         if (!isUpdate)
         {
             Debug.Log( "Script0 ======= Update" );
             isUpdate = true ;
         }
     }
 
     bool  isLateUpdate = false ;
     void  LateUpdate()
     {
         if (!isLateUpdate)
         {
             Debug.Log( "Script0 ======= LateUpdate" );
             isLateUpdate = true ;
         }
     }
}

播放游戏,看看它们的执行顺序。如下图所示,Awake、Update、LateUpdate,无论运行游戏多少次,它们的执行顺序是完全一样的。 
exeOrderInstance01


 

接着我们再做一个测试,把Script0的Update方法注释掉!! 

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
using  UnityEngine;
using  System.Collections;
public  class  Script0 : MonoBehaviour
{
  
     void  Awake ()
     {
         Debug.Log( "Script0 ========= Awake" );
     }
  
//  bool isUpdate = false;
//  void Update ()
//  {
//      if(!isUpdate)
//      {
//          Debug.Log("Script0 ========= Update");
//          isUpdate = true;
//      }
//  }
  
     bool  isLateUpdate = false ;
     void  LateUpdate()
     {
         if (!isLateUpdate)
         {
             Debug.Log( "Script0 ========= LateUpdate" );
             isLateUpdate = true ;
         }
     }
}

再次运行游戏,看看它的结果。脚本的执行顺序和以前完全一样,Script0即便删除掉了Update方法,但是它也不会直接执行LateUpdate方法,而是等待Script1和Script2中的Update方法都执行完毕以后,再去执行所有的LateUpdate方法。 
exeOrderInstance02 



通过这两个例子,我们就可以很清楚地断定,Unity后台是如何执行脚本的了。每个脚本的Awake、Start、Update、LateUpdate、FixedUpdate等等,所有的方法在后台都会被汇总到一起: 

1
2
3
4
5
6
7
后台的Awake()
{
     // 这里暂时按照上图中的脚本执行顺序,后面会谈到其实可以自定义该顺序的
     脚本2中的Awake();
     脚本1中的Awake();
     脚本0中的Awake();
}

后台的方法Awake、Update、LateUpdate等等,都是按照顺序,等所有游戏对象上脚本中的Awake执行完毕之后,再去执行Start、Update、LateUpdate等方法的。 

1
2
3
4
5
6
7
后台的Update()
{
     // 这里暂时按照上图中的脚本执行顺序,后面会谈到其实可以自定义该顺序的
     脚本2中的Update();
     脚本1中的Update();
     脚本0中的Update();
}

脚本的执行顺序

然后我们来看看这样一种情况:在脚本0的Awake方法中创建一个立方体对象,然后在脚本2的Awake方法中去获取这个立方体对象。代码如下: 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Script0.cs
using  UnityEngine;
using  System.Collections;
public  class  Script0 : MonoBehaviour
{
     void  Awake ()
     {
         GameObject.CreatePrimitive(PrimitiveType.Cube);
     }
}
 
// Script2.cs
using  UnityEngine;
using  System.Collections;
public  class  Script2 : MonoBehaviour
{
     void  Awake ()
     {
         GameObject go = GameObject.Find( "Cube" );
         Debug.Log(go.name);
     }
}

如果脚本的执行顺序是先执行Script0,然后再执行Script2,那么Script2中的Awake就可以正确地获取到该立方体对象;可是如果脚本的执行顺序是先执行Script2,然后是Script0,那么Script2肯定会报空指针错误的。

那么实际项目中的脚本会非常多,它们的先后执行顺序我们谁也不知道(有人说是按照栈结构来执行的,即后绑定到游戏对象上的脚本先执行。这一点可以从上面的例子中得到,但官方并没有这么说,还得进一步深入研究)。但一般的,建议在Awake方法中创建游戏对象或Resources.Load(Prefab)对象,然后在Start方法中去获取游戏对象或者组件,因为事件函数的执行顺序是固定的,这样就可以确保万无一失了。 
另外,Unity也提供了一个方法来设置脚本的执行顺序,在Edit -> Project Settings -> Script Execution Order菜单项中,可以在Inspector面板中看到如下图所示: 
Inspector

点击右下角的"+"将弹出下拉窗口,包括游戏中的所有脚本。脚本添加完毕后,可以用鼠标拖动脚本来为脚本排序,脚本名后面的数字也越小,脚本越靠上,也就越先执行。其中的Default Time表示没有设置脚本的执行顺序的那些脚本的执行顺序。 
Inspector2

按照上面这张图的设置,我们再来看一下控制台的输出结果,来确认一下我们的设置是否起作用(注意:把Script0脚本中的Update方法取消注释): 
exeOrderInstance03


脚本的编译顺序

关于脚本的编译顺序很是头疼,官方的说法有点模糊,请看官方的解释: 
phaseFour

由于脚本的编译顺序会涉及到特殊文件夹,比如上面提到的Plugins、Editor还有Standard Assets等标准的资源文件夹,所以脚本的放置位置就非常重要了。下面用一个例子来说明不同文件夹中的脚本的编译顺序:

fileStruct

实际上,如果你细心的话会发现,如果在你的项目中建立如上图所示的文件夹层次结构时,编译项目之后会在项目文件夹中生成一些文件名中包含Editor、firstpass这些字样的项目文件。比如按照上图的文件夹结构,我们打开项目文件夹来看一下产生的项目文件是什么样的? 
fileStruct01

下面就来详细探讨一下这些个字样是什么意思?它们与脚本的编译顺序有着怎样的联系?


1、首先从脚本语言类型来看,Unity3d支持3种脚本语言,都会被编译成CLI的DLL

如果项目中包含有C#脚本,那么Unity3d会产生以Assembly-CSharp为前缀的工程,名字中包含”vs”的是产生给Vistual Studio使用的,不包含”vs”的是产生给MonoDevelop使用的。 

项目中的脚本语言工程前缀工程后缀
C#Assembly-CSharpcsproj
UnityScriptAssembly-UnityScriptunityproj
BooAssembly-Boobooproj

 

如果项目中这三种脚本都存在,那么Unity将会生成3种前缀类型的工程。

2、对于每一种脚本语言,根据脚本放置的位置(其实也部分根据脚本的作用,比如编辑器扩展脚本,就必须放在Editor文件夹下),Unity会生成4中后缀的工程。其中的firstpass表示先编译,Editor表示放在Editor文件夹下的脚本。

在上面的示例中,我们得到了两套项目工程文件:分别被Virtual Studio和MonoDevelop使用(后缀包不包含vs),为简单起见,我们只分析vs项目。得到的文件列表如下: 
Assembly-CSharp-filepass-vs.csproj 
Assembly-CSharp-Editor-filepass-vs.csproj 
Assembly-CSharp-vs.csproj 
Assembly-CSharp-Editor-vs.csproj

根据官方的解释,它们的编译顺序如下: 
(1)所有在Standard Assets、Pro Standard Assets或者Plugins文件夹中的脚本会产生一个Assembly-CSharp-filepass-vs.csproj文件,并且先编译; 
(2)所有在Standard Assets/Editor、Pro Standard Assets/Editor或者Plugins/Editor文件夹中的脚本产生Assembly-CSharp-Editor-filepass-vs.csproj工程文件,接着编译; 
(3)所有在Assets/Editor外面的,并且不在(1),(2)中的脚本文件(一般这些脚本就是我们自己写的非编辑器扩展脚本)会产生Assembly-CSharp-vs.csproj工程文件,被编译; 
(4)所有在Assets/Editor中的脚本产生一个Assembly-CSharp-Editor-vs.csproj工程文件,被编译。

之所以按照这样建立工程并按此顺序编译,也是因为DLL间存在的依赖关系所决定的。

好了,到此为止,我们可以很容易地判断出上面举的实例中,脚本的编译顺序(实际上,我已经把顺序写在了脚本的文件名中了)

小练习

一个Unity3d的工程中,最多可以产生多少个工程文件呢?

4*3*2=24

写在最后的话

以上内容参考的文章有: 
1. Unity3D研究院之深入理解Unity脚本的执行顺序(六十二) 
2. 全面解析Unity3D自动生成的脚本工程文件

 

工程引用关系:

转载:http://www.cnblogs.com/wonderKK/p/4114447.html

<think>我们有一个需求:在Unity UGUI界面中截取指定Image区域并获取Texture2D对象。 思路: 1. 我们需要知道要截取的是哪个Image区域。这个Image组件可能是一个RawImage或者普通的Image(显示Sprite)。 2. 截取屏幕上的某个区域,需要知道这个区域在屏幕上的位置和大小。 3. 然后我们可以通过读取屏幕像素来获取这个区域的图像数据,并创建Texture2D。 但是,注意:UGUI的Image可能被其他元素遮挡,或者有透明部分,我们截取的是屏幕实际显示的区域(包括覆盖在上面的其他UI元素)?还是只想要Image本身的纹理? 根据问题描述,我们想要截取的是“指定Image区域”,通常是指这个Image在屏幕上显示的矩形区域内的内容(包括它上面覆盖的其他UI,但通常我们可能只想要这个Image本身的纹理,而不包括覆盖在它上面的其他UI?) 这里有两种情况: 情况一:只想要Image本身的纹理(不包含覆盖在它上面的其他UI元素)。这种情况我们需要直接访问Image的纹理数据,而不是从屏幕读取。 情况二:想要屏幕上实际显示的区域(包括覆盖在上面的UI)。这种情况需要从屏幕读取。 问题中并没有明确,但根据“截取指定Image区域”的描述,通常是指情况一,即获取Image本身显示的纹理(不包含覆盖在上面的其他UI)。但是,如果Image是RawImage,它可能显示的是RenderTexture,而RenderTexture可能是动态渲染的(比如相机渲染的画面),那么情况就复杂了。 然而,问题描述中引用的内容提到了RawImage和Image,并且引用[2]中有一个将RawImage的Texture转为Texture2D的方法,但那个方法实际上是创建了一个RenderTexture来复制纹理,然后读取像素。这种方法适用于任意Texture(包括RenderTexture),但前提是我们要获取整个纹理,而不是截取其中的一部分。 但是,我们的需求是截取Image的“指定区域”,也就是说,可能只需要这个Image纹理的一部分。 所以,我们需要分步骤: 步骤1:获取Image组件(可能是Image或RawImage)的纹理(Texture)对象。 步骤2:确定要截取的区域(相对于纹理的哪个矩形区域)。 步骤3:从这个纹理中提取指定区域的像素数据,创建新的Texture2D。 但是,注意:Image组件可能使用了九宫格(Sliced)、平铺(Tiled)或填充(Filled)等模式,这些模式改变了纹理的显示方式。但我们的需求是获取纹理本身的一部分,而不是显示效果。所以,我们通常不考虑显示模式,而是直接对原始纹理进行操作。 然而,如果Image显示的是Sprite,那么纹理可能是图集的一部分。所以,我们还需要考虑Sprite的纹理区域。 因此,我们需要: 1. 获取Image组件上的纹理(对于Image,是sprite.texture;对于RawImage,是texture)。 2. 获取这个纹理上的一个矩形区域(相对于纹理的像素坐标)。 但是,如何确定要截取的区域?问题中说是“指定区域”,这个区域应该是相对于Image的纹理的。所以,我们需要用户提供这个区域(比如相对于纹理的左上角为原点的矩形)。 然而,问题没有明确指定区域的方式。我们可以假设用户会提供一个Rect,表示在纹理上的像素坐标和大小。 但是,还有一种常见需求:截取整个Image的纹理(即整个纹理)。那么我们可以将整个纹理复制出来。 所以,我们可以写一个方法,传入Image(或RawImage)组件和一个Rect(表示要截取的部分),返回一个Texture2D。 注意:纹理可能是不可读的(readable=false),所以我们需要使用类似引用[2]中的方法,通过RenderTexture来复制纹理,使其可读。 步骤: 1. 获取原始纹理(sourceTexture)。 2. 创建一个临时的RenderTexture,大小与原始纹理相同(或者与要截取的区域相同?但为了操作方便,我们先创建和原始纹理一样大的)。 3. 使用Graphics.Blit将原始纹理复制到RenderTexture。 4. 激活这个RenderTexture,然后创建一个新的Texture2D(大小与要截取的区域相同),使用ReadPixels读取RenderTexture上的指定区域(注意:ReadPixels的坐标系是左下角为原点,而我们的Rect可能是左上角为原点,需要转换?但这里我们传入的Rect是相对于纹理的,纹理的坐标系是(0,0)在左下角,但是我们在处理Rect时,通常以左上角为原点,所以需要转换y坐标:纹理高度减去y再减去高度,得到左下角为原点的y坐标?) 5. 将读取的像素数据应用到Texture2D,并返回。 但是,注意:我们也可以不复制整个纹理,而是只复制我们需要的区域。但是,Graphics.Blit只能复制整个纹理。所以我们可以先复制整个纹理到RenderTexture,然后只读取需要的区域。 另外,对于Image组件,如果它显示的是Sprite,那么Sprite可能只使用了纹理的一部分(有纹理矩形)。所以,我们需要考虑Sprite的纹理矩形。但是,问题中要求的是截取指定图片区域,这个区域应该是相对于整个纹理的,还是相对于Sprite的纹理矩形的?这需要明确。 为了通用性,我们可以这样设计: 方法1:截取整个纹理(忽略Sprite的纹理矩形),直接使用原始纹理。 方法2:如果用户想要截取的是Sprite的纹理矩形内的区域,那么我们需要先获取Sprite的纹理矩形(sprite.rect),然后在这个矩形内再截取指定区域。 但是,问题没有明确,所以我们先提供截取整个纹理上指定区域的功能。如果用户想要相对于Sprite纹理矩形的区域,那么需要自己计算出在原始纹理上的区域(将sprite.rect的位置加上要截取的位置)。 因此,我们可以写一个工具方法,传入一个Texture(原始纹理)和一个Rect(要截取的区域,相对于原始纹理,以左上角为原点),返回Texture2D。 注意:Rect的坐标系问题。在Unity中,纹理的(0,0)在左下角,而我们在处理图片时通常以左上角为原点。所以,我们需要将传入的Rect(左上角原点)转换成纹理坐标系(左下角原点)的Rect。 转换方式:假设纹理高度为height,传入的Rect为(x, y, width, height),那么转换后的y坐标为:height - (y + height) = height - y - height = -y? 这显然不对。实际上,应该是:y_new = sourceTexture.height - rect.y - rect.height。 所以,在读取像素时,我们设置的矩形为:new Rect(rect.x, sourceTexture.height - rect.y - rect.height, rect.width, rect.height) ?但是,这样可能会超出范围。而且,我们传入的rect.y是相对于左上角的,所以需要这样转换。 但是,我们也可以不转换,而是将RenderTexture先渲染出来,然后读取时调整y坐标。不过,我们使用RenderTexture.active后,ReadPixels的坐标系是RenderTexture的坐标系,即左下角为原点。 因此,步骤: 1. 获取原始纹理(sourceTexture)和要截取的区域(rect,以左上角为原点)。 2. 创建一个RenderTexture,大小为sourceTexture的宽高。 3. 将sourceTexture通过Graphics.Blit复制到RenderTexture。 4. 设置当前激活的RenderTexture为这个临时RT。 5. 创建一个新的Texture2D,大小为rect的宽高(注意:rect的宽高必须是整数,可能需要取整)。 6. 读取像素:readableText.ReadPixels(new Rect(rect.x, sourceTexture.height - rect.y - rect.height, rect.width, rect.height), 0, 0); 解释:这里x坐标不变,y坐标转换为从底部开始计算(因为原始纹理的底部是0)。所以,我们截取的区域在纹理坐标系中,y坐标从(纹理高度-(y+高度))开始,高度不变。 7. 应用这些像素:readableText.Apply(); 8. 清理:恢复之前的RenderTexture.active,并释放临时RT。 但是,注意:如果传入的区域超出了纹理范围,需要处理。这里我们假设传入的区域在纹理范围内。 另外,对于Image组件,我们如何获取纹理? 如果是Image组件,我们通过image.sprite.texture获取,但要注意sprite可能为null。 如果是RawImage组件,通过rawImage.texture获取。 所以,我们可以写一个通用的方法,传入Texture和一个Rect,返回Texture2D。 但是,问题要求的是截取指定Image区域,所以我们可以提供一个方法,传入Image组件(或RawImage)和Rect,然后获取它的纹理,再调用上面的方法。 但是,Image组件(UGUI的Image)显示的是Sprite,而Sprite的纹理可能是一个图集,所以纹理可能很大。我们截取的是整个图集上的区域,而不是Sprite的rect区域?这取决于用户的需求。 因此,我们可以提供两种方法: 方法A:对于Image组件,截取整个纹理(即sprite.texture)上的指定区域。 方法B:对于Image组件,截取Sprite区域(sprite.rect)内的指定区域(即相对sprite.rect的左上角)。 我们这里先实现方法A(截取整个纹理上的指定区域)。如果用户需要相对Sprite.rect的区域,那么可以这样计算:最终截取区域 = new Rect(sprite.rect.x + rect.x, sprite.rect.y + rect.y, rect.width, rect.height) 所以,我们可以写一个工具方法,然后根据组件类型获取纹理。 下面我们编写代码: 注意:需要处理纹理不可读的情况,所以我们使用RenderTexture来复制。 步骤: 1. 获取原始纹理(sourceTexture)和要截取的区域(rect)。 2. 创建临时RenderTexture,格式和原始纹理一致(或者使用默认格式)。 3. 使用Graphics.Blit复制。 4. 读取像素。 代码实现: 我们写一个静态方法: ``` public static Texture2D CaptureTextureRegion(Texture sourceTexture, Rect region) { // 检查参数 if (sourceTexture == null) { Debug.LogError("Source texture is null!"); return null; } // 确保区域在纹理范围内 region.x = Mathf.Clamp(region.x, 0, sourceTexture.width); region.y = Mathf.Clamp(region.y, 0, sourceTexture.height); region.width = Mathf.Clamp(region.width, 0, sourceTexture.width - region.x); region.height = Mathf.Clamp(region.height, 0, sourceTexture.height - region.y); // 如果区域宽或高为0,则返回一个1x1的透明纹理 if (region.width <= 0 || region.height <= 0) { Debug.LogError("Region width or height is zero!"); return null; } // 创建临时RenderTexture RenderTexture rt = RenderTexture.GetTemporary( sourceTexture.width, sourceTexture.height, 0, // 深度 RenderTextureFormat.Default, // 格式 RenderTextureReadWrite.Default // 颜色空间 ); // 保存当前激活的RenderTexture RenderTexture previous = RenderTexture.active; // 将原始纹理复制到临时RenderTexture Graphics.Blit(sourceTexture, rt); // 设置当前激活的RenderTexture RenderTexture.active = rt; // 创建一个新的Texture2D,用于存储截取的区域 Texture2D tex = new Texture2D((int)region.width, (int)region.height, TextureFormat.RGBA32, false); try { // 计算在RenderTexture上的读取区域(注意坐标系转换:region的y是从上往下的,而RenderTexture的y是从下往上的) float readY = sourceTexture.height - region.y - region.height; Rect readRegion = new Rect(region.x, readY, region.width, region.height); // 读取像素 tex.ReadPixels(readRegion, 0, 0); tex.Apply(); } finally { // 恢复之前的RenderTexture RenderTexture.active = previous; // 释放临时RenderTexture RenderTexture.ReleaseTemporary(rt); } return tex; } ``` 然后,针对Image和RawImage组件,我们可以这样调用: 对于RawImage: ``` Texture sourceTexture = rawImage.texture; // 然后调用CaptureTextureRegion,并传入指定的区域 ``` 对于Image: ``` if (image.sprite != null) { Texture sourceTexture = image.sprite.texture; // 然后调用CaptureTextureRegion,并传入指定的区域 } else { Debug.LogError("Image has no sprite!"); } ``` 但是,注意:Image组件的纹理可能是一个图集,而我们截取的是整个图集上的区域。如果用户想要截取的是Sprite的原始区域,那么应该传入的区域是相对于sprite.rect的。或者,我们可以先获取sprite.rect,然后在这个rect内截取。 另外,如果用户想要截取整个Image的纹理(整个Sprite区域),那么可以直接传入sprite.rect作为区域。 所以,我们可以再提供一个方法,直接截取整个Sprite的纹理(即sprite.rect区域): ``` public static Texture2D CaptureSpriteTexture(Image image) { if (image.sprite == null) return null; Rect region = image.sprite.rect; return CaptureTextureRegion(image.sprite.texture, region); } ``` 但是,注意:sprite.rect是相对于图集的,所以这样截取出来的是图集上的一块。 所以,根据需求,我们可以灵活调用。 回到问题:用户想要截取指定Image区域(指定区域)。所以,我们主要提供CaptureTextureRegion方法,并让用户传入区域。 但是,如果用户传入的区域是相对于Image显示区域的(比如在屏幕上的位置),那又不同。但问题描述是“指定图片区域”,所以应该是相对于图片纹理的。 因此,我们提供上述方法。 注意:性能方面,每次截取都会创建RenderTexture和Texture2D,所以要注意在不需要时销毁。 另外,如果原始纹理已经是可读的(Read/Write enabled),我们可以不使用RenderTexture,而直接使用Texture2D.GetPixels。但是,我们无法知道纹理是否可读,所以使用RenderTexture是通用的方法。 总结: 1. 编写CaptureTextureRegion方法,用于截取任意纹理的指定区域(区域以纹理左上角为原点)。 2. 对于RawImage,直接使用rawImage.texture作为源纹理。 3. 对于Image,使用image.sprite.texture作为源纹理,然后用户传入的区域可以是相对于整个纹理的,或者是相对于sprite.rect的(需要用户自己计算)。 示例代码: 假设我们有一个RawImage,我们想截取它的中间部分(比如从(100,100)开始,宽200,高200): ``` Texture2D tex = CaptureTextureRegion(rawImage.texture, new Rect(100, 100, 200, 200)); ``` 对于Image,如果我们想截取整个Sprite: ``` Texture2D tex = CaptureTextureRegion(image.sprite.texture, image.sprite.rect); ``` 或者,如果我们想截取Sprite的一部分(比如Sprite的左上角一半): ``` Rect spriteRect = image.sprite.rect; Rect region = new Rect(spriteRect.x, spriteRect.y, spriteRect.width/2, spriteRect.height/2); Texture2D tex = CaptureTextureRegion(image.sprite.texture, region); ``` 这样,我们就实现了需求。 但是,注意:如果Image的纹理是压缩格式(比如安卓上的ETC2),那么直接读取可能会失败,所以使用RenderTexture来复制是安全的。 最后,我们还需要注意内存管理,创建的Texture2D需要由调用者负责销毁(使用Resources.UnloadAsset或Destroy)。 另外,如果纹理大小很大,创建RenderTexture可能会有性能问题,所以要注意使用。 希望这个回答对你有帮助。 §§相关问题§§ 1. 如何将Texture2D保存为PNG文件? 2. Unity中如何获取UI元素在屏幕上的位置和大小? 3. 如何实现屏幕截图(包括UI)? 4. 如何将Texture2D显示在RawImage上?</think>在Unity UGUI中截取指定Image区域并获取Texture2D对象,可以通过以下步骤实现: ### 解决方案步骤 1. **获取Image的屏幕区域** 计算目标Image在屏幕空间中的矩形区域: ```csharp RectTransform rectTransform = targetImage.GetComponent<RectTransform>(); Vector3[] corners = new Vector3[4]; rectTransform.GetWorldCorners(corners); // 获取UI的世界坐标角点 // 转换为屏幕坐标 Vector2 min = RectTransformUtility.WorldToScreenPoint(null, corners[0]); Vector2 max = RectTransformUtility.WorldToScreenPoint(null, corners[2]); Rect screenRect = new Rect(min.x, Screen.height - max.y, max.x - min.x, max.y - min.y); ``` 2. **创建临时RenderTexture** 使用RenderTexture临时存储屏幕内容: ```csharp RenderTexture rt = new RenderTexture((int)screenRect.width, (int)screenRect.height, 24); Camera.main.targetTexture = rt; // 关联主相机 Camera.main.Render(); // 渲染当前帧 ``` 3. **截取区域并生成Texture2D** 读取像素数据并创建Texture2D: ```csharp Texture2D tex = new Texture2D((int)screenRect.width, (int)screenRect.height, TextureFormat.RGBA32, false); RenderTexture.active = rt; tex.ReadPixels(new Rect(0, 0, screenRect.width, screenRect.height), 0, 0); // 左下角为原点 tex.Apply(); ``` 4. **清理资源** ```csharp RenderTexture.active = null; Camera.main.targetTexture = null; Destroy(rt); ``` ### 完整代码示例 ```csharp using UnityEngine; using UnityEngine.UI; public class ImageCapture : MonoBehaviour { public Image targetImage; // 拖入目标Image组件 public Texture2D CaptureImageRegion() { // 1. 获取屏幕区域 RectTransform rt = targetImage.GetComponent<RectTransform>(); Vector3[] corners = new Vector3[4]; rt.GetWorldCorners(corners); Vector2 min = RectTransformUtility.WorldToScreenPoint(null, corners[0]); Vector2 max = RectTransformUtility.WorldToScreenPoint(null, corners[2]); Rect screenRect = new Rect(min.x, Screen.height - max.y, max.x - min.x, max.y - min.y); // 2. 创建临时RenderTexture RenderTexture renderTex = RenderTexture.GetTemporary( (int)screenRect.width, (int)screenRect.height, 24 ); // 3. 渲染到Texture Camera.main.targetTexture = renderTex; Camera.main.Render(); // 4. 读取像素数据 Texture2D tex = new Texture2D((int)screenRect.width, (int)screenRect.height, TextureFormat.RGBA32, false); RenderTexture.active = renderTex; tex.ReadPixels(new Rect(0, 0, screenRect.width, screenRect.height), 0, 0); tex.Apply(); // 5. 清理资源 Camera.main.targetTexture = null; RenderTexture.active = null; RenderTexture.ReleaseTemporary(renderTex); return tex; } } ``` ### 关键注意事项 1. **坐标系转换** Unity屏幕坐标系原点在左下角,而UI的`GetWorldCorners`返回的坐标需通过`Screen.height - y`转换[^5]。 2. **性能优化** - 避免每帧调用:此操作较消耗资源,建议在需要时触发 - 使用`RenderTexture.GetTemporary`代替`new RenderTexture()`减少内存分配 3. **纹理格式** `TextureFormat.RGBA32`支持透明通道,如需压缩可后续调用`tex.Compress(true)` 4. **遮挡处理** 此方法会捕获屏幕上实际显示的内容(包括覆盖在Image上方的其他UI元素),若需原始纹理需直接访问Image组件的源纹理[^3]。 ### 应用场景 - 动态生成UI元素的缩略图 - 实现截图分享功能 - 创建UI动画序列帧 - 游戏内画图工具保存局部区域 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值