- Home /
Snap blocks that aren't cubes.
So I've seen dozens of questions asking how to snap blocks together Minecraft-style and that works great for cubes, but I am building with blocks that are not just 1 x 1 cubes. Some are 1x2, some are 0.5x0.5. Some are triangles, arches, cylinders, and other such shapes. Most have notional space of either a cube, half cube, or double cube, though. I can get the snapping to work beautifully for ones that have the same bounds as a cube, but it's the non-standard ones that are giving me problems. I have tried many approaches and nothing seems to work.
I've tried collider.bounds, and this works some of the time, but because I have to be able to rotate the blocks on all three axes, I can't quite figure out the right code to make it snap in the right direction. So say I have a 1x2 block. If it's standing upright, it works fine. If it's on its side, it will stick into its neighbor. Often they either float in the air, clip through another block, or position themselves offset from where they're supposed to be. All pivots are centered and I'm sure this has a lot to do with it. Do I just have to run separate checks for each size and rotation? Because that seems like a pain when I have 50+ different kinds of blocks. :/
Because I am using ProBuilder, I have tried reconstructing the blocks out of 1x1 cubes with various combinations of colliders and no colliders on parents and children to see if maybe I could try to fake the effect, but that didn't work either. Raycasting from all directions doesn't seem like it would work because I would still need to know which way the object is oriented, right?
I don't know. I'm at a loss here and am about at the point of maybe cutting out odd-shaped blocks if I can't get it to work. Anyone have any ideas?
Below is the code I've been working with so far. This one actually works for 1x1, but not for others:
using UnityEngine;
using System.Collections;
public class BlockPlacement : MonoBehaviour {
public GameObject currentBlock;
public GameObject currentBlockPrefab;
public LayerMask tileMask;
public LayerMask blockMask;
public Vector3 blockSize = Vector3.one;
[Tooltip("If checked, blocks will snap to a fixed value.")]
public bool snapBlockPlacement = true;
[Tooltip("The value to incremenent snapping to, e.g. 0.25.")]
public float snapValue = 0.25f;
public Material defaultMaterial;
public Material blockedMaterial;
[HideInInspector]
public bool canPlace;
Vector3 colliderSize;
Vector3 pos;
float offset;
WeaponManager weMan;
ColorManager col;
SelectionManager sel;
UI_Manager uiMan;
BlockTypeManager BTM;
void Start () {
canPlace = true;
col = GetComponent<ColorManager> ();
weMan = GetComponent<WeaponManager> ();
sel = GetComponent<SelectionManager> ();
uiMan = GetComponent<UI_Manager> ();
BTM = GetComponent<BlockTypeManager>();
if (currentBlock != null) {
blockSize = currentBlock.transform.localScale;
colliderSize = currentBlock.collider.bounds.extents;
}
}
// Update is called once per frame
void Update () {
// Check if the block can be placed.
CheckPlacement ();
// Check if you have a block.
if (currentBlock != null) {
//offset = currentBlock.collider.bounds.extents.y;
blockSize = currentBlock.transform.localScale;
colliderSize = currentBlock.collider.bounds.extents;
}
Ray ray = Camera.main.ScreenPointToRay (new Vector3 (Screen.width / 2, Screen.height / 2, 0f));
RaycastHit hit;
if (weMan.currentWeapon == WeaponManager.Weapon.Hand && BTM.currentType != BlockTypeManager.BlockType.Empty && currentBlock != null) {
// Place block on another block.
if (Physics.Raycast (ray, out hit, sel.range, blockMask)) {
pos = hit.transform.position + hit.normal;
currentBlock.transform.position = pos;
if (canPlace && Input.GetMouseButtonDown (0)) {
GameObject go = (GameObject)Instantiate (currentBlockPrefab);
go.transform.position = pos;
go.transform.rotation = currentBlock.transform.rotation;
go.renderer.sharedMaterial.color = col.currentColor;
go.layer = LayerMask.NameToLayer ("Block");
go.collider.enabled = true;
}
}
// Place block on a tile.
else if (Physics.Raycast (ray, out hit, sel.range, tileMask)) {
pos = hit.point + hit.normal * offset;
currentBlock.transform.position = snapBlockPlacement ? new Vector3 (Mathf.Round (pos.x) * snapValue, pos.y, Mathf.Round (pos.z) * snapValue) : pos;
if (canPlace && Input.GetMouseButtonDown (0)) {
GameObject go = (GameObject)Instantiate (currentBlockPrefab);
go.transform.position = currentBlock.transform.position;
go.transform.rotation = currentBlock.transform.rotation;
go.renderer.sharedMaterial.color = col.currentColor;
go.layer = LayerMask.NameToLayer ("Block");
go.collider.enabled = true;
}
}
// Place block on outer wall.
else if (Physics.Raycast (ray, out hit, sel.range)) {
if (hit.transform.tag == "Wall") {
pos = hit.point + hit.normal;
currentBlock.transform.position = snapBlockPlacement ? new Vector3 (Mathf.Round (pos.x) * snapValue, Mathf.Round (pos.y) * snapValue, Mathf.Round (pos.z) * snapValue) : pos;
if (canPlace && Input.GetMouseButtonDown (0)) {
GameObject go = (GameObject)Instantiate (currentBlockPrefab);
go.transform.position = currentBlock.transform.position;
go.transform.rotation = currentBlock.transform.rotation;
go.renderer.sharedMaterial.color = col.currentColor;
go.layer = LayerMask.NameToLayer ("Block");
go.collider.enabled = true;
}
}
}
else {
currentBlock.transform.position = new Vector3 (0f, -5f, 0f);
}
}
else {
currentBlock.transform.position = new Vector3 (0f, -5f, 0f);
}
}
// Check if the current block can be placed.
public void CheckPlacement () {
currentBlock.collider.isTrigger = true;
currentBlock.renderer.material = defaultMaterial;
currentBlock.renderer.material.color = canPlace ? Color.cyan : Color.red;
}
// Rotate block around current axis.
public void RotateBlock () {
currentBlock.transform.Rotate (uiMan.rotAxis, Space.World);
}
}
I should add that I have created separate GOs for a sort of "cursor" preview to show the block's position, which has a trigger (to prevent the FPS player from colliding with them when walking around) and then the actual blocks are instantiated as prefabs with normal colliders from the preview's position and rotation. Rotations are currently in increments of 90 degrees around one axis at a time.
I think it's just a hard problem, with no general good solution (source: personal experience); only things that work if you limit yourself in various ways (as you mentioned.) Using only Tetris-like blocks is easy -- make an Occupied array. When you want curved surfaces, that work properly, and can be closely packed -- ugg.
Well, I don't expect wedges and arcs to nest perfectly inside each other. I would be content with a 1x2 or 1x0.5 block that snaps to another block in increments of 0.5, 1, or 2.
Castle Story (which I know is made in Unity) sort of does this with their non-cube blocks like the 1x1.5 arch and door nesting one 1x1 and 1x2 blocks no matter which way they are oriented. That is sort of the effect I am looking for.
What you're looking at is probably really all 1x1 blocks (which is easy.) The 1x0.5 blocks are really 2x1, and so on. That's the Occupied array trick. You don't raycast or check the scene, since you might go through a hole or just miss a curve. You check the "voxel" array. An arch counts as a 3x2 with one center block taken out of it. They probably have special rules about where a door can go, and how it aligns to each legal piece.
Hmm, I see. So then I may have to completely rework this as I'm not currently using a voxel array, just raycasting to the collider of an existing block or tile and placing it relative to its normal in free space. Reason for that is because I have a combination of hexagonal blocks and tiles as well that kind of make grids a real pain. That sucks! :/
Thank you for the tip, though, about the arches and other blocks being all 1x1. That may be something I can work with. I will leave this open in case anyone else has other ideas while I play with that.
Damn, did you get something working?
I'm doing something similar, but am trying ( very slowly) to build snapping pegs/points like you have on the war machines in the Besiege game (http://www.besiege.spiderlinggames.co.uk/).
I think that was/is also built in Unity?
Answer by MarushiaDark · May 26, 2015 at 03:55 AM
After letting this settle for a few days and coming back to it, I came up with a potential work around and decided to post here in case anyone else is looking for a solution.
What I did, because I'm using ProBuilder, was to make sure that the extents of my block (opposite corners) contain a vertex for the collider. If it was a shape where that was not possible, I would position one vertex of the block on one extent and then position the vertex of an very tiny cube (0.0001 m) on the opposite corner, or use two of these on the extents if the main block was more centric - just something to make sure the extents both had a vertex for the collider and force the shape to be a cube even if it wasn't. I realized this was the reason blocks with the same bounds as a cube were working but others were not.
After that, I wound up merging the meshes of the shapes to form a single mesh and gave the micro cubes a transparent material to hide them.
It's not a perfect solution, but combined with the game controls of block rotation, this will function well enough (similar to the half-cube planks in Minecraft). I worry the extra geometry may come at a performance cost, even if they are invisible, but I will deal with optimization later. At least the feature is no longer broken.
Sadly, this also doesn't work for blocks that are larger than a cube. If I didn't need the exact mesh surface of the collider for other things, I could use the same technique, but in this case I may just scrap the larger blocks and let players build their own from component pieces instead now that I can make smaller ones behave how I want them to.
I will leave this open in case anyone has any further insights.