That's right, sometimes you gotta write code that writes code. Well, I've never had to do that.
Until now.
And I'm going to show you some methods of doing it.
So come on in. Take off your shoes. Stay for awhile.
Table of Contents
The Why
Programmers Only... Sort of
The How
CodeDOM and You
K.I.S.S., Stupid
The Why
Programmers Only! Sort of...
Feel free to skip this section to get to the actual tutorial.
I know what some of you are thinking, "Why would anyone have to/want to do this? This is dumb. You're dumb." Let me explain before you grab your torches and form your mobs.
I created a little asset for Unity called InputMaster that attempts to accomplish a few things:
Replace the Input.GetAxis system with a system that does not use strings as references to axes. Instead, the system will use variables.
Allow for runtime editing of controls.
Completely replicate the functionality of Unity's Input system. IE, the control's value should scale up and down exactly the same as in Unity's system.
This is great and all, but when the means lead to ends, the system was entirely designed with programmers in mind. I want to stress that I do not consider this a design issue, or an issue at all. Most Unity assets require some programming knowledge. In general, to make more than a basic game, you're going to need some programming knowledge.
Regardless, I wanted to make a quick and easy editor for the users of the asset. Thus I had been backed into a corner, because all Controls for InputMaster are defined as variables. So now what?
Codeception.
The How
CodeDOM And You
CodeDOM is one method of doing this, and for the most part it works really well. Unfortunately, I couldn't use it because it does not support object block style declaration, and my asset relies heavily on that for cleanliness.
Here is what code that generates code that does Hello World looks like:
using System; using System.CodeDom; using System.CodeDom.Compiler; using System.IO; using System.Reflection; using Microsoft.CSharp; namespace CodeDOM_Test { class Program { static void Main(string[] args) { var topLevel = new CodeCompileUnit(); var globalNS = new CodeNamespace(); var namespaceDecl = new CodeNamespace("ThisArgumentTotallyOptional"); globalNS.Imports.Add(new CodeNamespaceImport("System")); topLevel.Namespaces.Add(globalNS); topLevel.Namespaces.Add(namespaceDecl); var classDecl = new CodeTypeDeclaration("Program"); classDecl.Comments.Add(new CodeCommentStatement(new CodeComment(@"Prints ""Hello, World!""", true))); classDecl.TypeAttributes = TypeAttributes.NotPublic; namespaceDecl.Types.Add(classDecl); var entryPointDecl = new CodeEntryPointMethod(); classDecl.Members.Add(entryPointDecl); var statementDecl = new CodeMethodInvokeExpression(new CodeTypeReferenceExpression("Console"), "WriteLine", new CodePrimitiveExpression("Hello, World!")); entryPointDecl.Statements.Add(statementDecl); using(var fw = new StreamWriter(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "HelloWorld.cs"))) { var gen = new CSharpCodeProvider(); var options = new CodeGeneratorOptions(); gen.GenerateCodeFromCompileUnit(topLevel, fw, options); } } } }
That code there will generate this code here:
//------------------------------------------------------------------------------ // // This code was generated by a tool. // Runtime Version:4.0.30319.18449 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // //------------------------------------------------------------------------------ using System; namespace ThisArgumentTotallyOptional { /// Prints "Hello, World!" internal class Program { public static void Main() { Console.WriteLine("Hello, World!"); } } }
As you can see, it takes a little getting used to, but the code required is surprisingly simple to understand, is concise, and is super flexible (except in my case of course :D)
K.I.S.S., Stupid.
I could have tried some really convoluted method of generating the code I needed to make, but then I thought of Derek Yu's mindset when he made the random level generator for Spelunky: Keep It Simple, Stupid.
My method is based around string.Format(). First we read in this template (curly braces must be doubled to escape them in C# string formatting):
using UnityEngine; using BSGTools.IO; #if (UNITY_STANDALONE_WIN || UNITY_METRO) && !UNITY_EDITOR_OSX using BSGTools.IO.Xbox; #endif public class {0} : Singleton<{0}> {{ public InputMaster Master {{ get; private set; }} {1} void Awake(){{ Master = InputMaster.CreateMaster(this); }} }}
Then we iterate through all of the created controls and for each one, grab the correct template for it's type. For example, here's KeyControl
[HideInInspector] public KeyControl {0} = new KeyControl(KeyCode.{1}, KeyCode.{2}) {{ Dead = {3}f, Gravity = {4}f, Invert = {5}, IsBlocked = {6}, IsDebugControl = {7}, Name = "{8}", Sensitivity = {9}f, Snap = {10} }};
Here's XButtonControl:
[HideInInspector] public XButtonControl {0} = new XButtonControl({1}, XButton.{2}, XButton.{3}) {{ Dead = {4}f, Gravity = {5}f, Invert = {6}, IsBlocked = {7}, IsDebugControl = {8}, Name = "{9}", Sensitivity = {10}f, Snap = {11} }};
Here's XButtonControl for when an XButton control is created for multiple controllers:
[HideInInspector] public XButtonControl[] {0} = XButtonControl.CreateMultiple({1}, new XButtonControl({2}, XButton.{3}, XButton.{4}) {{ Dead = {5}f, Gravity = {6}f, Invert = {7}, IsBlocked = {8}, IsDebugControl = {9}, Name = "{10}", Sensitivity = {11}f, Snap = {12} }});
The code that actually puts it all together is right here in this method:
void CreateInputManager() { var templateText = File.ReadAllText(Application.dataPath + MASTER_TEMPLATE_LOCATION, Encoding.Default); var sb = new StringBuilder(); var kcs = controlList.Keys.OfType(); if(kcs.Count() > 0) { sb.AppendLine("\t#region KeyControls"); foreach(var c in kcs) sb.AppendLine(GetKeyControlString(c)); sb.AppendLine("\t#endregion"); sb.AppendLine(); } #if (UNITY_STANDALONE_WIN || UNITY_METRO) && !UNITY_EDITOR_OSX var xbs = controlList.Keys.OfType(); if(kcs.Count() > 0) { sb.AppendLine("\t#region XButtonControls"); foreach(var c in xbs) sb.AppendLine(GetXButtonControlString(c)); sb.AppendLine("\t#endregion"); sb.AppendLine(); } var xss = controlList.Keys.OfType(); if(kcs.Count() > 0) { sb.AppendLine("\t#region XStickControls"); foreach(var c in xss) sb.AppendLine(GetXStickControlString(c)); sb.AppendLine("\t#endregion"); sb.AppendLine(); } var xts = controlList.Keys.OfType(); if(kcs.Count() > 0) { sb.AppendLine("\t#region XTriggerControls"); foreach(var c in xts) sb.AppendLine(GetXTriggerControlString(c)); sb.AppendLine("\t#endregion"); sb.AppendLine(); } #endif var finalText = string.Format(templateText, scriptName, sb.ToString()); File.WriteAllText(string.Format("{0}/{1}.cs", Application.dataPath, scriptName), finalText); AssetDatabase.Refresh(); this.Close(); } string GetKeyControlString(KeyControl control) { var argList = new object[]{ controlList[control].varName, control.Positive, control.Negative, control.Dead, control.Gravity, control.Invert.ToString().ToLower(), control.IsBlocked.ToString().ToLower(), control.IsDebugControl.ToString().ToLower(), control.Name, control.Sensitivity, control.Snap.ToString().ToLower() }; var format = File.ReadAllText(Application.dataPath + KEYCONTROL_TEMPLATE_LOCATION); return string.Format(format, argList); } #if (UNITY_STANDALONE_WIN || UNITY_METRO) && !UNITY_EDITOR_OSX string GetXButtonControlString(XButtonControl control) { var meta = controlList[control]; var multi = meta.createCount > 1; var argList = (multi) ? new object[]{ meta.varName, meta.createCount, control.ControllerIndex, control.Positive, control.Negative, control.Dead, control.Gravity, control.Invert.ToString().ToLower(), control.IsBlocked.ToString().ToLower(), control.IsDebugControl.ToString().ToLower(), control.Name, control.Sensitivity, control.Snap.ToString().ToLower() } : new object[]{ meta.varName, control.ControllerIndex, control.Positive, control.Negative, control.Dead, control.Gravity, control.Invert.ToString().ToLower(), control.IsBlocked.ToString().ToLower(), control.IsDebugControl.ToString().ToLower(), control.Name, control.Sensitivity, control.Snap.ToString().ToLower() }; var format = File.ReadAllText(Application.dataPath + ((multi) ? XBUTTONCONTROLMULTI_TEMPLATE_LOCATION : XBUTTONCONTROL_TEMPLATE_LOCATION)); return string.Format(format, argList); } string GetXStickControlString(XStickControl control) { var meta = controlList[control]; var multi = meta.createCount > 1; var argList = (multi) ? new object[]{ meta.varName, meta.createCount, control.ControllerIndex, control.Stick, control.Dead, control.Gravity, control.Invert.ToString().ToLower(), control.IsBlocked.ToString().ToLower(), control.IsDebugControl.ToString().ToLower(), control.Name, control.Sensitivity, control.Snap.ToString().ToLower() } : new object[]{ meta.varName, control.ControllerIndex, control.Stick, control.Dead, control.Gravity, control.Invert.ToString().ToLower(), control.IsBlocked.ToString().ToLower(), control.IsDebugControl.ToString().ToLower(), control.Name, control.Sensitivity, control.Snap.ToString().ToLower() }; var format = File.ReadAllText(Application.dataPath + ((multi) ? XSTICKCONTROLMULTI_TEMPLATE_LOCATION : XSTICKCONTROL_TEMPLATE_LOCATION)); return string.Format(format, argList); } string GetXTriggerControlString(XTriggerControl control) { var meta = controlList[control]; var multi = meta.createCount > 1; var argList = (multi) ? new object[]{ meta.varName, meta.createCount, control.ControllerIndex, control.Trigger, control.Dead, control.Gravity, control.Invert.ToString().ToLower(), control.IsBlocked.ToString().ToLower(), control.IsDebugControl.ToString().ToLower(), control.Name, control.Sensitivity, control.Snap.ToString().ToLower() } : new object[]{ meta.varName, control.ControllerIndex, control.Trigger, control.Dead, control.Gravity, control.Invert.ToString().ToLower(), control.IsBlocked.ToString().ToLower(), control.IsDebugControl.ToString().ToLower(), control.Name, control.Sensitivity, control.Snap.ToString().ToLower() }; var format = File.ReadAllText(Application.dataPath + ((multi) ? XTRIGGERCONTROLMULTI_TEMPLATE_LOCATION : XTRIGGERCONTROL_TEMPLATE_LOCATION)); return string.Format(format, argList); }
If you're interested, here's the full editor script (InputMaster is completely open source).
Back in the swing of things with Adventures In Perfection. There is a pretty good reason that I stopped working on it for a while. Classic developer burn-out. Not the awesome racing game, but the act of becoming so overwhelmed with a project that it causes the developer to stop working on it altogether. And, I've fixed it! To understand how and why, all you need is your imagination.
The first build of Adventures in Perfection involved the main play structure rotating clockwise 90 degrees to progress. That looked cool, but the skybox wasn't rotating, so it looked VERY obvious that the structure was rotating, which is what I didn't want.
The game, up until five minutes ago, had the main camera rotating counter-clockwise around the structure, with the structure remaining still. This looked SO cool. Unfortunately, this threw a LOT of the logic for flinging the player straight out of the driver-side door of the car of simplicity and bouncing along the paved highway of complexity.
This is the part that requires imagination. Get your thinking caps ready.
Think about the game, with the camera facing the structure on side 1. That camera is facing the XY plane, with Z being away and towards you. When you flick the player, he is moving vertically on the Y axis and horizontally on the X axis. Once you complete that side, the camera rotates 90 degrees counter-clockwise around the structure to side 2. Now the camera is on the ZY plane, with -X being away from you and +X towards you. NOW when you flick the player, he is moving vertically on the Y axis and horizontally on the Z axis. This would be fine, except when you draw a line on the screen, that is in X,Y coordinates of the screen. This needs to be translated to the new direction of the fling. The game doesn't know this by itself (obviously) so you need to assign properties to each side to describe how the fling system should translate the line direction to the fling direction. When you go to side 3, its back to the XY plane, except for that the X direction is now flipped, so the fling system needs to know this too. When you get to side 4, the ZY plane is in use, except for that the Z direction is now flipped.
This caused a lot of problems that needed fixing in the fling system. That wasn't too bad to fix, but NOW I've begun to implement the 3D gameplay flinging. This became too much to handle, and I stopped developing.
I've fixed this quite simply.
The skybox is now a pseudo skybox, 4 quads facing inwards with a camera in the center that ONLY renders the quads. The main camera remains still, the camera that renders the pseudo skybox rotates -90 degrees while the structure is back to rotating 90 degrees. This looks EXACTLY the same, and allows the coordinate systems to match up.
Now, I am back, full-speed,
bringing you fresh Adventures in Perfection content.
Hey guys. The site is back up, so the GIFs are BACK IN ACTION.
I'm currently working on the 3D gameplay mode (still no better name for it). It's a little iffy because of the UI changes that need to take place when the game switches. Code is kind of all over the place. Thanks to MonoEvent, some things have become much cleaner, and much more reliable.
The GIFs in posts past may be missing (I know they're missing). My little raspberry pi that's running the BirdStreet Games site is down for a little snooze, and that's where those GIFs are located.
Not related to any game, but I figured it’d be cool to mention that I finally finished my first chiptune song. I’ve created many, and finished but just this one.
a post for a game I've been developing on the IndieGaming subreddit on Reddit. I cannot believe the responses. A lot of people really want to play this game.
The working title of this game, and the title of this post, is Adventures In Perfection. This was the gameplay GIF I posted on Reddit:
Basically the game is about moving around a cube that exists in a world that coexists in two different dimensions. The player is a black ball of sorts. You use fling style controls to get the player from point A to point B. The little yellow rotating sphere is what I like to call a Nullified Gravity Field, or NGF for short. The aqua colored version of that is the goal. Landing there causes the cube to spin about, and allowing the player to progress.
Many things have changed between what you see above (hereafter referred as version 0.07) and the newest build (hereafter referred as version 1.1), not including the many bug fixes. In 0.07, the playfield rotates 90 degrees when the player completes a side. In 1.1, the camera rotates around the center of the playfield. This may seem like a trivial change, but I will expand as to why this is important later (as well as the many problems this created within the underlying physics). The player is no longer a primitive sphere, but a custom 3D model I made for compatibility issues with shaders. The shader issue was due to my need for transparency (the little cut out sections of the player) and my desire for a beautiful looking cubemap based reflection of the environment on the player. That looks a little like this:
I also implemented a custom Coroutine based event system for 1.1.
Enough talk. Before you see a GIF of the newest version, there are some things you, the reader, should know:
The art style hasn't changed much. This is pre-alpha. Please keep that in mind. Art is not important at this stage, and being primarily a programmer, it's still as ugly as the 0.07.
The player is harder to see. This is due to the cubemap reflecting the skybox as part of the player's material. I will have to add something to fix this.
As an addendum to #1, most of the textures are from the Unity asset store plugin ProBuilder. These will not be there in the final version. I do not own these textures.
Okay? Here. We. Go!
It should be fairly obvious as to why the camera now rotates around the cube, instead of the cube rotating itself. In the case that it is not obvious, please refer to the below text:
The skybox looks pretty cool when the camera rotates.
Anyway, that's really all I have to show at this point. I do not want to say too much as to what I am currently adding. I will say this: the game is currently running on Android, Blackberry and PC/Mac/Linux (the latter two are untested). I plan on releasing this for iOS as well. 3D gameplay is coming. This is currently what I'm working on. It was a highly requested feature from the comments of the reddit post above. People want to be able to shoot the ball through the cube itself, hit something on the other side, and bounce it back. This will definitely not be an easy task, but I agree that it nonetheless be cool as fuck.
Thanks for reading guys.
I cannot wait to release this into the wild as my first ever launched game. Catch you on the dᴉlɟ side!
One more for good luck. I've been working with a team on something. Yes, that's as much as you'll know. I found that while my really fancy message based event system with a full GUI was nice, it kind of sucked. The custom editor GUI took FOREVER to get working, as it was my first time doing such a thing. I decided instead to do an event system that would let me create events in a way I'm used to. Code.
"What's that?", you say? "Code a system that will allow you to code? Get out of my face you fish-faced disaster, if anyone asks, I've never heard of you".
Shut up. This single abstract class makes scripting events a breeze. Clean, tidy, spotless, synonym.
Let's take a look at how to use it. The key method to take advantage of here is the abstract method InitEvent(). This method simply returns an array of Coroutines. I will NOT be going into how to use coroutines, so please look elsewhere for this information as it exists in abundance.
Here's a quick script to demonstrate it's usefullnessess:
So what is happening here? I'll tell ya what. When OnAwake is called in the base class, it calls InitEvent(), getting the list of Coroutines that you provided. It adds that list to a queue and when you call StartEvent() (another base class method), it executes them one by one in it's Update method (the base class is a MonoBehaviour). The cool thing is, it doesn't execute them all at the same time. It waits for the current one to finish before moving on. You'll notice that there is also no "Delay" method in this class, even though it is present in InitEvent(). That's another base class utility method. It works into the system in the same way. You can add in these delays between events for perfect timing. The example above is the most basic example. Here's one that's a little more involved, using more features from the base class:
It's been a while since I last posted. I've been working on another type of project that I'm not quite ready to link to myself yet publically. The horror game is more or less on hold due to 0 assets. I've been messing around with other ideas, including a really cool idea for a 2D western shooter. I'm currently fleshing out the story. I'm not sure what the final name will be, but I'm currently calling it "Pages From The Sand".
I'll be updating this blog again soon with more info.
I would take some time and explain a little bit how the item interaction in our horror game works on a lower level than just a video:
Table of Contents
Making The Player Script
The Abstract Class
The "T" Word
Final Words
Important Notes:
We use the entire SixBySeven Tools collection and iTween plugins for our game, and some screenshots and lines of code will use these plugins. However, you can do everything here using vanilla Unity. We do recommend at least using iTween, as it is a fabulous, free plugin.
Unless otherwise specified (almost never), all scripts are C# scripts.
Most of my explanations and tutorials will take place directly in the source code as comments.
Alright, first, a quick outline of how it all works (I will elaborate on each of these later):
Create a ray-cast from the center of the player's camera in the direction the camera is facing, at a specified distance.
Surround all objects that are interactable with an "Interactable Trigger" prefab.
Check if the ray-cast from step one is penetrating any triggers at the moment. Depending on certain input activity, trigger certain events.
Not too bad... right?
Making The Player Script
Let's start with step one, configuring the ObjectInteractor script. It's a regular MonoBehaviour, and is actually pretty simple. Here's our update method:
And then, we have the Highlight and DehighlightObj methods!
Here's what the player's "reach ray" looks like:
The Abstract Class
Step Two: Implementing AbstractInteractable into the mix:
Step Three: Make an item that extends AbstractInteractable:
The "T" Word
I had initially forgotten this part in the tutorial, but I'm back to add it!
The final step involves setting up an InteractableTrigger prefab. You need to set its layer to something unique. I decided to be boring and just go for "InteractableTrigger".
Make sure that even though its technically a trigger in practice, that you turn off "Is Trigger" under its collider component.
Then (unless I forgot anything), the last step is to disable physics for the InteractableTrigger. You do that under Edit -> Project Settings -> Physics:
Now, if this is a trigger for an object that physically moves in your environment, such as a door (or in my case, keys have rigidbodies), make sure the trigger is set up in the hierarchy to be a direct descendant of the moving part of your object:
Final Words
And finally, a demonstration video!
Thanks guys,
I hope this helps anyone that was struggling with implementing a system like this. Fortunately, it really doesn't take much to make a rock solid system that can be enhanced and expanded very, very easily.