How to Make a Cylindrical Combination Lock In Unity, Part II
10th January 2025 • 7 min read
In the previous part, we created a cylindrical combination lock with four digits. We wrote all the scripts and assembled the lock manually in the editor from various assets. But what if we want to quickly generate a lock with any number of digits? We could prepare prefab variants with digits ranging from 3 to 10, for example, or we can create an editor tool to handle the assembling for us.
In today's part, we'll see that building such a tool is not difficult if we prepare modular parts cleverly. And that's where we're going to start. If you followed along in the previous part and wish to continue, open the project and briefly review the Lock structure in the scene hierarchy.
Now, create a new directory in the Assets
folder of your project and name it Prefabs
. Drag everything out of the Lock
game object in scene hierarchy, reset its position to zero, and turn the empty Lock
into a prefab by dragging and dropping it into the Prefabs
directory. Make sure that the Lock
script and the MaterialSwitcher
script, along with all assigned values and material references, remains on the Lock prefab.
Repeat the same steps for one of the cylinders, don't forget to zero their positions, and do the same also for the case parts. While turning into a prefab the Case_Middle
object, set the Y component of its scale back to 100
. In the end, you should have a total of five prefabs: Lock
, Cylinder
, Case_Side_L
, Case_Side_R
, and Case_Middle
. Again, make sure all the scripts remains on these prefabs.
LockFactoryWindow Editor Script
We now have all the prefabs we need, so let's implement an editor window script. Create a new directory in the Assets
folder and name it Editor
. It's important to place all scripts that depend on the Unity Editor API inside this directory to ensure they are excluded from the build. Inside this Editor
directory, create a new file and name it LockFactoryWindow.cs
.
Let's now implement the LockFactoryWindow
class, starting with variables for input fields with default values and GameObject
references for the five prefabs we just created.
using UnityEditor;
using UnityEngine;
public class LockFactoryWindow : EditorWindow
{
private int _triggerValue = 512;
private int _initialValue = 128;
private int _cylinders = 4;
private GameObject _cylinderPrefab;
private GameObject _lockPrefab;
private GameObject _caseLeftPrefab;
private GameObject _caseMiddlePrefab;
private GameObject _caseRightPrefab;
Notice that the LockFactoryWindow
class inherits from EditorWindow
. Let's continue by implementing the ShowWindow static method, with the MenuItem attribute to add a new option to the Unity Editor's menu as Tools → Lock Factory
.
[MenuItem("Tools/Lock Factory")]
public static void ShowWindow()
{
LockFactoryWindow window = GetWindow<LockFactoryWindow>("Lock Factory");
window.Initialize();
}
The Initialize
method we call through window
, an instance of LockFactoryWindow
, is what we need to implement next. In the Initialize
method, we'll load our prefabs using the AssetDatabase.LoadAssetAtPath static method.
private void Initialize()
{
_cylinderPrefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Cylinder/Prefabs/Cylinder.prefab");
_lockPrefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Cylinder/Prefabs/Lock.prefab");
_caseLeftPrefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Cylinder/Prefabs/Case_Side_L.prefab");
_caseMiddlePrefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Cylinder/Prefabs/Case_Middle.prefab");
_caseRightPrefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Cylinder/Prefabs/Case_Side_R.prefab");
}
Now that we have all our assets loaded, we can continue by implementing the OnGUI method. This method is responsible for drawing the GUI elements and handling user interactions within our editor window.
private void OnGUI()
{
_triggerValue = EditorGUILayout.IntField("Trigger Value", _triggerValue);
_initialValue = EditorGUILayout.IntField("Initial Value", _initialValue);
_cylinders = EditorGUILayout.IntField("Cylinders", _cylinders);
if (GUILayout.Button("Create Lock"))
{
Create();
}
}
We created our three input fields and bound them to member variables. We also added a Create Lock
button, which invokes the Create
method when pressed. Let's now implement this method.
private void Create()
{
GameObject parentLock = CreateRootObject();
CreateCase(parentLock);
for (var i = 0; i < _cylinders; i++)
{
CreateCylinder(i, parentLock);
}
}
As you can see, in this top-level Create
method, we simply call other methods for assembling the lock we yet need to implement. But before that, let's implement a simple wrapper method for unpacking prefab instances with PrefabUtility.UnpackPrefabInstance. After unpacking, our instantiated game objects will become a fully independent game objects in the scene.
private void UnpackPrefab(GameObject gameObject)
{
PrefabUtility.UnpackPrefabInstance(gameObject, PrefabUnpackMode.Completely, InteractionMode.AutomatedAction);
}
We'll be unpacking all five prefabs, so it feels more clear and readable to call UnpackPrefab(...)
five times instead of using the much longer PrefabUtility.UnpackPrefabInstance(...)
call, especially when two out of three parameters remain the same. Let's now proceed with the CreateRootObject
method.
private GameObject CreateRootObject()
{
var root = PrefabUtility.InstantiatePrefab(_lockPrefab) as GameObject;
var lockScript = root.GetComponent<Lock>();
lockScript.triggerValue = _triggerValue;
lockScript.value = _initialValue;
UnpackPrefab(root);
return root;
}
We simply instantiate our Lock
prefab, get the Lock
component using the GetComponent method, and assign the triggerValue
and value
member variables from _triggerValue
and _initialValue
, respectively. These values come from the input fields we can modify in the editor. After that, we unpack the prefab and return it, because we'll need to set it as the parent for other instances.
Next, we'll implement the CreateCase
method. This one is a bit longer, but there's nothing too complicated happening here either.
private void CreateCase(GameObject parentLock)
{
var left = PrefabUtility.InstantiatePrefab(_caseLeftPrefab) as GameObject;
left.transform.position = Vector3.back * (0.5f * _cylinders);
left.transform.SetParent(parentLock.transform);
UnpackPrefab(left);
var right = PrefabUtility.InstantiatePrefab(_caseRightPrefab) as GameObject;
right.transform.position = Vector3.back * -0.5f;
right.transform.SetParent(parentLock.transform);
UnpackPrefab(right);
var middle = PrefabUtility.InstantiatePrefab(_caseMiddlePrefab) as GameObject;
middle.transform.position = Vector3.back * ((_cylinders - 1) * 0.25f);
var scale = middle.transform.localScale;
scale.y = _cylinders * 100f;
middle.transform.localScale = scale;
middle.transform.SetParent(parentLock.transform);
UnpackPrefab(middle);
}
We instantiated the left
, right
, and middle
objects from their respective case prefabs and positioned the left and right parts next to the first and last cylinders based on the cylinder count and cylinder width, which is 0.5
units. We also centered the middle part and scaled it to wrap it around all the cylinders. Finally, we set the parent for all of them to be parentLock
, the previously created root object passed as an argument, and unpacked all three prefabs.
The last method we need to implement is the CreateCylinder
method, which we call in the for
loop inside the Create
method, passing in the current index and parentLock
game object.
private void CreateCylinder(int i, GameObject parentLock)
{
var cylinder = PrefabUtility.InstantiatePrefab(_cylinderPrefab) as GameObject;
cylinder.transform.position += Vector3.back * (0.5f * i);
cylinder.transform.SetParent(parentLock.transform);
UnpackPrefab(cylinder);
}
This will create and position the cylinders based on the current index i
and their width of 0.5
units. Now, you can go back to the Unity Editor, open the Lock Factory
window from Tools → Lock Factory
, and try creating a few different locks.
If your solution does not work as expected, you can refer to the example project on GitHub. You can also implement proper error handling for cases where the values exceed the number of cylinders, or where any value is negative, which I have omitted to keep this example straightforward.
Conclusion
And this brings us to the end of this short tutorial series on creating a cylindrical combination lock in Unity. In the previous part, we did the heavy lifting by building a solid foundation, which we leveraged today to create a custom tool for assembling locks automatically. The key takeaway here is that when you build something with the potential for modularity and automation, it's worth thinking ahead and designing your modular pieces to be easy to work with.