Recently I wrote about authoring your 2D content in Photoshop and then setting it up for exporting. Today’s post will cover importing the files into Unity. You can find a link to download all the files at the end of the post.
最近,我写了关于在Photoshop中创作2D内容,然后对其进行设置以进行导出的文章。 今天的帖子将介绍如何将文件导入Unity。 您可以在文章末尾找到一个链接来下载所有文件。
Picking up where we left off
在我们离开的地方接机
Unity will happily import our images and XML file, but the images will be imported as normal textures by default, and the XML file will be imported as a text asset. So we need to write an importer within Unity to process the files for our game. You might think that writing an AssetPostprocessor would be the way to go and you’d be forgiven for thinking that. The problem with using AssetPostprocessor is that we’re limited in what we can do within that context and it won’t allow us to create a new material which we need to do. So the answer is to simply write an editor script to do what we want.
Unity将很乐意导入我们的图像和XML文件,但是默认情况下,图像将作为普通纹理导入,而XML文件将作为文本资源导入。 因此,我们需要在Unity中编写一个导入程序来处理游戏文件。 您可能会认为编写AssetPostprocessor将是您的理想之选,并且您会以为如此。 使用AssetPostprocessor的问题在于,在这种情况下我们只能做些有限的事情,并且它不允许我们创建需要做的新材料。 因此,答案是只需编写一个编辑器脚本即可完成我们想要的工作。
Our importer will need to:
我们的进口商将需要:
Writing Editor scripts As with the exporter, you can download the importer script here (). It should be placed in a folder called “Editor” in order for it to work. If you haven’t written editor scripts before you’re not taking advantage of one of Unity’s most powerful features. Editor scripts are a superset of normal scripting, so if you already know how to write game scripts you already know the basics of editor scripting too.
编写编辑器脚本与导出器一样,您可以在此处()下载导入器脚本。 应该将其放置在名为“ Editor”的文件夹中,以使其工作。 如果您尚未使用Unity最强大的功能之一,还没有编写编辑器脚本。 编辑器脚本是普通脚本的超集,因此,如果您已经知道如何编写游戏脚本,那么您也已经知道编辑器脚本的基础。
Editor scripts are distinguished from regular game scripts in essentially three ways. Firstly, editor scripts must be placed within a folder named “Editor”, or one of it’s sub-folders. Secondly, at the top of the scripting file you must place the following line:
编辑器脚本在本质上通过三种方式与常规游戏脚本区分开。 首先,必须将编辑器脚本放置在名为“编辑器”的文件夹或其子文件夹之一中。 其次,必须在脚本文件的顶部放置以下行:
1 |
using UnityEditor;
|
1 |
using UnityEditor ;
|
Finally, in order for the editor script to be called, it must derive from one of the editor sub-classes such as Editor, EditorWindow, ScriptableWizard, etc. You can use classes that don’t derive from these, but if you want to receive events and be interactive, these classes do so in a way similar to MonoBehaviour. For our purposes we’ll use the general Editor base class.
最后,为了调用编辑器脚本,它必须从编辑器子类之一(例如Editor,EditorWindow,ScriptableWizard等)派生。您可以使用不是从这些子类派生的类,但是如果要接收事件并进行交互,这些类的操作方式类似于MonoBehaviour。 为了我们的目的,我们将使用常规的Editor基类。
1
2
3
4
|
public class HogImporter : Editor
{
...
}
|
1
2
3
4
|
public class HogImporter : Editor
{
. . .
}
|
As with the exporter we won’t go over every line as the file is well commented, but let’s look at some of the more interesting bits. To start, we need some way to invoke our importer, so we’ll put it on the Assets menu inside Unity like this:
与导出器一样,由于文件已被注释,因此我们不会遍历每一行,但让我们看一些更有趣的部分。 首先,我们需要某种方式来调用导入器,因此我们将其放置在Unity内部的Assets菜单中,如下所示:
1
2
3
4
5
|
[MenuItem("Assets/Create/Import HOG Scene...")]
static public void ImportHogSceneMenuItem()
{
...
}
|
1
2
3
4
5
|
[ MenuItem ( "Assets/Create/Import HOG Scene..." ) ]
static public void ImportHogSceneMenuItem ( )
{
. . .
}
|
So now we are sitting in the Assets menu, and when chosen we can execute a script, so now all we have to do is do something useful.
因此,现在我们坐在“资产”菜单中,选择后就可以执行脚本,因此现在要做的只是做一些有用的事情。
The first thing we’ll do is convert the XML file to something more useable such as a data class. When we wrote the exporter, we organized our data into a logical set of elements so that it will map easily into a data class. Our data class looks like the following and matches our XML file in structure and data types:
我们要做的第一件事是将XML文件转换为更有用的东西,例如数据类。 编写导出器时,我们将数据组织成逻辑上的一组元素,以便可以轻松地映射到数据类中。 我们的数据类如下所示,并在结构和数据类型上匹配我们的XML文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
public class HogScene
{
public Layer[] layers;
public enum LayerStatus { Active, Found };
public enum LayerType { Custom, Item, Scenery };
public class Layer
{
public LayerType type;
public string name;
public LayerStatus layerStatus;
public Rect bounds;
public Image[] images;
}
public enum ImageType { Hotspot, Obscured, Whole, Shadow, Custom, Count };
public class Image
{
public ImageType type;
public string name;
public float x;
public float y;
public float z;
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
public class HogScene
{
public Layer [ ] layers ;
public enum LayerStatus { Active , Found } ;
public enum LayerType { Custom , Item , Scenery } ;
public class Layer
{
public LayerType type ;
public string name ;
public LayerStatus layerStatus ;
public Rect bounds ;
public Image [ ] images ;
}
public enum ImageType { Hotspot , Obscured , Whole , Shadow , Custom , Count } ;
public class Image
{
public ImageType type ;
public string name ;
public float x ;
public float y ;
public float z ;
}
}
|
To actually convert the XML file into the data class, we use the built-in .NET serialization methods.
要将XML文件实际转换为数据类,我们使用内置的.NET序列化方法。
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
|
HogScene hogScene = (HogScene)DeserializeXml(assetPath, typeof(HogScene));
...
static private object DeserializeXml(string filePath, System.Type type)
{
object instance = null;
StreamReader xmlFile = File.OpenText(filePath);
if(xmlFile != null)
{
string xml = xmlFile.ReadToEnd();
if((xml != null) && (xml.ToString() != ""))
{
XmlSerializer xs = new XmlSerializer(type);
UTF8Encoding encoding = new UTF8Encoding();
byte[] byteArray = encoding.GetBytes(xml);
MemoryStream memoryStream = new MemoryStream(byteArray);
XmlTextWriter xmlTextWriter =
new XmlTextWriter(memoryStream, Encoding.UTF8);
if(xmlTextWriter != null)
{
instance = xs.Deserialize(memoryStream);
}
}
}
xmlFile.Close();
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
|
HogScene hogScene = ( HogScene ) DeserializeXml ( assetPath , typeof ( HogScene ) ) ;
. . .
static private object DeserializeXml ( string filePath , System . Type type )
{
object instance = null ;
StreamReader xmlFile = File . OpenText ( filePath ) ;
if ( xmlFile != null )
{
string xml = xmlFile . ReadToEnd ( ) ;
if ( ( xml != null ) & amp ; & amp ; ( xml . ToString ( ) != "" ) )
{
XmlSerializer xs = new XmlSerializer ( type ) ;
UTF8Encoding encoding = new UTF8Encoding ( ) ;
byte [ ] byteArray = encoding . GetBytes ( xml ) ;
MemoryStream memoryStream = new MemoryStream ( byteArray ) ;
XmlTextWriter xmlTextWriter =
new XmlTextWriter ( memoryStream , Encoding . UTF8 ) ;
if ( xmlTextWriter != null )
{
instance = xs . Deserialize ( memoryStream ) ;
}
}
}
xmlFile . Close ( ) ;
return instance ;
}
|
Since we’ll be creating a scene using code, it’s possible that the user already has that scene open, or another, and we need to be sure they get a chance to save changes. To do this we call:
由于我们将使用代码创建场景,因此用户可能已经打开了该场景或另一个场景,因此我们需要确保他们有机会保存更改。 为此,我们称:
1
2
3
4
|
if(EditorApplication.SaveCurrentSceneIfUserWantsTo() == false)
{
return;
}
|
1
2
3
4
|
if ( EditorApplication . SaveCurrentSceneIfUserWantsTo ( ) == false )
{
return ;
}
|
Then we simply create a new scene, deleting the old one first if it already exists:
然后我们简单地创建一个新场景,如果已经存在,则首先删除旧场景:
1
2
3
4
5
6
7
8
|
string scenePath = baseDirectory + baseFilename + " Scene.unity";
if(File.Exists(scenePath) == true)
{
File.Delete(scenePath);
AssetDatabase.Refresh();
}
// now create a new scene
EditorApplication.NewScene();
|
1
2
3
4
5
6
7
8
|
string scenePath = baseDirectory + baseFilename + " Scene.unity" ;
if ( File . Exists ( scenePath ) == true )
{
File . Delete ( scenePath ) ;
AssetDatabase . Refresh ( ) ;
}
// now create a new scene
EditorApplication . NewScene ( ) ;
|
Next up is to fix the texture import settings. By default textures aren’t readable and we need to read them in order to put them into an atlas. While we’re at it, we also need to change some of the other settings.
接下来是修复纹理导入设置。 默认情况下,纹理是不可读的,我们需要阅读它们才能将其放入地图集。 在进行此操作时,我们还需要更改其他一些设置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
List<Texture2D> textureList = new List<Texture2D>();
for(int layerIndex = 0; layerIndex < hogScene.layers.Length; layerIndex++)
{
for(int imageIndex = 0;
imageIndex < hogScene.layers[layerIndex].images.Length;
imageIndex++)
{
// we need to fixup all images that were exported from PS
string texturePathName = baseDirectory +
hogScene.layers[layerIndex].images[imageIndex].name;
Texture2D inputTexture =
(Texture2D)AssetDatabase.LoadAssetAtPath(
texturePathName, typeof(Texture2D));
// modify the importer settings
TextureImporter textureImporter =
AssetImporter.GetAtPath(texturePathName) as TextureImporter;
textureImporter.mipmapEnabled = false;
textureImporter.isReadable = true;
textureImporter.npotScale = TextureImporterNPOTScale.None;
textureImporter.wrapMode = TextureWrapMode.Clamp;
textureImporter.filterMode = FilterMode.Point;
...
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
List & lt ; Texture2D & gt ; textureList = new List & lt ; Texture2D & gt ; ( ) ;
for ( int layerIndex = 0 ; layerIndex & lt ; hogScene . layers . Length ; layerIndex ++ )
{
for ( int imageIndex = 0 ;
imageIndex & lt ; hogScene . layers [ layerIndex ] . images . Length ;
imageIndex ++ )
{
// we need to fixup all images that were exported from PS
string texturePathName = baseDirectory +
hogScene . layers [ layerIndex ] . images [ imageIndex ] . name ;
Texture2D inputTexture =
( Texture2D ) AssetDatabase . LoadAssetAtPath (
texturePathName , typeof ( Texture2D ) ) ;
// modify the importer settings
TextureImporter textureImporter =
AssetImporter . GetAtPath ( texturePathName ) as TextureImporter ;
textureImporter . mipmapEnabled = false ;
textureImporter . isReadable = true ;
textureImporter . npotScale = TextureImporterNPOTScale . None ;
textureImporter . wrapMode = TextureWrapMode . Clamp ;
textureImporter . filterMode = FilterMode . Point ;
. . .
}
}
|
Now we’re ready to create the new atlas material, texture and pack everything into it.
现在,我们准备创建新的Atlas材料,纹理并将所有内容打包到其中。
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
|
// create material
string materialPath = baseDirectory + baseFilename + " Material.mat";
// remove previous one if it exists
if(File.Exists(materialPath) == true)
{
File.Delete(materialPath);
AssetDatabase.Refresh();
}
// make a material and link it to atlas, save that too
Material material = new Material(Shader.Find("Transparent/Diffuse"));
AssetDatabase.CreateAsset(material, materialPath);
AssetDatabase.Refresh();
// load it back
material = (Material)AssetDatabase.LoadAssetAtPath(materialPath, typeof(Material));
// make a new atlas texture
Texture2D atlas = new Texture2D(2048, 2048);
// to make an atlas we need an array instead of a list
Texture2D[] textureArray = textureList.ToArray();
// pack it with all the textures we have
Rect[] atlasRects = atlas.PackTextures(textureArray, 0, 2048);
// save it to disk
byte[] atlasPng = atlas.EncodeToPNG();
string atlasPath = baseDirectory + baseFilename + " Atlas.png";
if(File.Exists(atlasPath) == true)
{
File.Delete(atlasPath);
AssetDatabase.Refresh();
}
File.WriteAllBytes(atlasPath, atlasPng);
|
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
|
// create material
string materialPath = baseDirectory + baseFilename + " Material.mat" ;
// remove previous one if it exists
if ( File . Exists ( materialPath ) == true )
{
File . Delete ( materialPath ) ;
AssetDatabase . Refresh ( ) ;
}
// make a material and link it to atlas, save that too
Material material = new Material ( Shader . Find ( "Transparent/Diffuse" ) ) ;
AssetDatabase . CreateAsset ( material , materialPath ) ;
AssetDatabase . Refresh ( ) ;
// load it back
material = ( Material ) AssetDatabase . LoadAssetAtPath ( materialPath , typeof ( Material ) ) ;
// make a new atlas texture
Texture2D atlas = new Texture2D ( 2048 , 2048 ) ;
// to make an atlas we need an array instead of a list
Texture2D [ ] textureArray = textureList . ToArray ( ) ;
// pack it with all the textures we have
Rect [ ] atlasRects = atlas . PackTextures ( textureArray , 0 , 2048 ) ;
// save it to disk
byte [ ] atlasPng = atlas . EncodeToPNG ( ) ;
string atlasPath = baseDirectory + baseFilename + " Atlas.png" ;
if ( File . Exists ( atlasPath ) == true )
{
File . Delete ( atlasPath ) ;
AssetDatabase . Refresh ( ) ;
}
File . WriteAllBytes ( atlasPath , atlasPng ) ;
|
Since we have created all the basic parts we need, we can now loop over the images, create meshes, create the GameObjects and put them into the scene. It’s a fair amount of code so it isn’t repeated in it’s entirety, but certainly the most interesting part has to be creating the 2D sprites in Unity. You might be surprised to find out how simple and easy it is. I use a simple function to do it for all my 2D stuff within Unity and it looks like the following.
由于已经创建了所需的所有基本部件,因此我们现在可以遍历图像,创建网格,创建GameObject并将它们放入场景中。 这是相当数量的代码,因此不会整体重复,但是,最有趣的部分肯定是在Unity中创建2D精灵。 您可能会惊讶地发现它是如此简单和容易。 我使用一个简单的函数对Unity中的所有2D东西进行处理,如下所示。
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
|
static private void ConfigureGo(GameObject go, Texture2D texture, Material material,
Rect uvRect, string meshPath)
{
// create meshFilter if new
MeshFilter meshFilter = (MeshFilter)go.GetComponent(typeof(MeshFilter));
if(meshFilter == null)
{
meshFilter = (MeshFilter)go.AddComponent(typeof(MeshFilter));
}
// create mesh if new
Mesh mesh = meshFilter.sharedMesh;
if(mesh == null)
{
mesh = new Mesh();
}
mesh.Clear();
// setup rendering
MeshRenderer meshRenderer =
(MeshRenderer)go.GetComponent(typeof(MeshRenderer));
if(meshRenderer == null)
{
meshRenderer =
(MeshRenderer)go.AddComponent(typeof(MeshRenderer));
}
meshRenderer.renderer.material = material;
// create the mesh geometry
// Unity winding order is counter-clockwise when viewed
// from behind and facing forward (away)
// Unity winding order is clockwise when viewed
// from behind and facing behind
// 1---2
// | / |
// | / |
// 0---3
Vector3[] newVertices;
int[] newTriangles;
Vector2[] uvs;
float hExtent = texture.width * 0.5f;
float vExtent = texture.height * 0.5f;
newVertices = new Vector3[4];
newVertices[0] = new Vector3(-hExtent, -vExtent, 0);
newVertices[1] = new Vector3(-hExtent, vExtent, 0);
newVertices[2] = new Vector3(hExtent, vExtent, 0);
newVertices[3] = new Vector3(hExtent, -vExtent, 0);
newTriangles = new int[] { 0, 1, 2, 0, 2, 3 };
uvs = new Vector2[4];
uvs[0] = new Vector2(uvRect.x, uvRect.y);
uvs[1] = new Vector2(uvRect.x, uvRect.y + uvRect.height);
uvs[2] = new Vector2(uvRect.x + uvRect.width, uvRect.y + uvRect.height);
uvs[3] = new Vector2(uvRect.x + uvRect.width, uvRect.y);
Color[] vertColors = new Color[4];
vertColors[0] = Color.white;
vertColors[1] = Color.white;
vertColors[2] = Color.white;
vertColors[3] = Color.white;
// update the mesh
mesh.vertices = newVertices;
mesh.colors = vertColors;
mesh.uv = uvs;
mesh.triangles = newTriangles;
// generate some some normals for the mesh
mesh.normals = new Vector3[4];
mesh.RecalculateNormals();
if(File.Exists(meshPath) == true)
{
File.Delete(meshPath);
AssetDatabase.Refresh();
}
AssetDatabase.CreateAsset(mesh, meshPath);
AssetDatabase.Refresh();
meshFilter.sharedMesh = mesh;
// add collider
go.AddComponent(typeof(MeshCollider));
}
|
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
|
static private void ConfigureGo ( GameObject go , Texture2D texture , Material material ,
Rect uvRect , string meshPath )
{
// create meshFilter if new
MeshFilter meshFilter = ( MeshFilter ) go . GetComponent ( typeof ( MeshFilter ) ) ;
if ( meshFilter == null )
{
meshFilter = ( MeshFilter ) go . AddComponent ( typeof ( MeshFilter ) ) ;
}
// create mesh if new
Mesh mesh = meshFilter . sharedMesh ;
if ( mesh == null )
{
mesh = new Mesh ( ) ;
}
mesh . Clear ( ) ;
// setup rendering
MeshRenderer meshRenderer =
( MeshRenderer ) go . GetComponent ( typeof ( MeshRenderer ) ) ;
if ( meshRenderer == null )
{
meshRenderer =
( MeshRenderer ) go . AddComponent ( typeof ( MeshRenderer ) ) ;
}
meshRenderer . renderer . material = material ;
// create the mesh geometry
// Unity winding order is counter-clockwise when viewed
// from behind and facing forward (away)
// Unity winding order is clockwise when viewed
// from behind and facing behind
// 1---2
// | / |
// | / |
// 0---3
Vector3 [ ] newVertices ;
int [ ] newTriangles ;
Vector2 [ ] uvs ;
float hExtent = texture . width * 0.5f ;
float vExtent = texture . height * 0.5f ;
newVertices = new Vector3 [ 4 ] ;
newVertices [ 0 ] = new Vector3 ( - hExtent , - vExtent , 0 ) ;
newVertices [ 1 ] = new Vector3 ( - hExtent , vExtent , 0 ) ;
newVertices [ 2 ] = new Vector3 ( hExtent , vExtent , 0 ) ;
newVertices [ 3 ] = new Vector3 ( hExtent , - vExtent , 0 ) ;
newTriangles = new int [ ] { 0 , 1 , 2 , 0 , 2 , 3 } ;
uvs = new Vector2 [ 4 ] ;
uvs [ 0 ] = new Vector2 ( uvRect . x , uvRect . y ) ;
uvs [ 1 ] = new Vector2 ( uvRect . x , uvRect . y + uvRect . height ) ;
uvs [ 2 ] = new Vector2 ( uvRect . x + uvRect . width , uvRect . y + uvRect . height ) ;
uvs [ 3 ] = new Vector2 ( uvRect . x + uvRect . width , uvRect . y ) ;
Color [ ] vertColors = new Color [ 4 ] ;
vertColors [ 0 ] = Color . white ;
vertColors [ 1 ] = Color . white ;
vertColors [ 2 ] = Color . white ;
vertColors [ 3 ] = Color . white ;
// update the mesh
mesh . vertices = newVertices ;
mesh . colors = vertColors ;
mesh . uv = uvs ;
mesh . triangles = newTriangles ;
// generate some some normals for the mesh
mesh . normals = new Vector3 [ 4 ] ;
mesh . RecalculateNormals ( ) ;
if ( File . Exists ( meshPath ) == true )
{
File . Delete ( meshPath ) ;
AssetDatabase . Refresh ( ) ;
}
AssetDatabase . CreateAsset ( mesh , meshPath ) ;
AssetDatabase . Refresh ( ) ;
meshFilter . sharedMesh = mesh ;
// add collider
go . AddComponent ( typeof ( MeshCollider ) ) ;
}
|
That may seem like a lot of code, but it’s actually quite straightforward and simple. Finally, after we built all our 2D sprite assets, we simply fix-up the camera so that it will render our scene pixel perfect like it was originally in Photoshop.
这可能看起来像很多代码,但是实际上非常简单明了。 最后,在建立所有2D精灵资源之后,我们只需修复摄影机,使其可以像最初在Photoshop中一样完美呈现场景像素。
1
2
3
4
5
6
7
8
9
10
|
// setup our game camera
Camera.main.gameObject.AddComponent<HOGController>();
position = Vector3.zero;
position.z = -hogScene.layers.Length;
Camera.main.transform.position = position;
Camera.main.isOrthoGraphic = true;
Camera.main.orthographicSize = (768.0f/2.0f);
Camera.main.nearClipPlane = 1;
Camera.main.farClipPlane = hogScene.layers.Length + 1;
RenderSettings.ambientLight = Color.white;
|
1
2
3
4
5
6
7
8
9
10
|
// setup our game camera
Camera . main . gameObject . AddComponent & lt ; HOGController & gt ; ( ) ;
position = Vector3 . zero ;
position . z = - hogScene . layers . Length ;
Camera . main . transform . position = position ;
Camera . main . isOrthoGraphic = true ;
Camera . main . orthographicSize = ( 768.0f / 2.0f ) ;
Camera . main . nearClipPlane = 1 ;
Camera . main . farClipPlane = hogScene . layers . Length + 1 ;
RenderSettings . ambientLight = Color . white ;
|
If you look through the importer you should be able to follow it pretty easily.
如果您浏览进口商,您应该可以很容易地跟随它。
If everything went well, after running the importer script you should see the scene fully composed and running in Unity.
如果一切顺利,运行导入程序脚本后,您应该会看到场景已完全组成并在Unity中运行。
![Hog-in-Unity-cropped](https://i-blog.csdnimg.cn/blog_migrate/c33c8ff639561d2acbcbab8cea09fab1.png)
You can download a Unity package with all the files for this tutorial.