unity3d编辑器和插件制作(一)

2015年02月01日 11:03 0 点赞 0 评论 更新于 2025-11-21 15:55

今天,我们将系统地探讨Unity3D编辑器和插件制作。内容较长,若想学习,还需耐心阅读。

1. 基础图形绘制原理

1.1 从移动平台控件到Unity图形

在iOS和Android开发中,控件多以四边形为基础。在Unity里,我们可以依据图形绘制的“三点一面”理论,使用6个点构建由两个三角形组成的四边形。

1.2 绘制简单面

首先,我们要学会绘制一个简单的面。在Unity中,每绘制一个面就会产生一个Draw Call。为了节省绘制开销,我们可以利用面合并原理减少Draw Call的产生(OpenGL会自动处理)。

以下是定义六个点来构建三角形的代码:

public static int[] initTri()
{
int[] triangle = new int[6];
triangle[0] = 0;
triangle[1] = 2;
triangle[2] = 1;
triangle[3] = 2;
triangle[4] = 3;
triangle[5] = 1;
return triangle;
}

接着,我们定义三角形的大小,这里涉及到锚点(ancPointxancPointy)以及面的宽和高(widthheight):

public static Vector3[] initVertice(float width, float height, float ancPointx, float ancPointY)
{
Vector3[] viewVer = new Vector3[4];
viewVer[0] = new Vector3(-ancPointx * -width, -ancPointY * height, 0);
viewVer[1] = new Vector3((1 - ancPointx) * -width, -ancPointY * height, 0);
viewVer[2] = new Vector3(-ancPointx * -width, (1 - ancPointY) * height, 0);
viewVer[3] = new Vector3((1 - ancPointx) * -width, (1 - ancPointY) * height, 0);
return viewVer;
}

1.3 完整面的定义

为了让面具有颜色,我们还需要法线、材质和Shader。以下是法线的定义代码:

public static Vector3[] initNormal()
{
Vector3[] normals = new Vector3[4];
normals[0] = -Vector3.forward;
normals[1] = -Vector3.forward;
normals[2] = -Vector3.forward;
normals[3] = -Vector3.forward;
return normals;
}

若要在面上绘制图片,还需定义UV:

public static Vector2[] initUV()
{
Vector2[] uv = new Vector2[4];
uv[0] = new Vector2(0, 0);
uv[1] = new Vector2(1, 0);
uv[2] = new Vector2(0, 1);
uv[3] = new Vector2(1, 1);
return uv;
}

1.4 公用类代码

以下是包含上述方法的公用类 InitBase

using UnityEngine;
using System.Collections;

public class InitBase : MonoBehaviour
{
public static Vector3[] initVertice(float width, float height, float ancPointx, float ancPointY)
{
Vector3[] viewVer = new Vector3[4];
viewVer[0] = new Vector3(-ancPointx * -width, -ancPointY * height, 0);
viewVer[1] = new Vector3((1 - ancPointx) * -width, -ancPointY * height, 0);
viewVer[2] = new Vector3(-ancPointx * -width, (1 - ancPointY) * height, 0);
viewVer[3] = new Vector3((1 - ancPointx) * -width, (1 - ancPointY) * height, 0);
return viewVer;
}

public static int[] initTri()
{
int[] triangle = new int[6];
triangle[0] = 0;
triangle[1] = 2;
triangle[2] = 1;
triangle[3] = 2;
triangle[4] = 3;
triangle[5] = 1;
return triangle;
}

public static Vector3[] initNormal()
{
Vector3[] normals = new Vector3[4];
normals[0] = -Vector3.forward;
normals[1] = -Vector3.forward;
normals[2] = -Vector3.forward;
normals[3] = -Vector3.forward;
return normals;
}

public static Vector2[] initUV()
{
Vector2[] uv = new Vector2[4];
uv[0] = new Vector2(0, 0);
uv[1] = new Vector2(1, 0);
uv[2] = new Vector2(0, 1);
uv[3] = new Vector2(1, 1);
return uv;
}
}

1.5 物体上的类 VKView

using UnityEngine;
using System.Collections;

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
[ExecuteInEditMode]
public class VKView : MonoBehaviour
{
Mesh viewMesh;
Material viewDefultMat;
bool isChange;
[HideInInspector] public float ancPointx = 0.5f, ancPointy = 0.5f;
[HideInInspector] public int width = 100, height = 100;
public string test;

// Use this for initialization
void Start()
{
viewDefultMat = new Material(Shader.Find("VK/VKViewShader"));
gameObject.GetComponent<MeshRenderer>().sharedMaterial = viewDefultMat;
viewMesh = new Mesh();
gameObject.GetComponent<MeshFilter>().mesh = viewMesh;
updateView();
}

public void updateView()
{
if (viewDefultMat != null)
{
viewMesh.vertices = InitBase.initVertice(width, height, ancPointx, ancPointy);
viewMesh.triangles = InitBase.initTri();
viewMesh.normals = InitBase.initNormal();
viewMesh.uv = InitBase.initUV();
}
}

#if UNITY_EDITOR
void OnDrawGizmosSelected()
{
Gizmos.color = Color.blue;
Gizmos.DrawWireCube(transform.position, new Vector3(width, height, 0f));
}
#endif
}

1.6 VKView 的编辑器类

using UnityEngine;
using System.Collections;
using UnityEditor;

[CustomEditor(typeof(VKView))]
[ExecuteInEditMode]
public class VKViewEditor : Editor
{
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
VKView vkView = (VKView)target;
vkView.width = EditorGUILayout.IntField("Width", vkView.width);
vkView.height = EditorGUILayout.IntField("Height", vkView.height);
vkView.test = EditorGUILayout.TextField("面板显示的名字:", vkView.test);
vkView.updateView();
EditorUtility.SetDirty(vkView);
EditorUtility.UnloadUnusedAssets();
}
}

1.7 Shader代码

Shader "VK/VKViewShader"
{
Properties
{
_Color("Main Color", Color) = (1,1,1,1)
}
SubShader
{
Pass
{
Cull Front
Color [_Color]
}
}
}

2. 在场景中绘制图片

2.1 图片属性设置

在Unity中,图片种类繁多,默认类型为 Texture,还有其他属性。有些人可能会遇到素材编译后变模糊的问题,这通常是因为未修改图片属性所致。

以下是对 Texture 模式下图片属性的分析:

  • Wrap Mode:在选择图片四方连续时会用到,也可减轻图片白边的影响。
  • Filter Mode:即图片的文件模式,包括点、两角线、三角线等,数值越高,图片质量越好,但内存占用也越高。
  • Aniso Level:目前较少使用,貌似在位图中才会用到。
  • Max Size:图片的最大支持尺寸。例如,若图片为100 * 100,选择1024也不会浪费内存,因为这里指的是最大图集支持。若图片超过1024,该值也需相应调整。
  • Format:压缩模式,是决定内存占用的关键部分,有自动压缩(最节省)、16位压缩(尚可)、真彩色(无压缩,质量高但内存占用大)三个选项。

高级选项模式是最常用的模式:

  • Non power of 2:若不想让图片变为符合2的n次方的正方形,可选择 none,以保证图片大小的合理性;否则,可选择自动适应、最大适应或最小适应。
  • Cubemap:一般不用于普通图片,常用于模型贴图。
  • Read/Write Enabled:建议勾选,以支持读写。

建议小图尽量拼成大图,合理处理UV(可使用NGUI的自动压缩图或自行压缩,后续会介绍如何制作自己的图集),大图最好不超过1024,减少面的数量并尽量合并面,以降低内存占用和Draw Call。

2.2 渲染图片到场景

要将图片渲染到场景中,只需完成两步:

  1. 更换Shader,从纯色Shader切换为带贴图的Shader。
  2. 适配图片比例,因为图片大小不固定。

以下是带贴图的Shader代码:

Shader "VK/VKTextureShader"
{
Properties
{
_Color("Main Color", COLOR) = (1,1,1,1)
_MainTex("Base (RGB) Trans (A)", 2D) = "" {}
}
SubShader
{
Tags { "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent" }
Pass
{
Blend SrcAlpha OneMinusSrcAlpha
Cull off
SetTexture [_MainTex]
{
constantColor [_Color]
Combine texture * constant
}
}
}
}

2.3 继承 VKViewVKImageView

using UnityEngine;
using System.Collections;

public class VKImageView : VKView
{
Material imgViewDefultMat;
Mesh imgViewDefultMesh;
[HideInInspector] public Texture imgViewTex, highLightedTex;
[HideInInspector] public float scale = 1;
[HideInInspector] public string info = null;
[HideInInspector] public bool highLighted = false;
[HideInInspector] public float alpha = 1;

// Use this for initialization
void Start()
{
imgViewDefultMat = new Material(Shader.Find("VK/VKTextureShader"));
imgViewDefultMesh = new Mesh();
GetComponent<MeshFilter>().mesh = imgViewDefultMesh;
GetComponent<MeshRenderer>().material = imgViewDefultMat;
updateImageView();
}

public void updateImageView()
{
if (imgViewTex != null)
{
if (!highLighted)
{
if (imgViewDefultMat != null)
imgViewDefultMat.mainTexture = imgViewTex;
if (imgViewDefultMesh != null)
imgViewDefultMesh.vertices = InitBase.initVertice(imgViewTex.width * scale, imgViewTex.height * scale, ancPointx, ancPointy);
height = imgViewTex.height;
width = imgViewTex.width;
}
else
{
if (imgViewDefultMat != null)
imgViewDefultMat.mainTexture = highLightedTex;
if (imgViewDefultMesh != null)
imgViewDefultMesh.vertices = InitBase.initVertice(highLightedTex.width * scale, highLightedTex.height * scale, ancPointx, ancPointy);
height = highLightedTex.height;
width = highLightedTex.width;
}
}
else
{
if (imgViewDefultMat != null)
imgViewDefultMat.mainTexture = null;
if (imgViewDefultMesh != null)
imgViewDefultMesh.vertices = InitBase.initVertice(width * scale, height * scale, ancPointx, ancPointy);
}

if (imgViewDefultMat != null)
{
Color newcolor = imgViewDefultMat.color;
imgViewDefultMat.color = new Color(newcolor.r, newcolor.g, newcolor.b, alpha);
}

if (imgViewDefultMesh != null)
{
imgViewDefultMesh.triangles = InitBase.initTri();
imgViewDefultMesh.normals = InitBase.initNormal();
imgViewDefultMesh.uv = InitBase.initUV();
}
}

public void switchButton()
{
// 后面会讲到,可以先删掉,这个是转化按钮来用的。
VKButton button = gameObject.AddComponent<VKButton>();
button.buttonDefultMesh = imgViewDefultMesh;
button.buttonDefultMat = imgViewDefultMat;
button.buttonTex = imgViewTex;
button.pressButtonTex = highLightedTex;
button.info = info;
button.scale = scale;
button.ancPointx = ancPointx;
button.ancPointy = ancPointy;
button.updateButton();
DestroyImmediate(GetComponent<VKImageView>());
}
}

2.4 VKImageView 的编辑器类

using UnityEngine;
using System.Collections;
using UnityEditor;

[CustomEditor(typeof(VKImageView))]
public class VKImageViewEditor : Editor
{
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
VKImageView imgView = (VKImageView)target;
imgView.imgViewTex = EditorGUILayout.ObjectField("ImageTexture", imgView.imgViewTex, typeof(Texture), true) as Texture;
imgView.highLightedTex = EditorGUILayout.ObjectField("HighLightedTex", imgView.highLightedTex, typeof(Texture), true) as Texture;
imgView.alpha = EditorGUILayout.Slider("Alpha", imgView.alpha, 0.0f, 1.0f);
imgView.highLighted = EditorGUILayout.Toggle("highLighted", imgView.highLighted);
imgView.info = EditorGUILayout.TextField("info", imgView.info);
imgView.ancPointx = EditorGUILayout.Slider("AnchorX", imgView.ancPointx, 0.0f, 1.0f);
imgView.ancPointy = EditorGUILayout.Slider("AnchorY", imgView.ancPointy, 0.0f, 1.0f);

if (imgView.imgViewTex == null)
{
imgView.width = EditorGUILayout.IntField("Width", imgView.width);
imgView.height = EditorGUILayout.IntField("Height", imgView.height);
}

GUILayout.BeginHorizontal();
if (GUILayout.Button("2X"))
{
imgView.scale = 0.5f;
}
if (GUILayout.Button("1X"))
{
imgView.scale = 1f;
}
if (GUILayout.Button("1.5X"))
{
imgView.scale = 0.75f;
}
GUILayout.EndHorizontal();

GUILayout.BeginHorizontal();
if (GUILayout.Button("ChangeName"))
{
if (imgView.imgViewTex != null)
{
imgView.name = imgView.imgViewTex.name;
}
}
if (GUILayout.Button("SwitchButton"))
{
imgView.switchButton();
}
GUILayout.EndHorizontal();

imgView.updateImageView();
if (imgView != null)
EditorUtility.SetDirty(imgView);
EditorUtility.UnloadUnusedAssets();
}
}

3. Unity点击事件检测机制

3.1 Unity事件传递方式

在Unity中,检测点击事件时,使用NGUI的开发者可能知道,NGUI采用 SendMessage 方式进行事件传递,这也是Unity中较为简便的方式。不过需要注意,若要进行大于万次的循环,该方式可能会产生延迟,但一般不会同时发送万条事件。

3.2 检测点击物体

要使用 SendMessage 传递事件,需先获取 GameObject 对象。可通过射线检测来获取点击的物体,因为Unity中的射线常用于探测,且所有可见物体都在相机之下,所以可从相机发射射线来检测物体,前提是物体要有碰撞体。

以下是部分代码,本章节结束后会提供完整代码:

Ray cameraRay;
RaycastHit hit;
Vector3 touchPos, pressOffSet;
public static GameObject touchObj = null;
public static VKCamera shareVkCamera;

void Update()
{
#if UNITY_EDITOR
if (Input.GetMouseButtonDown(0))
{
onPressDown(Input.mousePosition);
}
else if (Input.GetMouseButtonUp(0))
{
onPressUp(Input.mousePosition);
}
else if (Input.GetMouseButton(0))
{
onDrag(Input.mousePosition);
}
#endif

#if UNITY_IPHONE || UNITY_ANDROID
Touch touch;
if (Input.touchCount == 1)
{
touch = Input.GetTouch(0);
switch (touch.phase)
{
case TouchPhase.Began:
onPressDown(touch.position);
break;
case TouchPhase.Moved:
onPressUp(touch.position);
break;
case TouchPhase.Ended:
case TouchPhase.Canceled:
onDrag(touch.position);
break;
}
}
#else
if (Input.GetMouseButtonDown(0))
{
onPressDown(Input.mousePosition);
}
else if (Input.GetMouseButtonUp(0))
{
onPressUp(Input.mousePosition);
}
else if (Input.GetMouseButton(0))
{
onDrag(Input.mousePosition);
}
#endif
}

public void onPressDown(Vector2 vec)
{
touchPos = vec;
for (int i = 0; i < Camera.allCameras.Length; i++)
{
cameraRay = Camera.allCameras[i].ScreenPointToRay(touchPos);
if (Physics.Raycast(cameraRay, out hit, 9999, Camera.allCameras[i].cullingMask) && touchObj == null)
{
touchObj = hit.transform.gameObject;
if (touchObj != null && touchObj.GetComponent<VKButton>())
{
touchPos = Camera.allCameras[i].ScreenToWorldPoint(touchPos);
pressOffSet = touchObj.transform.position - touchPos;
VKButton button = touchObj.GetComponent<VKButton>();
if (!iSNull(button.pressEventName) && button.eventObj != null)
button.eventObj.SendMessage(button.pressEventName, button);
if (button.pressButtonTex != null)
{
button.renderer.sharedMaterial.mainTexture = button.pressButtonTex;
}
if (button.isDrag && !iSNull(button.dragStartEventName))
{
button.eventObj.SendMessage(button.dragStartEventName, vec);
}
if (button.isAni)
{
button.SendMessage("onPressAni");
}
}
}
}
}

public void onPressUp(Vector2 vec)
{
if (touchObj != null)
{
VKButton button = touchObj.GetComponent<VKButton>();
if (button != null)
{
if (button.buttonTex != null)
{
touchObj.renderer.sharedMaterial.mainTexture = touchObj.GetComponent<VKButton>().buttonTex;
}
if (!iSNull(button.clickEventName) && button.eventObj != null)
{
button.eventObj.SendMessage(button.clickEventName, button);
}
if (button.isDrag && !iSNull(button.dragEndEventName))
{
button.SendMessage(button.dragEndEventName, vec);
}
if (button.isAni)
{
button.SendMessage("onClickAni");
}
}
touchObj = null;
}
}

public void onDrag(Vector2 vec)
{
if (touchObj != null)
{
VKButton button = touchObj.GetComponent<VKButton>();
if (button != null && button.isDrag)
{
for (int i = 0; i < Camera.allCameras.Length; i++)
{
Vector2 worldVec = Camera.allCameras[i].ScreenToWorldPoint(vec);
touchObj.transform.position = new Vector3(worldVec.x + pressOffSet.x, worldVec.y + pressOffSet.y, touchObj.transform.position.z);
if (!iSNull(button.dragEventName))
button.eventObj.SendMessage(button.dragEventName, worldVec);
}
}
}
}

bool iSNull(string eventName)
{
bool buttonIsNull = false;
if (eventName == null || eventName.Equals("null"))
{
buttonIsNull = true;
}
return buttonIsNull;
}

3.3 摄像机适配方案

这里提供了摄像机的初始化代码,但摄像机适配方案超出了Unity物理引擎的范围,不建议使用,仅供参考。该方案采用1420 * 800的设计方案。

public void initVKCamere()
{
gameObject.name = "VKCamere";
this.camera.orthographic = true;
this.camera.backgroundColor = Color.black;
this.camera.nearClipPlane = 0;
this.camera.farClipPlane = 9999;
this.camera.orthographic = true;
this.camera.orthographicSize = getCameraSize();
this.transform.position = new Vector3(0, 0, -1000);
this.transform.rotation = Quaternion.Euler(Vector3.zero);
this.transform.localScale = Vector3.one;
Application.targetFrameRate = 60;
if (GetComponent<AudioListener>())
{
//DestroyImmediate(GetComponent<AudioListener>());
}
}

int getCameraSize()
{
int size = 384;
bool isLandscape = (Camera.main.pixelWidth > Camera.main.pixelHeight);
float rad = Camera.main.pixelWidth / Camera.main.pixelHeight;
bool isIPad = (Mathf.Abs(rad - 1.3333f) < 0.001f) || (Mathf.Abs(rad - 0.75f) < 0.001f);
if (isIPad)
{
if (isLandscape)
{
size = 400;
}
else
{
size = 533;
}
}
else
{
if (isLandscape)
{
size = 400;
}
else
{
// iPhone 5
if (Camera.main.pixelHeight / Camera.main.pixelWidth > 1.6)
{
size = 710;
}
else
{
size = 600;
}
}
}
return size;
}

3.4 摄像机的编辑器类

using UnityEditor;
using UnityEngine;
using System.Collections;

[CustomEditor(typeof(VKCamera))]
public class VKCameraEditor : Editor
{
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
VKCamera vkCamere = (VKCamera)target;
if (GUILayout.Button("ReSetCamera"))
{
vkCamere.initVKCamere();
}
EditorUtility.SetDirty(vkCamere);
EditorUtility.UnloadUnusedAssets();
}
}

4. Button相关内容

Button的主要作用是处理摄像机传来的信息。Button继承自前面的 VKView。虽然原理简单,但实现起来仍有不少代码。后续将继续深入探讨Button的实现细节。

作者信息

feifeila

feifeila

共发布了 3994 篇文章