How can I get the real Texture/Sprites reference in OnPostprocessSprites in Unity? - unity3d

My goal was to create an import script (AssetPostprocessor), that handles a few things for me whenever I drag a spritesheet (a picture that contains multiple frames for an animation) into a specific Unity folder.
I want the script to split the picture up into multiple frames (similar to when its done manually, with Sprite Mode = Multiple), and then create an animation out of it. I parse the instructions (name, sprite frame size, frame holds) from the file name that gets handled.
Example: "Player_Walk_200x100_h3-3-3-3-3.png"
So far, I managed to accomplish all of those points, but whenever I create an animation and link the given Sprites to it, the resulting animation is "empty". As far as I could figure out, that's because the Texture2D and Sprite[] given to OnPostprocessSprites() seems to be temporary. The object exists during creation, but seemingly gets discarded later on. How can I solve this? How can I grab a reference to Sprite[] that doesn't get dropped?
Here is a stripped down version of my current core:
void OnPostprocessTexture( Texture2D texture ) {
// the texture is split into frames here, code not included since this part works fine and would complicate things.
}
void OnPostprocessSprites( Texture2D texture, Sprite[] sprites ) {
if( !this.assetPath.Contains( 'spritetest' ) ) return;
Debug.Log( "Number of Sprites: " + sprites.Length ); // shows the correct number of sprites
if( sprites.Length == 0 ) {
AssetDatabase.ImportAsset( this.assetImporter.assetPath );
return;
}
int frameRate = 24;
AnimationClip clip = new AnimationClip();
clip.frameRate = frameRate;
EditorCurveBinding spriteBinding = new EditorCurveBinding();
spriteBinding.type = typeof( SpriteRenderer );
spriteBinding.path = "";
spriteBinding.propertyName = "m_Sprite";
ObjectReferenceKeyframe[] spriteKeyFrames = new ObjectReferenceKeyframe[ sprites.Length ];
for( int i = 0; i < sprites.Length; i++ ) {
spriteKeyFrames[ i ] = new ObjectReferenceKeyframe();
spriteKeyFrames[ i ].time = i;
spriteKeyFrames[ i ].value = sprites[ i ]; // these sprites are empty in the editor
}
AnimationUtility.SetObjectReferenceCurve( clip, spriteBinding, spriteKeyFrames );
AssetDatabase.CreateAsset( clip, "Assets/Assets/spritetest/Test.anim" );
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
I've looked up forum posts, documentations, and even questions here (like Unity Editor Pipeline Scripting: how to Pass Sprite to SpriteRenderer in OnPostprocessSprites() Event? ) but none really managed to resolve this problem: whenever I try to grab the "real" sprite or texture via AssetDatabase, the resulting object is always just null.
For example, if I try doing this:
// attempt 1
Sprite sprites = AssetDatabase.LoadAssetAtPath<Sprite>( this.assetPath );
Debug.Log( "Real Sprite: " + sprite ); // sprite is null
// attempt 2
Object[] objects = AssetDatabase.LoadAllAssetRepresentationsAtPath( this.assetPath );
Debug.Log( "Real Objects: " + objects.Length ); // is always 0
Calling AssetDatabase.SaveAssets() or AssetDatabase.Refresh() beforehand doesn't change the result. I find plenty of people with similar issues, but no code or examples that seem to really resolve this issue.
Here is another person with a similar issue: ( source: https://answers.unity.com/questions/1080430/create-animation-clip-from-sprites-programmaticall.html )
The given link didn't resolve my problem either.
Essentially: How can I create an animation from an imported asset, by using my generated sprites, in AssetPostprocessor?

Related

UNITY How do I change image position?

Below is a snippet of code thats running every update but when I log the local position of the image it still says 0,0,0 when it should be 10,10,10. What am I doing wrong??? Ultimately I am trying to understand how to programmatically move an image around on screen
public partial class MainCanvasSystem : SystemBase
{
protected override void OnUpdate()
{
if (MainGameObjectCanvas.Instance != null && SystemAPI.HasSingleton<MainEntityCanvas>())
{
Entity mainEntityCanvasEntity = SystemAPI.GetSingletonEntity<MainEntityCanvas>();
LocalToWorld targetLocalToWorld = SystemAPI.GetComponent<LocalToWorld>(mainEntityCanvasEntity);
Canvas canvas = MainGameObjectCanvas.Instance;
Image image = canvas.GetComponentInChildren<Image>();
var rect = image.GetComponent<RectTransform>();
rect.localScale.Set(10,10,10);
Debug.Log(rect.localPosition.x);
}
}
}
I think there is general misunderstanding here.
rect.localScale.Set(10,10,10);
does .. nothing!
Transform.localScale is a property and returns a COPY of a Vector3 struct.
You are calling Vector3.Set on it which replaces the values within that Vector3 copy, yes, but then you never actually apply it anywhere.
=> you need to actually set the property!
You rather would do e.g.
rect.locaScale = Vector3.one * 10;
or
rect.localScale = new Vector3(10,10,10);
However, this said, changing a localScale won't change the position at all. The RectTransform.anchoredPosition is probably rather the one to go with.

Unity GUIEditor - how can I access the sprites within my texture from code

I want to access the sprites under the png I've got selected. (Which has already been sliced in the sprite editor)
Example: Here I know how I can make my GUIEditor see I have Druid_jump selected, now I want to grab hold of the Sprite type objects Druid_Jump_0_00 until Druid_Jump_3_03 by code (without interaction of my user) so that I can set up the 4 animations for them
I was trying with the following code sample:
List<Texture2D> textures = new List<Texture2D>(Selection.GetFiltered<Texture2D>(SelectionMode.Unfiltered));
foreach (Texture texture in textures)
{
if (texture != null)
{
string path = AssetDatabase.GetAssetPath(texture);
string containingFolder = null;
if (string.IsNullOrEmpty(containingFolder))
{
containingFolder = path.Remove(path.LastIndexOf('/'));
}
var importer = AssetImporter.GetAtPath(path) as TextureImporter;
List<Sprite> currentFrames = new List<Sprite>();
int index = 0;
foreach (SpriteMetaData spriteMetaData in importer.spritesheet)
{
string[] slice = spriteMetaData.name.Split('_');
if (slice[2] != index.ToString())
{
//CreateAnimation(currentFrames.Count, currentFrames);
currentFrames = new List<Sprite>();
index++;
}
// The Code works fine until here, I need the "Sprite" type object to set up the animation
Sprite sprite = AssetDatabase.LoadAssetAtPath<Sprite>($"{containingFolder}/{spriteMetaData.name}");
//currentFrames.Add(sprite);
Debug.Log(sprite.name);
}
}
}
I was hoping Sprite sprite = AssetDatabase.LoadAssetAtPath<Sprite>($"{containingFolder}/{spriteMetaData.name}"); would get the individual Sprite, but it currently finds null, while the spriteMetaData is actually returning the correct spritenames.
Sprite sprite = Sprite.Create(Texture2D texture, Rect rect, Vector2 pivot);
Your rect will be probably (0,0,resultionX,resulutionY), and pivot (0.5f, 0.5f)
Considering the image was already sliced into sprites, the correct answer was using AssetDatabase.LoadAllAssetsAtPath instead, which returns an Object array and includes the sprites under the png image. The AssetDatabase.GetAssetPath function does not include those.
Once we have the object array, we loop through the objects and cast the object to the sprite type and then we are good to go.
Object[] data = AssetDatabase.LoadAllAssetsAtPath(AssetDatabase.GetAssetPath(texture));
if(data != null)
{
foreach (Object obj in data)
{
if (obj.GetType() == typeof(Sprite))
{
Sprite sprite = obj as Sprite;

Cant instantiate multi-sprite

I can't seem to load a sprite runtime.
I have a main grid object, on the grid I have a TerrainDrawer component.
The code for the script :
void Start()
{
Sprite[] myFruit = Resources.LoadAll<Sprite>("Sprites/multisprite");
foreach(var sprite in myFruit)
{
print("sprite : " + sprite.name);
}
/*var spritePath = "Sprites/225835_hyptosis_tile-art-batch-1";
//GameObject go = new GameObject();*/
SpriteRenderer renderer = gameObject.AddComponent<SpriteRenderer>();
renderer.sprite = myFruit[0];
//renderer.sprite = Resources.Load(spritePath , typeof(Sprite)) as Sprite;
}
The sprite that I'm wanting to use:
When I start up my game, I get this error :
IndexOutOfRangeException: Index was outside the bounds of the array.
TerrainDrawer.Start () (at Assets/Scripts/TerrainDrawer.cs:21)
I have a strong feeling that my problem is with the path to the sprite, but now matter what I try, I can't get it loaded up.
Any advice?
To load from Resources in code there must exist a folder named Resources with that sprite in it. You are basically loading in an empty array and that is why you get an out of range error.
Take a look at this page from the docs.
Hope this helps!

Using a Tilemap composite collider as a trigger, what is the best approach for getting hold of the subcollider that is actually being triggered?

This seems a lot less straight forward than perhaps it should be. I'm using a Tilemap with a composite collider set up as a trigger to draw ladders on the map, and want to lock the player to the central x point of the ladder when they're climbing it. However, I'm hitting a wall in that it seems very difficult to simply get ahold of the individual tile that is being collided with.
The method mentioned elsewhere only works for collisions (ie non-triggers), and even that seems somewhat convoluted for what I would've expected to be a routine task.
Does anyone have any suggestions how to do this? I need to either somehow get the contact point of the trigger (seems to be unsupported) in order to then fire a ray to get the collider, or otherwise add additional gameobjects to the scene that can act as anchors for the ladder, which seems to go against the point of using tilemaps.
Thanks for any help.
There is a way to create a separate collider at the tile location of the tile map to use that location.
It is more accurate than raycast.
Because it is an example script, please refactor and use it
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;
[RequireComponent( typeof( Tilemap ) )]
public class TilemapCollider : MonoBehaviour
{
Tilemap t;
void Start()
{
t = GetComponent<Tilemap>();
BoundsInt bounds = t.cellBounds;
TileBase[] allTiles = t.GetTilesBlock( bounds );
var s = t.layoutGrid.cellSize / 2;
var availablePlaces = new List<Vector3>();
for( int n = t.cellBounds.xMin; n < t.cellBounds.xMax; n++ )
{
for( int p = t.cellBounds.yMin; p < t.cellBounds.yMax; p++ )
{
Vector3Int localPlace = ( new Vector3Int( n, p, (int)t.transform.position.y ) );
Vector3 place = t.CellToWorld( localPlace );
var tile = t.GetTile( localPlace );
if( tile )
{
availablePlaces.Add( place );
var c = new GameObject().AddComponent<BoxCollider2D>();
c.isTrigger = true;
c.transform.parent = t.transform;
c.transform.localPosition = t.CellToLocal( localPlace ) + s;
Debug.Log( "x:" + n + " y:" + p + " tile:" + tile.name );
}
}
}
}
}
TilemapCollider.cs
private void OnTriggerEnter2D( Collider2D collision )
{
var tilemap = GameObject.Find( "Grid/Tilemap" ).GetComponent<Tilemap>();
tilemap.SetTile( tilemap.WorldToCell( collision.transform.position ), null );
}
Example of finding a location on OnTriggerEnter2D

Add remote image as texture during loop

I am very new to Unity3d.
I have a JSON array that contains the parameters of the prefabs I want to create at runtime.
I want to display images that are stored on my server in the scene.
I have a prefab "iAsset" that has a plane (mesh filter) and I want to load the image files as the texture of the plane.
I am able to Instatiate the prefabs however the prefab is showing up as a white square. This is my code:
for(var i = 0; i < bookData.Assets.Count; i++){
GameObject newAsset = null;
newAsset = (GameObject)Instantiate(iasset, new Vector3(2*i, 0, 0), Quaternion.identity);
if(!imageAssetRequested )
{
remoteImageAsset = new WWW(((BookAssets)bookData.Assets[i]).AssetContent);
imageAssetRequested = true;
}
if(remoteImageAsset.progress >=1)
{
if(remoteImageAsset.error == null)
{
loadingRemoteAsset = false;
newAsset.renderer.material.mainTexture = remoteImageAsset.texture;
}
}
}
the urls to the images on my server is retrieved from the JSON array:
((BookAssets)bookData.Assets[i]).AssetContent);
The code builds without any errors, I would very much appreciate any help to display the remote images.
You are not waiting for your downloads to complete.
The WWW class is asynchronous, and will commence the download. However, you need to either poll it (using the code you do have above) at a later time, or use a yield WWW in a CoRoutine that will block your execution (within that CoRoutine) until the download finishes (either successfully or due to failure).
Refer to Unity Documentation for WWW
Note however, that page sample code is wrong, and Start is not a CoRoutine / IEnumarator. Your code would look something like :
void Start()
{
... your code above ...
StartCoroutine(DownloadImage(bookData.Assets[i]).AssetContent, newAsset.renderer.material.mainTexture));
}
IEnumerator DownloadImage(string url, Texture tex)
{
WWW www = new WWW(url);
yield return www;
tex.LoadImage(www.bytes)
}