- Home /
How to render a lot of the same small objects
In my game I want to create a pressure plate trap of which the user is able to set at runtime. The player should be able to set the trap on a specific part of a tile. Having every brick of a tile as seperate gameobject can be quite heavy in the amount of batches, in terms of performance this would be bad especially if you have a big floor. So to solve that I want to use a more combined mesh but still be able to raycast for the individual tile parts so they can still be manipulated. So kind of batched for the GPU to render but still able to manipulate them individually using a raycast to identify the tile part.
To visualize what I want to create, see the screenshot below. The part marked in red I have a empty parent GO and each tile piece is an individual GO named TilePart. They both already have a trap that is triggered, one of their tiles is slightly pushed down. If I were to duplicate these tiles extensively, my batch count would go up by a lot which will kill performance.
On the left side I have a tile, which is 1 single mesh but now I am unable to select the individual tile part using raycasting, because it is no longer a seperate object.
So I am quite uncertain how to solve this problem. I kind of don't want to end up with a lot of loose mesh parts because of performance (high batch count, imagine a lot of these duplicated to form a floor). But I do need to be able to select and manipulate parts of a mesh else the user cannot select a specific part to place the trap.
Edit 1: ok so I instantiated a floor using a prefab with every brick as child. I even used a GPU instanced material to keep the batch count low. But the framerate will still drop after instantiating a lot of them? So I would still need to Mesh Combine to solve that issue. But then I cannot manipulate them individually.
@$$anonymous$$asked$$anonymous$$ouse I generated 40,000 cubes just fine with no lag. All at once of course it will lag. That is why I instantiate them during a load screen. Then I do what I want to the objects I instantiated. I do this by putting them in a public list that I can access using any script. Then I do what I need to do. Can you fill me in on more of what you want so I can come up with a example script that fits your needs?
Of course the generation isn't the problem, but rather having them all on screen is. With GPU Instancing I kept the batches low, like 70 batches or so, still having them all on screen lags.
Basically what I want to do is create a large floor of individual bricks. The "runtime editor" player who looks over the whole place, should be able to select one of the bricks on the floor and change it into a trap. Once the "runner" player is walking over such a brick it should move down a little bit (cause you stepped on it, activating the trap).
As I have in the screenshot, I want to use the right side setup where I can manipulate each individual bricks. The left hand side is 1 single mesh, I cannot manipulate individual bricks (orange). The right side prefab would lag, the left side prefab would not do so as quickly. Even though both ways, they both use GPU Instancing, having a low batch count, low set pass count. The merged version outperforms the separated version.
$$anonymous$$aybe another solution would be, being less precise. Using the left side prefab but use the whole tile as a trap rather than individual bricks. $$anonymous$$aybe I am being too ambiguous :)
Thanks for the replies so far though!
Answer by MrMatthias · Apr 26, 2018 at 12:02 PM
you could use colliders, or other helper objects that don't get rendered
Well I can combine the mesh and use colliders for each part. But then I still have a problem, how am I going to move the mesh down after it is combined?
I should be able to change it there, but I have no clue how. So supposedly the right top tile, each tilePart would not be a meshrenderer but rather only a box collider so I can identify the part of the tile. The parent "Tile" gameobject will then be the mesh renderer, combined of all parts.
But how would I still be able to go from the right bottom tile state to right top tile state. Because when a mesh is combined, I have no control over the individual parts anymore.
What I am kind of searchong for is like.. Give the gpu the combined mesh but still be able to manipulate it in parts. With gpu instancing it doesn’t rlly seem to do that
Answer by KittenSnipes · Apr 27, 2018 at 11:31 AM
@MaskedMouse Here is a huge example:
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class BuildSystem : MonoBehaviour {
//Enum holding the types of possible build modes
public enum BuildModes
{
None,
Build,
Edit,
}
[Header("Key Used To Place Prefabs")]
public KeyCode buildKey = KeyCode.N;
[Header("Key Used To Edit Placed Prefabs")]
public KeyCode editKey = KeyCode.M;
[Header("List Of All Placeable Prefabs")]
public List<GameObject> buildables;
//These two public variables make up my build menu
public Canvas BuildMenu;
public Dropdown selector;
//All objects that have been currently placed
List<BuildObject> buildObjects;
//Currently selected placeable prefab
GameObject chosenPrefab;
//Reference of the parent object
Transform parentObj;
//Current build mode
BuildModes mode = BuildModes.None;
//Boolean variables of our current build statuses
bool build = false;
bool edit = false;
bool currentlyEditing = false;
/// <summary>
/// Sets current build mode
/// </summary>
public void Mode(BuildModes mode)
{
//Sets our boolean statuses based on build mode
switch (mode)
{
case BuildModes.None:
build = false;
edit = false;
break;
case BuildModes.Build:
build = true;
edit = false;
break;
case BuildModes.Edit:
build = false;
edit = true;
break;
}
}
void Start()
{
//Clears our lists for a fresh start
buildObjects = new List<BuildObject>();
selector.options = new List<Dropdown.OptionData>();
//This fills our build list with all currently buildable prefabs
for (int i = 0; i < buildables.Count; i++)
{
selector.options.Add(new Dropdown.OptionData(buildables[i].name, null));
}
//This sets our current buildable object to the start of our list
chosenPrefab = buildables[selector.value];
}
void Update()
{
//If we are pressing the build key
if (Input.GetKeyDown(buildKey))
{
//Set our mode accordingly:
//If we are already in build mode
if (mode == BuildModes.Build)
{
//Disable it
mode = BuildModes.None;
Debug.Log("Build Mode Disabled!");
}
//Else
else
{
//Enable build mode
mode = BuildModes.Build;
Debug.Log("Build Mode Enabled!");
}
}
//If we are pressing the edit key
else if (Input.GetKeyDown(editKey))
{
//Set our mode accordingly:
//If we are already in edit mode
if (mode == BuildModes.Edit)
{
//Disable it
mode = BuildModes.None;
currentlyEditing = false;
Debug.Log("Edit Mode Disabled!");
}
//Else
else
{
//Enabled edit mode
mode = BuildModes.Edit;
Debug.Log("Edit Mode Enabled!");
}
}
//Set up our booleans
Mode(mode);
//If editing
if (edit)
{
//If pressing left click
if (Input.GetMouseButtonDown(0))
{
//If we are not currently editing
if (currentlyEditing == false)
{
//Setup a raycast
RaycastHit hit;
//And a ray from our camera to the clicked mouse position
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
//And if our raycast hits something
if (Physics.Raycast(ray, out hit))
{
//And the raycasts parent isnt null
if (hit.transform.parent != null)
{
//Set our parent object
parentObj = hit.transform.parent;
//If our parent object is a build object
if (ContainsBuildObject(parentObj.gameObject))
{
//Allow it to be edited
EditMode(parentObj.gameObject);
}
}
//If the raycast isnt null
else if (hit.transform != null)
{
//Set it as our parent object
parentObj = hit.transform;
//If our parent object is a build object
if (ContainsBuildObject(parentObj.gameObject))
{
//Allow it to be edited
EditMode(parentObj.gameObject);
}
}
//Else
else
{
//Our parent object doesnt exist and is null
parentObj = null;
}
}
//We are editing an object so we are currently editing
currentlyEditing = true;
}
}
//If we are currently editing and are pressing right click
else if (Input.GetMouseButtonDown(1) && currentlyEditing)
{
//If the parent object has children
if (GetBuildObjectFromGameObject(parentObj.gameObject).GetChildObjects() != null)
{
//And the child count is greater than 0
if (GetBuildObjectFromGameObject(parentObj.gameObject).GetChildObjects().Count > 0)
{
//Loop though the child objects
for (int j = 0; j < GetBuildObjectFromGameObject(parentObj.gameObject).GetChildObjects().Count; j++)
{
//Setup a raycast
RaycastHit hit;
//And a ray from our camera to the clicked mouse position
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
//If our raycast hits something
if (Physics.Raycast(ray, out hit))
{
//If the hit object is a child of the parent
if (hit.transform.gameObject.Equals(GetBuildObjectFromGameObject(parentObj.gameObject).GetChildObjects()[j].GetObject()))
{
//And if it is not a trap
if (GetBuildObjectFromGameObject(parentObj.gameObject).GetChildObjects()[j].GetIsTrap() == false)
{
//Set it as a trap
GetBuildObjectFromGameObject(parentObj.gameObject).GetChildObjects()[j].SetIsTrap(true);
Debug.Log("Trap Set!");
}
//Else
else
{
//Disable the trap
GetBuildObjectFromGameObject(parentObj.gameObject).GetChildObjects()[j].SetIsTrap(false);
Debug.Log("Trap Deactivated!");
}
}
}
}
}
}
}
}
//If we are not editing
else
{
//And we have built objects
if (buildObjects.Count > 0 && buildObjects != null)
{
//Loop through the built objects
for (int i = 0; i < buildObjects.Count; i++)
{
//If the built objects have children
if (buildObjects[i].GetChildObjects() != null)
{
//And the child count is greater than 0
if (buildObjects[i].GetChildObjects().Count > 0)
{
//Loop through the children
for (int j = 0; j < buildObjects[i].GetChildObjects().Count; j++)
{
//And parent them back if they were edited
buildObjects[i].GetChildObjects()[j].GetObject().transform.parent = buildObjects[i].GetBuildObject().transform;
}
}
}
}
}
}
//If we are building
if (build)
{
//Enabled our build menu
BuildMenu.enabled = true;
//Set up our game object to build based on what has been selected
chosenPrefab = buildables[selector.value];
//If we are left clicking
if (Input.GetMouseButtonDown(0))
{
//Setup a raycast
RaycastHit hit;
//And a ray from our camera to the clicked mouse position
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
//If our raycast hit something
if (Physics.Raycast(ray, out hit))
{
//Create an instance of our currently selected buildable prefab at that point clicked
GameObject obj = Instantiate(chosenPrefab, hit.point, Quaternion.identity);
//Set the created prefab as a built object
BuildMode(obj);
}
}
}
//If we are not building
else
{
//Disable the build menu
BuildMenu.enabled = false;
}
}
/// <summary>
/// Converts given object in to a build object
/// </summary>
public void BuildMode(GameObject obj)
{
//Reference of new built object
BuildObject ofb;
//If the built object exists
if (ContainsBuildObject(obj))
{
//We say so
ofb = GetBuildObjectFromGameObject(obj);
}
//Else
else
{
//Setup a new built object using the game object
ofb = new BuildObject(obj);
//Make a new list for the possible children of the built object
List<BuildObjectChild> objects = new List<BuildObjectChild>();
//Loop through the possible children
for (int i = 0; i < obj.transform.childCount; i++)
{
//If there is then add it to the list of children of the built object and set them up as normal blocks
//Meaning they are not traps
objects.Add(new BuildObjectChild(obj.transform.GetChild(i).gameObject, false));
}
//If the children exist
if (objects.Count > 0)
{
//Add it to the built object
ofb.SetChildObjects(objects);
}
//Our object is now a built object and will be added to the currently built objects list
buildObjects.Add(ofb);
}
}
/// <summary>
/// Returns if our object is a build object
/// </summary>
bool ContainsBuildObject(GameObject obj)
{
//Loop through the currently built objects
for (int i = 0; i < buildObjects.Count; i++)
{
//If it exists
if (buildObjects[i].GetBuildObject() == obj)
{
//return true
return true;
}
}
//Else we return false
return false;
}
/// <summary>
/// Gets a build object from the given game object
/// </summary>
BuildObject GetBuildObjectFromGameObject(GameObject obj)
{
for (int i = 0; i < buildObjects.Count; i++)
{
if (buildObjects[i].GetBuildObject() == obj)
{
return buildObjects[i];
}
}
Debug.LogError("ERROR: The List 'buildObjects' does not contain " + obj + "(Line 167)");
return null;
}
/// <summary>
/// Edit Mode: Detaches child/children of given object for editing
/// </summary>
public void EditMode(GameObject obj)
{
obj.transform.DetachChildren();
}
}
public class BuildObject
{
private GameObject buildObj;
private List<BuildObjectChild> children;
public BuildObject()
{
buildObj = null;
children = null;
}
public BuildObject(GameObject obj)
{
buildObj = obj;
children = null;
}
public BuildObject(GameObject obj, List<BuildObjectChild> co)
{
buildObj = obj;
children = co;
}
/// <summary>
/// Gets the game object of a build object
/// </summary>
public GameObject GetBuildObject()
{
return buildObj;
}
/// <summary>
/// Sets the game object of a build object
/// </summary>
public void SetBuildObject(GameObject obj)
{
buildObj = obj;
}
/// <summary>
/// Gets the children of a build object
/// </summary>
public List<BuildObjectChild> GetChildObjects()
{
return children;
}
/// <summary>
/// Sets the children of a build object
/// </summary>
public void SetChildObjects(List<BuildObjectChild> co)
{
children = co;
}
}
public class BuildObjectChild
{
private bool isTrap;
private GameObject child;
public BuildObjectChild()
{
child = null;
isTrap = false;
}
public BuildObjectChild(GameObject obj)
{
child = obj;
isTrap = false;
}
public BuildObjectChild(GameObject obj, bool status)
{
child = obj;
isTrap = status;
}
/// <summary>
/// Gets the gameobject of a build object's child
/// </summary>
public GameObject GetObject()
{
return child;
}
/// <summary>
/// Sets the gameobject of a build object's child
/// </summary>
public void SetObject(GameObject obj)
{
child = obj;
}
/// <summary>
/// Gets the gameobject of a build object's child current trap status
/// </summary>
public bool GetIsTrap()
{
return isTrap;
}
/// <summary>
/// Sets the gameobject of a build object's child current trap status
/// </summary>
public void SetIsTrap(bool status)
{
isTrap = status;
}
}
In order for this script to work you must have a canvas a UI Element attached to it called a Dropdown
@$$anonymous$$ittenSnipes I appreciate the work you've put into this. Like I said generation isn't the problem. I am more trying to tackle the problem of rendering than creation. For creation I'm eventually going to save them as modular prefab pieces. Let the user use that to create his level. I am more worried about rendering while keeping an eye on the level. That level may be quite huge.
So here's two screenshots, one prefab with 1 mesh, one prefab with 5 meshes. Both creating similar floors (slightly different, but neglectable). one is 30 fps, other is 90+
Problem with a merged mesh is, that I don't have control over the individual bricks because the mesh is merged into 1 floor tile, sure I can put a box colliders on it to resemble the tile. But OnCollisionEnter I cannot tell the individual brick to move it's Y value down because it is 1 single mesh.
Problem with individual bricks as a mesh is that the performance is 30 fps where my target would be 60.
What you see here is 80x80 instantiated prefabs. The prefab looks like the one in my original post screenshot. Situation 1 uses the 5 individual bricks as 1 small floor tile, situation 2 uses 1 merged mesh.
Situation 1:
Situation 2:
Would be nice if I could tell the GPU like, here have some gameobjects, they're all the same mesh, all the same material, render it in 1 go. I thought that's what GPU Instancing was doing but apparently having a lot of individual bricks it brings the FPS down to 30. $$anonymous$$erging that mesh brings the FPS up to 90 which is nice but I can't tell the individual brick anymore to move it's Y value because they're merged into 1 tile.
I've looked at Graphics.Draw$$anonymous$$eshInstanced, not sure if thats the way to go. Or just keep them as the single mesh merge and use that as trap, but lose the fiddlety of choosing an individual brick that is part of 1 tile as trap.
$$anonymous$$aybe the original question wasn't clear I'll update the title.
@$$anonymous$$asked$$anonymous$$ouse
I will figure out a way. I would like to know for my own learning
@$$anonymous$$asked$$anonymous$$ouse
I edited my answer. When editing it will take apart one big prefab and access it's children so you can make them traps. Then when it is all done it will turn in to one big prefab again.
Your answer
Follow this Question
Related Questions
Mesh deformation with collisions 2 Answers
How to check if a raycast is not hitting any tagged colliders? 0 Answers
How do i make an object move around a 3D mesh? 1 Answer
Using raycast for visible bullet collision (not oncollisionenter) in C# 1 Answer
What's a reliable way to detect if an object is no longer being hit by a ray? 1 Answer