Scenario: User has different tile palettes for each season (Summer, Fall, etc.) and switching between the two using popular Unity techniques would be tedious if the number of tiles in the tileset is greater than 5 or 10. How would said user switch between their tile palettes programmatically, instead of using a wasteful solution like prefab tiles?
This problem seems significant enough that Unity docs would cover it. However I found myself digging through years-old forum posts to come up with my solution. Here's what I came up with, and it's not too complicated.
Create your tilesets. Import your tilesets as multi-sprite spritesheets, and split them using the Unity sprite editor. Do not bother naming each sprite. Each tileset should be organized the same exact way (bushes/trees/objects should be in the same spot on every season's tilesheet). When you are finished, create a folder "Resources" in your "Assets" folder. Inside of resources, create a folder "Spritesheets", and place your spricesheets in it.
Run this renaming script:
using UnityEngine;
using UnityEditor;
using System.Collections;
public class SpriteRenamer : MonoBehaviour
{
public Texture2D[] texture2Ds;
public string newName;
private string path;
private TextureImporter textureImporter;
void Start ()
{
foreach(Texture2D texture2D in texture2Ds){
path = AssetDatabase.GetAssetPath (texture2D);
textureImporter = AssetImporter.GetAtPath (path) as TextureImporter;
SpriteMetaData[] sliceMetaData = textureImporter.spritesheet;
int index = 0;
foreach (SpriteMetaData individualSliceData in sliceMetaData)
{
sliceMetaData[index].name = string.Format (newName + "_{0}", index);
print (sliceMetaData[index].name);
index++;
}
textureImporter.spritesheet = sliceMetaData;
EditorUtility.SetDirty (textureImporter);
textureImporter.SaveAndReimport ();
AssetDatabase.ImportAsset (path, ImportAssetOptions.ForceUpdate);
}
}
}
Attach it to an empty GameObject in an empty Scene (just for simplicity). Drag and drop your spritesheets into the Texture2D array. Set the newName field to whatever you want, it will be the prefix for the name of each sprite in each spritesheet. Finally, run the scene and each spritesheet's sprites will be renamed to make each corresponding sprite have the same name.
We now have each of our seasons' tilesheets modified so each tile is identically named. The next step is to create a Grid object with a child TilePalette. Draw all of your scenery, collision, etc. You can use as many TilePalettes as needed, as long as they are children of the Grid object. Now, create a script called ReskinnableTileBase:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;
public class ReskinnableTileBase : TileBase
{
public Sprite sprite;
public override void GetTileData(Vector3Int position, ITilemap tilemap, ref TileData tileData)
{
tileData.sprite = sprite;
}
}
Attach this Reskinner script to your Grid object:
using System;
using System.Linq;
using UnityEngine;
using UnityEngine.Tilemaps;
public class Reskinner : MonoBehaviour
{
Sprite[] subSprites;
Tilemap[] tilemaps;
void Start(){
tilemaps = GetComponentsInChildren<Tilemap>();
SetSkin("#INSERT DEFAULT TILE PALETTE NAME HERE#");
}
public void SetSkin(string name){
reloadSprites(name);
foreach(Tilemap tilemap in tilemaps){
for(int x = (int)tilemap.localBounds.min.x; x < tilemap.localBounds.max.x; x++){
for(int y = (int)tilemap.localBounds.min.y; y < tilemap.localBounds.max.y; y++){
TileBase tb = tilemap.GetTile(new Vector3Int(x, y, 0));
Debug.Log(tb);
ReskinnableTileBase rtb = (ReskinnableTileBase)ScriptableObject.CreateInstance(typeof(ReskinnableTileBase));
if(tb == null || rtb == null || tb.name.Length < 1){
continue;
}
Sprite replace = getSubSpriteByName(tb.name);
rtb.sprite = replace;
rtb.name = tb.name;
tilemap.SwapTile(tb, (TileBase)rtb);
}
}
}
}
void reloadSprites(string name){
subSprites = Resources.LoadAll<Sprite>("Spritesheets/" + name);
}
Sprite getSubSpriteByName(string name){
foreach(Sprite s in subSprites){
if(s.name == name){
return s;
}
}
return null;
}
}
And there you go! Now, any time you need to change the skin/season/tilesheet, just use a reference to the Grid's Reskinner script, and call the SetSkin method, like so:
Reskinner reskinner = gridObject.GetComponent<Reskinner>();
reskinner.SetSkin("summer");
Related
I am working on a project where I have a script that combines all the meshes of a gameobjects childs into one mesh while in edit mode. Now the problem is that even though I have managed to then save the mesh into my assets folder it is only saved as a default asset(blank white icon) and not as a mesh that I can then use for other things. Now I would like to know how to get it saved as a mesh asset. I think I could either somehow convert the default asset file to a mesh asset if that is possible, or I could directly save an objects mesh into the asset folder but I dont know if that's possible.
I would be very grateful if you could share code that I could use to do this(it doesnt have to be fance, it should just work).
Here is the code that I have currently:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
[ExecuteInEditMode]
[RequireComponent(typeof(MeshFilter))]
[RequireComponent(typeof(MeshRenderer))]
// Copy meshes from children into the parent's Mesh.
// CombineInstance stores the list of meshes. These are combined
// and assigned to the attached Mesh.
public class CombineMeshes : EditorWindow
{
[Header("References")]
public GameObject generateFromObject;
[MenuItem("Window/My/MeshGenerator")]
static void Init()
{
CombineMeshes window = (CombineMeshes)GetWindow(typeof(CombineMeshes));
}
private void OnGUI()
{
generateFromObject = (GameObject)EditorGUILayout.ObjectField(generateFromObject, typeof(GameObject), true);
if (GUILayout.Button("Generate"))
{
Generate();
}
}
void Generate()
{
MeshFilter[] meshFilters = generateFromObject.GetComponentsInChildren<MeshFilter>();
CombineInstance[] combine = new CombineInstance[meshFilters.Length];
int i = 0;
while (i < meshFilters.Length)
{
combine[i].mesh = meshFilters[i].sharedMesh;
combine[i].transform = meshFilters[i].transform.localToWorldMatrix;
i++;
}
generateFromObject.transform.GetComponent<MeshFilter>().mesh = new Mesh();
generateFromObject.transform.GetComponent<MeshFilter>().mesh.CombineMeshes(combine);
generateFromObject.transform.gameObject.SetActive(true);
Mesh mesh = generateFromObject.transform.GetComponent<MeshFilter>().mesh;
AssetDatabase.CreateAsset(mesh, "Assets/myAsset");
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(mesh));
AssetDatabase.SaveAssets();
}
}
I have an inventory of objects in a game, with image and description.
To represent it, I want a scrollable list with a grid (made with a UI canvas with a scroll view inside), and every element of the grid is a prefab, composed by a UI Panel with an Image and a Text.
I've tried assigning the values of the Image and the Text programmatically in the script:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class PopulateGrid : MonoBehaviour
{
public GameObject prefab;
public Sprite mySprite;
private void Start()
{
Populate();
}
public void Populate()
{
GameObject newObj;
for (int i = 0; i < 20; i++)
{
newObj = (GameObject)Instantiate(prefab, transform); //prefab has image + text
newObj.GetComponentInChildren<Text>().text = "test" + i;
newObj.GetComponentInChildren<Image>().sprite = mySprite; //I've also tried .overrideSprite
}
}
}
The funny thing is, the Text gets correctly updated, while the Image doesn't. I'm accessing them in the same way, but one acts like I want it to and the other one does his thing.
This is the end result:
UPDATE: more details! Here's the code at runtime showing that the Image sprite does indeed get assigned
and here's my inspector:
Since you say that the Panel object itself also has an Image component you do not want to use GetComponentInChildren since it also returns any Image attached to the newObj itself!
Your code doesn't throw an exception but assigns the mySprite to the Image component of the Panel object, not its children called Image.
You would either have to use e.g.
newObj.transform.GetChild(0).GetComponent<Image>()
or as mentioned create your own class and attach it to the prefab like e.g.
public class TileController : Monobehaviour
{
// Reference these via the Inspector in the prefab
[SerializeField] private Text _text;
[SerializeField] private Image _image;
public void Initialize(Sprite sprite, String text)
{
_image.sprite = sprite;
_text.text = text;
}
}
and then do
// using the correct type here makes sure you can only reference a GameObject
// that actually HAS the required component attached
[SerializeField] private TileController _prefab;
public void Populate()
{
TileController newObj;
for (int i = 0; i < 20; i++)
{
// no cast needed since Instantiate returns the type of the prefab anyway
newObj = Instantiate(_prefab, transform);
newObj.Initialize(mySprite, "test" + i);
}
}
Are you sure GetComponentInChildren is pointing to the right Image component? I would create a custom MonoBehaviour class for your prefab, and create a SetImage() method that points to the correct Image. When using GetComponentInChildren, it will return the first component found, and this can lead to unexpected behaviour.
You can do a Debug on newObj.GetComponentInChildren<Image>() to verify that it's the right component. Your code seems to be OK.
i am creating a video game like Crypt of the necrodancer and i am blocked when i want to generate a map
my idea was simple :
make a grid system and replace some tiles to have specific tiles (obstacle, wall...) but i don't know how i should store the information to replace my tile.
I based my grid generation map from Sebastian Lague tutorial
i would like to replace in the inspector my tiles
as far as i am, i've found a script for replacing some tiles by another but i can't save it...
Here is the script for replacing stuff
using UnityEngine;
using UnityEditor;
public class ReplaceWithPrefab : EditorWindow
{
[SerializeField] private GameObject prefab;
[MenuItem("Tools/Replace With Prefab")]
static void CreateReplaceWithPrefab()
{
EditorWindow.GetWindow<ReplaceWithPrefab>();
}
private void OnGUI()
{
prefab = (GameObject)EditorGUILayout.ObjectField("Prefab", prefab, typeof(GameObject), false);
if (GUILayout.Button("Replace"))
{
var selection = Selection.gameObjects;
for (var i = selection.Length - 1; i >= 0; --i)
{
var selected = selection[i];
var prefabType = PrefabUtility.GetPrefabType(prefab);
GameObject newObject;
if (prefabType == PrefabType.Prefab)
{
newObject = (GameObject)PrefabUtility.InstantiatePrefab(prefab);
}
else
{
newObject = Instantiate(prefab);
newObject.name = prefab.name;
}
if (newObject == null)
{
Debug.LogError("Error instantiating prefab");
break;
}
Undo.RegisterCreatedObjectUndo(newObject, "Replace With Prefabs");
newObject.transform.parent = selected.transform.parent;
newObject.transform.localPosition = selected.transform.localPosition;
newObject.transform.localRotation = selected.transform.localRotation;
newObject.transform.localScale = selected.transform.localScale;
newObject.transform.SetSiblingIndex(selected.transform.GetSiblingIndex());
Undo.DestroyObjectImmediate(selected);
}
}
GUI.enabled = false;
EditorGUILayout.LabelField("Selection count: " + Selection.objects.Length);
}
}
any ideas for how i could save the informations?
Unity recently added a tilemap system a few versions ago. It seems to be what you're looking for and it already handles the grids for you.
You can read more about it here:
https://docs.unity3d.com/Manual/class-Tilemap.html
You can edit the tilemap with the SetTile method and a Tile object, rather than instanciating a bunch of prefabs. The Tiles themselves are created with a tile palette which you can also read more about in the page above.
I have swapped some of my Sprite objects in favour of Meshes without textures as they should take up less memory.
However, when checking my Profiler, even after the swap the used texture memory is all the same.
So I decided to unload those textures forcefully but things aren't going very well.
My intention with this code is to find all the textures by the name of the GameObject they were on (as I just dragged the sprites on the scene via inspector, their name matches. I know which GO-s to look for as only they contain a MeshFilter) and then unload them by hand.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
public class DumpUnused : MonoBehaviour
{
private TextureContainer swappedTextures = new TextureContainer();
private IEnumerator Start()
{
RemoveSwappedTextures();
var unloader = Resources.UnloadUnusedAssets();
while (!unloader.isDone)
{
yield return null;
}
}
private void RemoveSwappedTextures()
{
//getting all my scene objects that has a mesh filter.
/*
* NOTE: I swapped my sprites for generated meshes.
* However, the texture memory used didn't decrease so I bet they are still loaded.
* The textures I used as sprites had the same name as the assets themselves as assets were
* just dragged on the scene.
*/
var sceneObjects =
UnityEngine.Object.FindObjectsOfType<GameObject>()
.Where(g => g.GetComponent<MeshFilter>() != null);
//we have the repaced gameobjects.
//now to get the used textures by their names:
foreach (var item in sceneObjects)
{
//getting everything, even assets
var strToFind = GetOriginalName(item.name);
swappedTextures.Append(
Resources.FindObjectsOfTypeAll<Texture2D>()
.Where(t => t.name.Equals(strToFind)
));
}
foreach (var text2Unload in swappedTextures)
{
//destroy the loaded textures.
DestroyImmediate(text2Unload);
}
swappedTextures = null;
}
private string GetOriginalName(string name)
{
/*
* getting the original asset name by the "unity populated name".
* reason: say I have a "backGr" named texture.
* if I copy paste it all over the place, unity makes their name
* "backGr (1)", "backGr (2)" and so on.
* So I search until the first whitespace and anything before that
* is the original name of the texture.
*/
string str = "";
char c;int i = 0;
for (c = name[i]; i < name.Length && c != ' '; i++)
{
str += c;
}
return str;
}
public class TextureContainer : List<Texture2D>
{
//made only for convenience
public void Append(IEnumerable<Texture2D> textures)
{
foreach (var _t in textures)
{
this.Add(_t);
}
}
}
}
The reason I guess it doesn't do what I want is because the used texture memory is all the same and used total memory is even higher than it was before.
So, unless it is normal for Unity to use more texture memory when there's less textures in the scene, I guess I messed up somewhere.
The question is, how to make this work? If I'm on the wrong road, what are the best practices to Unload textures by name?
I want to change the UI image in random order. I have a gameobject in UI(canvas) containing Image component and it has null image initially. I have a script attached to it(gameobject) to change the image on run time.
using UnityEngine;
using System.Collections;
using UnityEngine.UI;
public class changeImg : MonoBehaviour {
public Sprite sprite1;
public Sprite sprite2;
public Sprite sprite3;
void Start()
{
ChangeImg();
}
void ChangeImg()
{
int rand=Random.Range(0,3);
if(rand==0)
{
gameObject.GetComponent<Image> ().sprite = sprite1;
//gameObject.GetComponent<UnityEngine.UI.Image> ().sprite = sprite1;
}
else if(rand==1)
{
gameObject.GetComponent<Image> ().sprite = sprite2;
// gameObject.GetComponent<UnityEngine.UI.Image> ().sprite = sprite2;
}
else if(rand==2)
{
gameObject.GetComponent<Image> ().sprite = sprite3;
//gameObject.GetComponent<UnityEngine.UI.Image> ().sprite = sprite3;
}
}
}
I have assigned the public field (sprite1,sprite2,sprite3) in inspector. And I tried the both option as I had commented in code. I did not get an error but also the image did not get change as I want. During runtime of a program, GameObject(to which the script is attached) has null image source as it was initially.
Use overrideSprite field instead of sprite - documentation
Unfortunately, unity ui is full of such pitfalls and it's api is totally counter-intuitive, so you have to be careful and check the docs regularly
You can also just use Image if you are in Unity 2017.3 (not sure if this works for older versions). For example:
using UnityEngine.UI;
-----
public Image ObjectwithImage;
public Sprite spriteToChangeItTo;
void Start () {
ObjectwithImage.sprite = spriteToChangeItTo;
}
Works great for me.
Have you checked the position of the Gameobject? Also the color of the image?
To change the Image from a Button, don't use GetComponent<Image> () as you can potentially get another Image component that does not belong to the button. It can also return null if the object is disabled.
Use the Button.image.sprite or Button.image.overrideSprite variable instead.
public Button pb;
public Sprite newSprite;
void Start()
{
pb.image.sprite = newSprite;
}
Or
pb.image.overrideSprite = newSprite;
It really doesn't matter which one is used. Any of these two should work.