Introducing oQo

I mentioned it in my last post, I have a new side project!

Its name is oQo, I’m working on it with two friends, and it’s a soothing 2D puzzle game set in a minimalist world. I has a light emergent story, which you can discover by generating waves and riding on their edge to move from level to level.

We created the first prototype during the Global Game Jam 2017, where I met Maxime and Florent from the video game studio Lance. We’ve worked on it since, and we finally decided it had reached its final state and that it was time to start working on the final game itself.

You can try this prototype here, let me know how you feel about it!

Have fun!

Short update

Yup, I know, the last post on this blog is more than 6 months old. That’s a lot, especially considering that naive ambition I had to write at least one post a week. But well, if you have ever tried to maintain a blog, I’m confident you won’t blame me too hard (beside, I suspect that there aren’t too many people dying for new posts here, so I’m not really ashamed).

Now that I confessed my sins, let’s just say that 3-50 is still here, I’m still making games on my free time, even if I haven’t showed anything recently. The thing is, I have worked for about 5 months on a wargame with a game designer I met online, and there wasn’t really any gorgeous screenshots or brilliant ideas to show, since it was more the underground, very specific and targeted kind of projects. Sadly, the collaboration came to an end and “RAF” will never see the light of the day.

Sadly, did I say? Well, not so much, since it allowed me to start a new project, which is something positive… right? Of course, what what supposed to be a very short game, made in a month, is now at about 10%, after two months of work, but it’s ok, I just didn’t invest the time it deserved, and I have good hope it will be a fun, short experience that players will be able to discover at least in their browser, in some months!

Universe Clicker - working title, obviously
Universe Clicker – working title, obviously

So this is the first peek at Universe Clicker a narrative idle/clicker game about travelling in space, which you should discover a bit more soon as I progress.

But it’s not the only thing on my mind lately… More about that in a next post!

Post-Mortem Red Skies

RedSkies-logo

Post-mortem

 

A month and a half ago, I released Red Skies, a nostalgic shoot-them-up for iOS and Android. Let’s break the suspense: I’m not rich by now. However, it was far from being my goal. Some might find it odd, but it was merely an attempt to see if I could develop a complete game on my own (and with the help of an artist – and a sound designer for the last weeks), from concept to release and marketing, and see what I could learn in the process for future games. I have ambitious plans, but I didn’t want to start right away with my Big Project; I just wanted to be aware of the most common pitfalls every game dev has to face at some point.

Note that I already have some experience, since I work as a programmer in a video game studio with a few released games; this project was an opportunity for me to have a look at the other aspects of the development, such as game design, project management, marketing…

Numbers

Don’t lie, we all love numbers in post-mortems. Mine won’t be very impressive, but at least you’ll have an idea of the outcomes of this release. There was a premium version on iOS sold at about $2 without in-app purchase, and a free version on iOS and Android, with ads, containing a single in-app purchase at $2 that brings the game to the level of the paid version, removing the ads and doubling the currency earnings.

The paid version didn’t get a lot of success, with 2625 App Store views resulting in 16 sold app, for a great total of $23. Yup, not rich yet (although I don’t even know where most of them come from).

The free version had a better conversion rate, with 856 view on the App Store, translating to 144 downloads on iOS. There were 126 downloads on Android, and an astonishing total amount of 1 iap bought (on Android).

About 500 ad views earned $3.60.

And that’s it for the numbers; I don’t think it would be interesting to break that down further, since they are too low to have a real meaning.

The Big Bullet Points Lists

Now it’s time to have the famous Good/Bad lists about how the project went!

What went right

– Project management

We managed to stay quite focused on the dev for most of the project’s duration. Despite a few short periods with less efforts, there were pretty consistent progresses made, and the execution felt natural. We chose a very small scope for the game, which was a good thing in regard of the ambitions we had, and if obviously it evolved a bit while we were working on it, it didn’t grow out of control. We managed to put in the game almost everything we wanted, and on most aspects I am satisfied by the polish level.

Initially, we saw this project as a 3-months work, but it would have needed a LOT more investment from us to reach that deadline. It ended taking about 7 months, which may be a bit long for a project of that size, but it’s acceptable. The good part is that, at some point, we set a date for the release (two months before the said date), and we stuck to it. We had no imperative deadline, but I felt it was important to respect (as long as it was possible of course) our own decision.

– Unity & git

I use Unity and git in my work, and I can’t say enough how much I love those. I won’t list all of their assets, but we managed to have an efficient “team” process using them. It allowed the artist to easily test the game and tweak graphic settings, without me having to build new versions for each new element. We also had some git conflicts, because where would be the fun otherwise, but on a project of this scope, with only 2 people on it, they were quite easily resolved.

On a personal level, it gave me the opportunity to have a look at some aspects of the dev that I didn’t have to consider on previous professional projects, such as optimization (and the game runs pretty well on not-that-powerful devices).

It also was very convenient for some less pleasing features: the social integration, be it posting on social medias or using Game Center and Google Play Game Services, was made extremely easy. When Unity didn’t plain simply allow the use of it (Game Center works right out of the box), free plugins or tutos were available online and allowed me to implement all of those boring elements in a rather short time. The integration of Unity Ads was as well far easier than what I feared.

– Playtests

I never ran playtests before, but I found out it’s a great thing to do! I didn’t expect to learn so many things about the game just by watching people play it, it’s extremely precious. Even if the game isn’t the best game ever, you can feel that, because you’re standing next to them, players enjoy quite a lot testing it. I made a first session in a local game devs group, with a lot of valuable feedbacks, and a second one at a nice event in my town over two days. A lot of people tried Red Skies then, and most of them signed to the newsletter, showing interest in the future of the game. Some kids (and one or two grown-ups) didn’t even want to give up the device after a lot of tries, which was heart-warming.

In the end, it doesn’t mean that much about the quality of the game, since being face-to-face with a player changes a lot his reception of the game, but it was very good for motivation!

The online playtests however were a lot more disappointing, with only a few people willing to test the game, and even fewer taking the time to fill in the feedback sheet. What’s more, the most valuable part of the playtests is simply watching the player through the game, which obviously isn’t possible online.

What went wrong

Most of the points listed below come from the fact that I didn’t have really high hopes for this game. It was from the beginning a “small, training project”, so why bother with the boring aspects? Obviously, one won’t go very far with this mind-set, and I guess I should have try a bit harder, yet even by failing in those areas, I still feel I learned interesting things! It’s easy to say “why bother, the project has no commercial viability anyway”, but it’s definitely something I won’t tell myself for my next projects.

– Ads

It didn’t really went wrong; as I said, Unity Ads integration was the easiest thing ever. The bad side is that we tried to offer a free-to-play, ad-monetized experience to players, without any marketing knowledge of the thing. There aren’t a lot of forced ads in the game, especially if you choose sometimes to watch an optional one at the end of a run to double your income, so the model isn’t probably the worst, but I feel there are a lot of details that could have been made in a better way, in the frequency or the presentation of the ads.

What’s more, I’m completely lost when it comes to understanding how those ads bring us money. With Unity Ads, the eCPM varies freely, reaching $7 in some countries for some days, and staying to a shiny $0 in others. If this system is perfect for a noob like me, I’m not sure it’s the best solution for someone really trying to make money with an ad-supported game.

– Analytics

I’ve read enough posts on Gamasutra to know that analytics are crucial for any project, yet my analytics implementation was very, very poor. I didn’t take the time to really understand the important metrics and how to gather them, so I ended up with a bunch of events impossible to analyse in a way that make sense, and I only have a very vague idea of how players behaved in the game.

I didn’t test seriously enough my implementation of events, just putting some here and there (yes, exactly the way most articles warn you not to do), and it probably would have been easy to improve that if I dedicated a bit more time to think about it.

– Audience targeting

We didn’t spend a lot of time thinking about our audience, and I believe it’s one of the core aspects of mobile development. The game is made to appeal to achievers, with a progress curve coming from the upgrades, the skill of the player and his knowledge of the enemy waves (which are always the same), but in the same time it has nothing really original. The mechanisms are very classic, with the small exception of the bombs you can drop on the boats, and the graphics aren’t extremely compelling. All in all, our target players don’t have any good reason to seriously look at Red Skies.

Another failure in that domain is the session duration. Mobile games without very deep mechanisms should probably have short sessions, something between 3 and 5 minutes. Those timers are respected in the training mode, but it isn’t meant to be the core of the game; the infinite, main mode will require from you, once you’re starting to be good at it, 15, 20 or even 30 minutes to complete a run.

In the end, except for some players that had the motivation to really dive into it (and made amazing scores, which means they must have been nicely hooked), I think we missed most of both casual and hardcore players through some poor choices and generic look.

– Numbers, marketing and visibility

Well if you read the Numbers section, I think it speaks for itself. On the commercial side, this project is a disaster, and I’m glad I never saw it as a real mean to make money (of course, one could say it happens the other way around: if you start to make a game without wanting to make money out of it, it won’t happen). The good thing is that now, I have a personal better experience of the mobile market.

Red Skies doesn’t really stand out visually, nor in its gameplay. It’s classic, already seen, not bad in its kind (I hope), but today that’s far from being enough to gain any visibility. I sent about 50 personalized and targeted e-mails to press, got two answers and 0 review. I hoped to get at least a small article on a shmup-specialized website or two, but nothing. The game has nothing to become viral, so it literally has 0 way to gain organic growth. Since I didn’t want to invest money on marketing, there hasn’t been a lot of players, beside family and friends (who overwhelmed me with joy by being a lot more to download and rate the game than what I expected).

If I’m still not too sure about what exactly needs to be done to obtain visibility, but at least I now know what isn’t enough for that.

RedSkiesScreenshot1

In the end, I’m rather happy with how everything went. I released a game I’m not totally ashamed of, which was my primary objective; I learned a lot of things, on various subjects, and I gained some confidence on my abilities. All of those “bad” sides aren’t really so bad in my opinion, especially since I was aware of most of them, and chose not to extend the development for too long in order to fix them.

I still don’t feel totally ready for my Big Project, but I’m a bit closer to it. I’ll try to make one or two more games with similar scopes (a bit bigger, and better targeted – be it with a more stand-out game or with a niche game), in order to learn more things, and then, maybe, I’ll try to go a bit further!

Looking for playtesters!

EN: We are looking for playtesters! Come give a try to our upcoming mobile game Red Skies, an infinite side-scrolling shoot-them-up.

If you are interested and have some time to give to our game, please fill out this survey: https://goo.gl/uKQKxz

Some more images: http://3-50.net/red-skies/

 

FR : Nous cherchons des testeurs ! Venez essayer notre jeu mobile Red Skies, un shoot-them-up infini en vue de côté.

Si tu es intéressé et que tu as un peu de temps à donner au jeu, remplis ce questionnaire : https://goo.gl/T7pr96

Quelques images supplémentaires : http://3-50.net/red-skies/

 

LittleMahyem_3

Don't let them invade the screen!

Make your tools in Unity editor

The path editor for my game in progress
The path editor for my game in progress

With Unity, you can save a lot of time by creating some tools adapted to your specific needs. Right above, a small path editor I made to save me some time when creating enemy patterns in my current project, Red Skies. With just a few click, I quickly design the general shape I want, then I can perfect it using the transform coordinates.

The Unity editor offers a LOT of tools to make yours, but I will just cover some basics in this post, then it will be up to you to look at the APIs to discover hidden gems.

WARNING: This post is ALL about code, with (commented) snippets rather than theory; that’s the way I like to learn, I hope I’m not alone!


Custom inspector

The most common tool you can use to make your life easier is custom inspector.

A very basic way to improve your inspectors is to use Property Attributes, which do not require to rewrite the entire inspector, but rather make it more readable:

So simple, yet so beautiful
So simple, yet so beautiful

using UnityEngine;

 

public class MyScript : MonoBehaviour

{

    [Header(“First category”)]

    [Tooltip(“Primary name for MyScript.”)]

    public string scriptName;

    public float scriptFloat;

 

    [Header(“Second category”)]

    [Tooltip(“Secondary name for MyScript.”)]

    public string secondaryName;

}

And then you can go pretty crazy with the custom inspectors themselves. Those scripts are editor-side only (they have to inherit from Editor and be placed in a folder called Editor) and let you write what you want, how you want, in the inspector when an instance of your script is selected. There is a ton of functions to experiment with, that can be found in various editor classes such as EditorGUILayout, GUILayout, EditorGUIUtility and many others, ranging from simple to highly customizable.

I bet it could be of some use… maybe… for a weird project
I bet it could be of some use… maybe… for a weird project

using UnityEngine;

using UnityEditor;

 

//This line will automatically call your custom editor for instances of MyScript

[CustomEditor(typeof(MyScript))]

public class MyScriptInspector : Editor

{

    //Keep a reference on your style rather than regenerating them every update

    GUIStyle headerStyle;

 

    //This custom inspector will generate random ids in order to display a list to

    // the user from which he can then choose

    bool choosingNewId = false;        //Are options displayed?

    string[] newIds;                //Random ids generated to choose from

 

    public override void OnInspectorGUI()

    {

        //The member “target” allow you to grab a ref to the instance of your component

        MyScript myScriptTarget = (MyScript) target;

 

        //Init styles

        if(headerStyle == null)

        {

            InitStyles();

        }

 

        EditorGUILayout.Space();

        EditorGUILayout.LabelField(“First Category”, headerStyle);

        EditorGUILayout.LabelField(“Script id: ” + myScriptTarget.scriptName);

 

        //If player has already clicked on the “Generate new name” button,

        // then we only display the generated options

        if(choosingNewId)

        {

            for(int i = 0; i < newIds.Length; i++)

            {

                if(GUILayout.Button(newIds[i]))

                {

                    myScriptTarget.scriptName = newIds[i];

                    choosingNewId = false;

                }

            }

        }

        else if(GUILayout.Button(“Generate new name”))

        {

            newIds = new string[] { Random.Range(0, int.MaxValue).ToString(),

                Random.Range(0, int.MaxValue).ToString(),

                Random.Range(0, int.MaxValue).ToString() };

            choosingNewId = true;

        }

    }

 

    void InitStyles()

    {

        headerStyle = new GUIStyle();

        headerStyle.fontStyle = FontStyle.Bold;

        headerStyle.normal.textColor = Color.yellow;

    }

}

Note that I could have used a PopUp to display the choices in another way (there are many other solutions, I guess, but the EditorGUILayout.PopUp is pretty neat).

You just indicate the class for which you want to make your custom inspector, then the OnInspectorGUI is ready to make all your dreams come true. The EditorGUILayout and GUILayout classes are usually the more convenient to use, since you don’t have to handle the position and size of your elements, but you can use EditorGUI and GUI as well for very precise display (you could recreate, say, a color picker in the inspector by drawing a bunch of colored pixels and using the events of the mouse).

There are of course some limitations and tricky cases, but it’s up to you to find out about those according to your needs.


Editor windows

Did I speak about your custom color picker? Well why not, after all! Let’s make it in a small window, so that it doesn’t take all the space in the inspector. Editor windows use the same apis as custom inspector. To spice things up, we’ll allow the user to select the amount of color he wants to be able to choose from. (It’s not a course about color, so forgive me if my color theory isn’t perfect!)

A color picker, not the best idea for a gif… but I spent way too much time doing it to change it now
A color picker, not the best idea for a gif… but I spent way too much time doing it to change it now

using UnityEngine;

using UnityEditor;

 

public class DiscreteColorPicker : EditorWindow

{

    //Create and open a discrete color picker

    [MenuItem(“Window/Open Discrete color picker”)]

    public static void OpenDiscreteColorPicker()

    {

        DiscreteColorPicker windowInstance = ScriptableObject.CreateInstance<DiscreteColorPicker>();

        windowInstance.Init();

        windowInstance.Show();

    }

 

    int division = 1;        //Base value to calculate amount of available colors

    int colorAmount

    {

        //There are {pickerTextureHeight} base color to choose from, each generating

        // a grid of {luminosityTextureSize}*{luminosityTextureSize} tints, with

        // the last line being only black squares.

        get { return pickerTextureHeight * luminosityTextureSize * luminosityTextureSize – luminosityTextureSize + 1; }

    }

 

    Color currentBaseColor;                    //Current selected base color

 

    Vector2 currentLuminosityCoordinates;    //Coordinates of selected tint in luminosity texture

    float currentMainColorY = -1f;            //Coordinate of selected base color

 

    Texture2D pickerTexture;        //Texture containing the base colors

    Texture2D luminosityTexture;    //Texture based on the current base color with luminosity variations

    Texture2D cursorTexture;        //Texture used to display cursors

    Texture2D sampleColorTexture;    //Texture containing final color

 

    int pickerTextureHeight

    {

        get { return division * 6; }

    }

    int luminosityTextureSize

    {

        get { return division * 3; }

    }

 

    int pickerSize = 300;           //GUI object size

 

    bool draggingLuminosity;    //Is user dragging from luminosity square

    bool draggingColor;            //Is user dragging from color picker

 

    void OnGUI()

    {

        EditorGUILayout.LabelField(“Discrete color”);

 

        EditorGUI.indentLevel++;

 

        //First, a slider to decide the amount of colors

        bool needsUpdate = false;

        int newDivision = EditorGUILayout.IntSlider(“Color levels:”, division, 1, 30, GUILayout.MaxWidth(pickerSize));

        if(newDivision != division)

        {

            //If division changed, update luminosity coordinates to
adapt to new grid

            currentLuminosityCoordinates *= ((float) newDivision) / ((float) division);

            division = newDivision;

 

            //Then regenerate textures, since their size has changed

            RegenerateTextures();

            needsUpdate = true;

        }

        EditorGUILayout.LabelField(“=> “ + colorAmount + ” colors”);

        GUILayout.Space(10);

 

        //Reserve a rect in the windows, where we can draw with GUI functions, and that

        // GUILayout functions will consider as already filled

        Rect colorPickerRect = GUILayoutUtility.GetRect(EditorGUIUtility.currentViewWidth, pickerSize);

 

        //Draw luminosity texture

        Rect luminosityRect = new Rect(colorPickerRect);

        luminosityRect.x += 5;

        luminosityRect.width = pickerSize;

        GUI.DrawTexture(luminosityRect, luminosityTexture, ScaleMode.StretchToFill, false, colorPickerRect.width/((float) luminosityTextureSize));

 

        //Draw small black dot to show which color is selected

        Rect luminosityCursorRect = new Rect(luminosityRect);

        luminosityCursorRect.position += new Vector2((int) currentLuminosityCoordinates.x, (int) currentLuminosityCoordinates.y) * pickerSize / ((float) luminosityTextureSize);

        luminosityCursorRect.y = luminosityRect.height – luminosityCursorRect.y + 2*luminosityRect.y;

        luminosityCursorRect.position += new Vector2(0, -5f);

        luminosityCursorRect.size = new Vector2(5, 5);

        GUI.DrawTexture(luminosityCursorRect, cursorTexture);

 

        //Draw color picker

        colorPickerRect.x += luminosityRect.width + 10;

        colorPickerRect.width = 20;

        GUI.DrawTexture(colorPickerRect, pickerTexture);

       

        //Draw small black line to show which color is selected

        Rect colorPickCursorRect = new Rect(colorPickerRect);

        colorPickCursorRect.y = colorPickerRect.y + currentMainColorY * colorPickerRect.height;

        colorPickCursorRect.height = 1;

        GUI.DrawTexture(colorPickCursorRect, cursorTexture);

       

        //Check if user is beginning or ending a drag in one of the pickers

        if(Event.current.type == EventType.MouseDown)

        {

            if(luminosityRect.Contains(Event.current.mousePosition))

            {

                draggingLuminosity = true;

            }

            else if(colorPickerRect.Contains(Event.current.mousePosition))

            {

                draggingColor = true;

            }

            else

            {

                draggingLuminosity = false;

                draggingColor = false;

            }

        }

        else if(Event.current.type == EventType.MouseUp)

        {

            draggingLuminosity = false;

            draggingColor = false;

        }

 

        //Warning: OnGUI can be called for different event types; you only want to use some of them, since

        // during others the layout is being constructed and values will be wrong

        bool input = Event.current.type == EventType.MouseDown || Event.current.type == EventType.MouseDrag;

        if(input)

        {

            Vector2 mousePosition = Event.current.mousePosition;

            if(draggingLuminosity)

            {

                //Calculate new coordinates in luminosity texture

                Restrain(ref mousePosition, luminosityRect);

                Vector2 coordinates = mousePosition – luminosityRect.position;

                coordinates *= luminosityTextureSize / luminosityRect.width;

                currentLuminosityCoordinates = new Vector2((int) coordinates.x, luminosityTextureSize – 1 – ((int) coordinates.y));

                needsUpdate = true;

            }

            else if(draggingColor)

            {

                //Calculate new base color and regenerate luminosity texture

                Restrain(ref mousePosition, colorPickerRect);

                currentMainColorY = (mousePosition.y – colorPickerRect.position.y) / colorPickerRect.height;

                int textureY = (int) ((1f – currentMainColorY) * pickerTextureHeight);

                currentBaseColor = pickerTexture.GetPixel(0, textureY);

                RecalculateLuminosityTexture();

                needsUpdate = true;

            }

        }

 

        if(needsUpdate)

        {

            //A change has been made, sample texture has to be updated

            sampleColorTexture.SetPixel(0, 0, luminosityTexture.GetPixel((int)
currentLuminosityCoordinates.x, (
int) currentLuminosityCoordinates.y));

            sampleColorTexture.Apply();

            Repaint();

        }

 

        GUILayout.Space(10);

 

        //Draw currently selected color to sample texture

        Rect sampleColorRect = GUILayoutUtility.GetRect(1, 20);

        sampleColorRect.x += 5;

        sampleColorRect.width = pickerSize + 20;

        GUI.DrawTexture(sampleColorRect, sampleColorTexture);

 

        EditorGUI.indentLevel–;

    }

 

    //Simple helper function

    void Restrain(ref Vector2 v, Rect r)

    {

        v.x = Mathf.Min(r.x + r.width – 1f, Mathf.Max(r.x + 1f, v.x));

        v.y = Mathf.Min(r.y + r.height – 1f, Mathf.Max(r.y + 1f, v.y));

    }

 

    //Each time the base color changes, the luminosity texture has to be updated

    void RecalculateLuminosityTexture()

    {

        Color rowColor;

        Color rowGreyColor;

        for(int i = 0; i < luminosityTextureSize; i++)

        {

            //Row by row, simple lerp of two vertical gradients (white to black on the left,

            // base color to black on the right)

            rowColor = currentBaseColor * ((float) (luminosityTextureSize – i – 1)) / ((float) (luminosityTextureSize – 1));

            rowGreyColor = Color.white * ((float) (luminosityTextureSize – i – 1)) / ((float) (luminosityTextureSize – 1));

            rowColor.a = 1;

            rowGreyColor.a = 1;

            for(int j = 0; j < luminosityTextureSize; j++)

            {

                luminosityTexture.SetPixel(j, luminosityTextureSize – i – 1, Color.Lerp(rowGreyColor, rowColor, ((float) j)/((float) (luminosityTextureSize – 1))));

            }

        }

        //Don’t forget to call Apply after you change a texture, or it won’t

        // have any effect

        luminosityTexture.Apply();

    }

 

    //Regenerate all the texture, because they don’t exist yet or because

    // their size was changed

    void RegenerateTextures()

    {

        //Just create a texture and fill it with basic rainbow

        // There must be a name for that, sorry I don’t know it

        // (…and there must be a better way to do it, but it’s not the point)

        pickerTexture = new Texture2D(1, pickerTextureHeight);

        pickerTexture.filterMode = FilterMode.Point;

        Color colorInProgress = Color.red;

        float increment = 6f / ((float) pickerTextureHeight);

        for(int i = 0; i < 6; i++)

        {

            for(int j = 0; j < pickerTextureHeight / 6; j++)

            {

                int index = i * pickerTextureHeight / 6 + j;

                pickerTexture.SetPixel(0, index, colorInProgress);

                //Grab current base color to setup cursor in color picker at init

                if(colorInProgress == currentBaseColor && currentMainColorY < 0)

                {

                    currentMainColorY = 1f – (1f / (2f * pickerTextureHeight) + ((float) index) / ((float) pickerTextureHeight));

                }

                switch(i)

                {

                    case 0:

                        colorInProgress.g += increment;

                        break;

                    case 1:

                        colorInProgress.r -= increment;

                        break;

                    case 2:

                        colorInProgress.b += increment;

                        break;

                    case 3:

                        colorInProgress.g -= increment;

                        break;

                    case 4:

                        colorInProgress.r += increment;

                        break;

                    case 5:

                        colorInProgress.b -= increment;

                        break;

                }

            }

        }

        pickerTexture.Apply();

        int textureY = (int) ((1f – currentMainColorY) * pickerTextureHeight);

        currentBaseColor = pickerTexture.GetPixel(0, textureY);

 

        luminosityTexture = new Texture2D(luminosityTextureSize,  luminosityTextureSize);

        luminosityTexture.filterMode = FilterMode.Point;

        RecalculateLuminosityTexture();

 

        sampleColorTexture = new Texture2D(1, 1);

        sampleColorTexture.SetPixel(0, 0, luminosityTexture.GetPixel((int)
currentLuminosityCoordinates.x, (
int) currentLuminosityCoordinates.y));

        sampleColorTexture.Apply();

    }

 

    //Init default values and create cursor texture, since it won’t be updated after

    void Init()

    {

        currentBaseColor = Color.red;

        currentLuminosityCoordinates = new Vector2(luminosityTextureSize – 1, luminosityTextureSize – 1);

 

        RegenerateTextures();

 

        cursorTexture = new Texture2D(1, 1);

        cursorTexture.SetPixel(0, 0, Color.black);

        cursorTexture.Apply();

    }

}

Of course there must be some more convenient and/or efficient ways of doing that (like using the default color picker from Unity – random example), but you can see that there is virtually no limit to what you can do, it’s more a matter of time and perseverance.

However we won’t go very far with this window, in term of utility… Let’s say we want to change that, and have a DiscreteColor struct that can be tweaked from the inspector like a standard color. For that, we will use a Property Drawer to customize the field shown in the inspector.

I feel a bit like a father now
I feel a bit like a father now

First we need a DiscreteColor struct and an object to use it as public field:

using UnityEngine;

 

public class MyScript : MonoBehaviour

{

    public DiscreteColor[] myColors;

}

 

//You have to make the class serializable so it can be used with a custom

// property drawer

[System.Serializable]

public struct DiscreteColor

{

    //Make the private fields serializable so they are saved

    [SerializeField]

    int colorsDivision;

    [SerializeField]

    float baseColor;

    [SerializeField]

    Vector2 luminosityCoordinates;

 

    //Public field is automatically serializable

    public Color resultColor;

}

Then we need to create the custom property drawer itself, that will use a slightly changed DiscreteColorPicker:

using UnityEngine;

using UnityEditor;

 

//Indicate we are creating a custom property drawer for the DiscreteColor type

[CustomPropertyDrawer(typeof(DiscreteColor))]

public class DiscreteColorDrawer : PropertyDrawer

{

    //Override the line height used to display the property in the inspector

    const float propertyHeight = 30;

    //Texture to display a sample of the selected color

    Texture2D colorSample;

 

    // Draw the property inside the given rect

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)

    {

        //Find all the serialized attributes of the DiscreteColor

        SerializedProperty serializedColorsDivision = property.FindPropertyRelative(“colorsDivision”);

        SerializedProperty serializedBaseColor = property.FindPropertyRelative(“baseColor”);

        SerializedProperty serializedLuminosityCoordinates = property.FindPropertyRelative(“luminosityCoordinates”);

        SerializedProperty serializedFinalColor = property.FindPropertyRelative(“resultColor”);

 

        //Using BeginProperty / EndProperty on the parent property means that

        // prefab override logic works on the entire property.

        EditorGUI.BeginProperty(position, label, property);

 

        //Default int value is 0, which is not an accetable value for our colors division

        serializedColorsDivision.intValue = Mathf.Max(serializedColorsDivision.intValue, 1);

 

        //The final color field was added only to avoir having to recalculate it from our weird parameters

        // By default it’s clear, so set its alpha to 1, since we don’t use alpha.

        Color finalColor = serializedFinalColor.colorValue;

        finalColor.a = 1;

 

        //Create a GUIStyle to display the selected color with the button text

        GUIStyle buttonStyle = new GUIStyle(“button”);

        GUIStyleState activeState = buttonStyle.active;

        activeState.textColor = finalColor;

        GUIStyleState normalState = buttonStyle.normal;

        normalState.textColor = finalColor;

 

        //Create the sample texture if it doesn’t exist and fill it with the final color

        if(colorSample == null)

            colorSample = new Texture2D(1, 1);

        colorSample.SetPixel(1, 1, finalColor);

        colorSample.Apply();

 

        //Display the sample as a little square on the left

        Rect sampleRect = new Rect(position.x, position.y, propertyHeight, propertyHeight);

        GUI.DrawTexture(sampleRect, colorSample);

 

        //Display a button to open the color picker and link it to the current property

        Rect buttonRect = new Rect(position.x + propertyHeight + 5, position.y + 5, position.width – 10 – propertyHeight, propertyHeight – 10);

        if(GUI.Button(buttonRect, “Set color”, buttonStyle))

        {

            DiscreteColorPicker.OpenWithColor(property.serializedObject,

                serializedColorsDivision, serializedBaseColor, serializedLuminosityCoordinates, serializedFinalColor);

        }

       

        EditorGUI.EndProperty();

    }

 

    //Override the line height used to display the property in the inspector

    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)

    {

        return propertyHeight;

    }

}

 And the changes in the DiscreteColorPicker:

using UnityEngine;

using UnityEditor;

 

public class DiscreteColorPicker : EditorWindow

{

    //Opens a picker with the serialized fields of a DiscreteColor

    public static DiscreteColorPicker OpenWithColor(SerializedObject serializedObject,

        SerializedProperty serializedColorsDivision,

        SerializedProperty serializedBaseColor,

        SerializedProperty serializedLuminosityCoordinates,

        SerializedProperty serializedFinalColor)

    {

        DiscreteColorPicker windowInstance = ScriptableObject.CreateInstance<DiscreteColorPicker>();

        windowInstance.Init(serializedObject, serializedColorsDivision,
serializedBaseColor, serializedLuminosityCoordinates, serializedFinalColor);

        windowInstance.Show();

        //Picker is returned so that the custom property drawer can get values

        return windowInstance;

    }

 

    //Keep a reference on the serialized fields

    SerializedObject serializedObject;

    SerializedProperty serializedColorsDivision;

    SerializedProperty serializedBaseColor; 

    SerializedProperty serializedLuminosityCoordinates;

    SerializedProperty serializedFinalColor;

 

    //Keep the final color so that it can be given back to the DiscreteColor

    // without having to recalculate it

    Color currentFinalColor;

 

   

 

    void OnGUI()

    {

 

       

 

        if(needsUpdate)

        {

            //A change has been made, sample texture has to be updated

            currentFinalColor = luminosityTexture.GetPixel((int) currentLuminosityCoordinates.x, (int) currentLuminosityCoordinates.y);

            sampleColorTexture.SetPixel(0, 0, currentFinalColor);

            sampleColorTexture.Apply();

            Repaint();

           

            //Final color has changed, we set the new values in the serialized fields

            if(serializedObject != null)

            {

                serializedColorsDivision.intValue = division;

                serializedBaseColor.floatValue = currentMainColorY;

                serializedLuminosityCoordinates.vector2Value = currentLuminosityCoordinates;

                serializedFinalColor.colorValue = currentFinalColor;

                //Save your changes

                serializedObject.ApplyModifiedProperties();

            }

 

       

   

 

    //Init the window with the serialized properties

    void Init(SerializedObject _serializedObject,

        SerializedProperty _serializedColorsDivision,

        SerializedProperty _serializedBaseColor,

        SerializedProperty _serializedLuminosityCoordinates,

        SerializedProperty _serializedFinalColor)

    {

        serializedObject = _serializedObject;

        serializedColorsDivision = _serializedColorsDivision;

        serializedBaseColor = _serializedBaseColor;

        serializedLuminosityCoordinates = _serializedLuminosityCoordinates;

        serializedFinalColor = _serializedFinalColor;

  

        division = serializedColorsDivision.intValue;

        currentMainColorY = serializedBaseColor.floatValue;

        currentLuminosityCoordinates = serializedLuminosityCoordinates.vector2Value;

 

        RegenerateTextures();

 

        cursorTexture = new Texture2D(1, 1);

        cursorTexture.SetPixel(0, 0, Color.black);

        cursorTexture.Apply();

    }

 

    //Close the window when user clicks somewhere else. There seems to be a bug with

    // this in current Unity version (even with system windows), which sometimes causes

    // a crash… so don’t use it in your commercial production, maybe.

    void OnLostFocus()

    {

        serializedObject = null;

        Close();

    }

}

You can as well build entire systems for game settings, accessible from menus, that parse assets in your project, save data to txt or asset files, modify prefabs… You can have access to or even set the selected object in the project tab, find all subclasses of a given class to create assets from scratch, open, modify and save scenes… You’ve got it: it’s powerful.


Scene view

It can also be very useful to build tools directly in the scene view, especially for things like level design. And guess what: it’s as simple as what we’ve seen so far! You just register to the scene GUI delegate, then use the Handles to reach your goal. Here is a simple example reproducing the first gif of this post:

So convenient!
So convenient!

And the code to do that:

using UnityEngine;

 

//Import UnityEditor only when on editor, in order to be able to build project

#if UNITY_EDITOR

using UnityEditor;

#endif

 

//In order to have the code running while application is not playing

[ExecuteInEditMode]

public class Path : MonoBehaviour

{

    public Transform[] waypoints;

 

    void Start ()

    {

        //When in the editor not playing, register to callback allowing

        // to draw on the Scene view. Unregister first if the delegate

        // had previously been added

        if(!Application.isPlaying)

        {

#if UNITY_EDITOR

            SceneView.onSceneGUIDelegate -= OnSceneGUI;

            SceneView.onSceneGUIDelegate += OnSceneGUI;

#endif

        }

    }

 

    protected virtual void OnDestroy()

    {

        //Unregister if the object is destroyed

        if(!Application.isPlaying)

        {

#if UNITY_EDITOR

            SceneView.onSceneGUIDelegate -= OnSceneGUI;

#endif

        }

    }

 

#if UNITY_EDITOR

    void OnSceneGUI(SceneView sceneView)

    {

        //Check if this object or one of its children is selected

        bool isOnMe = Selection.activeGameObject == gameObject;

        if(!isOnMe)

        {

            for(int i = 0; i < transform.childCount; i++)

            {

                if(Selection.activeTransform == transform.GetChild(i))

                {

                    isOnMe = true;

                    break;

                }

            }

        }

 

        if(isOnMe)

        {

            //If there are no waypoint yet, create the array and initialize it with

            // a spawn point

            if(waypoints == null || waypoints.Length == 0)

            {

                Vector3 newSpawnPointPos = Vector3.zero;

                GameObject newSpawnPointGo = new GameObject();

                newSpawnPointGo.transform.SetParent(transform);

                waypoints = new Transform[1];

                waypoints[0] = newSpawnPointGo.transform;

                newSpawnPointGo.transform.position = newSpawnPointPos;

            }

 

            //Draw the spawn point and name it

            //Check if it isn’t null, in case user has deleted it

            if(waypoints[0] != null)

            {

                waypoints[0].gameObject.name = “Spawn”;

 

                //Display the spawn point handle in red

                Handles.color = Color.red;

 

                //The handle can be moved by the user and returns its new position,

                // so reassign it to the transform

                waypoints[0].position = Handles.FreeMoveHandle(waypoints[0].position,

                                        Quaternion.identity,

                                        0.2f, Vector3.zero,

                                        Handles.DotCap);

            }

 

            //Draw the rest of the path

            for(int i = 1; i < waypoints.Length; i++)

            {

                //Always check for a null transform

                if(waypoints[i] != null)

                {

                    waypoints[i].gameObject.name = “WP” + i.ToString();

                    Vector3 position = waypoints[i].position;

 

                    //We’ll draw a line between the waypoints, from the previous one

                    // to the current one

                    if(waypoints[i – 1] != null)

                    {

                        Handles.color = Color.blue;
Handles.DrawLine(waypoints[i-1].position,
position);

                    }

 
//If it’s the last waypoint, display it in black

                    if(i == waypoints.Length – 1)

                        Handles.color = Color.black;

                    //Otherwise, show it green

                    else

                        Handles.color = Color.green;

 

                    //Create the handle to move it

                    waypoints[i].position = Handles.FreeMoveHandle(position,

                         Quaternion.identity,

                         0.2f, Vector3.zero,

                          Handles.DotCap);

 

                }

            }

 

            //Finally, after the last waypoint, display a button to create another one

            int maxIndex = waypoints.Length – 1;

            if(waypoints[maxIndex] != null)

            {

                Vector3 newWPPosition = waypoints[maxIndex].position + new Vector3(0.5f, 0);

                Handles.color = Color.cyan;

                //Create the clickable handles button

                if(Handles.Button(newWPPosition, Quaternion.identity, 0.2f, 0.2f, Handles.DotCap))

                {

                     //If clicked, create the new waypoint

                     GameObject newWP = new GameObject();

                     newWP.transform.SetParent(transform);

                     Transform[] newPath = new Transform[maxIndex + 2];

                     for(int j = 0; j < maxIndex + 1; j++)

                     {

                         newPath[j] = waypoints[j];

                     }

                     waypoints = newPath;

                     waypoints[waypoints.Length – 1] = newWP.transform;

                     newWP.transform.position = newWPPosition;

                }

                //Add a “+” label on the button, scaled according to button size

                float handleSize = HandleUtility.GetHandleSize(newWPPosition);

                Handles.color = Color.black;

                GUIStyle buttonStyle = new GUIStyle();

                buttonStyle.fontSize = (int) (25f/handleSize);

                Handles.Label(newWPPosition + new Vector3(-0.12f, 0.2f), “+”,
buttonStyle);

            }

        }

    }

#endif

}

That’s all! As said at the beginning of the post, this was just a very light overview of the possibilities offered by Unity’s editor, but the doc is quite nice, and there are tons of questions and answers on the webz about this subject. All the examples given here are small scripts, but you can build mad interfaces with all the options your game designers have always dreamed of without thinking it could come true someday. It will save you a crazy amount of time!


 

For more articles, you can follow me on Twitter, Facebook or subscribe to this blog.

 

Further “readings”:

A great video about Editor Scripting (the one that got me into this)

THE thread to talk about serialization