Some Handy Procedural Methods (Placing objects on the ground in circle/grid patterns or individually)

804 views

It's been a while since I posted any code..  Here are some handy methods that I've been using frequently in my SlingBots game.  Keep in mind, some examples use local variables where it's better to reuse variables with more scope for memory/GC reasons in practical application.    If you see something that I could really improve, please let me know.  Many of these are "setup" routines so I've not bothered with optimization really at all since they run once, or very infrequently.

All of these are designed to work with the standard Unity orientation, with normal terrain objects or terrain meshes.

RayCast Position of Ground at any point on terrain:

    Vector3 GroundPosAt(Vector3 position)
{
Vector3 result = position;
//some altitude above the highest possible ground position.
float checkAltitude = 2000.0f;
Ray rr = new Ray(new Vector3(position.x, checkAltitude, position.z), -Vector3.up);
//I use a fixed allocation ray cast as a general practice to keep GC down.
//It will need to be big enough to contain all the possible hits it could catch finding your ground object..
//YMMV with these *NonAlloc methods, but they work wonderfully for me.
RaycastHit[] hits = new RaycastHit[20];
if (Physics.RaycastNonAlloc(rr, hits) > 0)
{
foreach(RaycastHit hit in hits)
{
//make sure this entry isn't null
if (hit.collider != null)
{
//check for collision with object tagged ground
if (hit.collider.CompareTag("ground"))
{
result = hit.point;
}
}
}
}

return result;
}

Get A Vector3 position on a Circle at a specific angle and radius:

    Vector3 CirclePos(Vector3 center, float radius, float angle)
{
Vector3 pos;
pos.y = center.y;
return pos;
}

Object Placement using fixed arrays for object tracking and management:

This example shows one of the ways I programmatically instantiate turrets into SlingBot Boarding.

    public GameObject worldTurretPrefab;
GameObject[] worldTurrets = new GameObject[100];
SnowBallTurret[] _worldTurrets = new SnowBallTurret[100];

void PlaceWorldTurret(GameObject parentobject, Vector3 position, int firefrequency, float firepower, float sightdistance)
{
for (int i = 0; i < worldTurrets.Length; i++)
{
if (worldTurrets[i] == null)
{
if (parentobject == null)
{
worldTurrets[i] = Instantiate(worldTurretPrefab);
}
else
{
worldTurrets[i] = Instantiate(worldTurretPrefab, parentobject.transform, false);
}
_worldTurrets[i] = worldTurrets[i].GetComponentInChildren<SnowBallTurret>();
_worldTurrets[i].playerAvatar = GameController.GControl.Player;
_worldTurrets[i].fireFrequency = firefrequency;
_worldTurrets[i].id = (i + 1).ToString();
_worldTurrets[i].turretProjectileVelocity = firepower;
_worldTurrets[i].turretSightingDistance = sightdistance;
worldTurrets[i].transform.localPosition = position;
break;
}
}
}

I use an array for both the object and a reference to the script it holds to save time on future getcomponent lookups.  This will come in handy when I want to upgrade the settings on all of the already instantiated objects due to a player increase in level/etc.

I use a fixed array so I can predict(reasonably) what the upper level of memory usage will be(after testing).  I iterate through the existing collection and only add a new object if there is an empty slot in the array.  This allows me to ensure there will be ZERO runtime exceptions related to object instantiation.  It is better for my game to end up being a little easier than it should have been than it would be for an exception to be thrown right at an exciting moment.  Note: the above method could be easily modified to return a bool on success/failure if you wanted to adjust the object arrays and try again on failure.

Putting this all together, here's instantiating Turrets in a circle around a specific point(on uneven terrain):

    void PlaceTurretsCircle(Vector3 position, int turretcount, float turretradius)
{
//place turrets in circle
for (int i = 0; i < turretcount; i++)
{
//Adjust settings based on players level
float levelModifier = 1.0f;
if (currentGameLevel > 1)
{
//add 20% to modifier for each level
levelModifier += (0.2f * currentGameLevel);
}
//Calculate angle for object around circle
float angl = (360 / turretcount) * i;
PlaceWorldTurret(null,
GroundPosAt(CirclePos(position, turretradius, (360 / turretcount) * i)),
(int)(2000.0f / levelModifier),
50.0f * levelModifier,
500.0f);
}
}

and a bonus, Here's placing turrets on a Grid:

This one is presently written to require a parent object as it places the turrets in relative position to that object.

void PlaceTurretsGrid(GameObject parentobject, float xstart, float zstart, float xrowdist, float zrowdist, float yoffset, int xrowcount, int count, int firefrequency, float firepower, float sightdistance)
{
float xoffset = xstart;
float zoffset = zstart;
if (count > 100) count = 100;
int xmaxcount = xrowcount - 1;
int xcount = 0;
for (int i = 0; i < count; i++)
{
//Without ground position checking
PlaceWorldTurret(parentObject, new Vector3(xoffset, yoffset, zoffset), turretFireFrequency, turretFirePower, turretSightDistance);
//With ground position checking(untested)
//PlaceWorldTurret(parentObject, GroundPosAt(parentObject.position + new Vector3(xoffset, yoffset, zoffset)), turretFireFrequency, turretFirePower, turretSightDistance);
xcount++;
xoffset += xrowdist;
if (xcount > xmaxcount)
{
xcount = 0;
xoffset = xstart;
zoffset += zrowdist;
}
}
}

Not a lot of rocket science going on here, but it could be a time-saver or a mental-block fixer for somebody I'm sure.

Check out the game if you get a chance:

Happy coding out there!!

Thanks for sharing.

12 minutes ago, Rutin said:

Thanks for sharing.

Happy to!  I have a hard time remembering to post stuff like this, if there's anything in the game you(or anybody) would like me to elaborate on in code form feel free to let me know.  I often forget how long it takes me to figure things out and begin looking at them as simple bits of code that don't really need sharing.

You could drastically speed up your raycast/ground check by using a layermask.  If you only checked for objects on the ground layer, there would be no need to compare tags.

In fact, at that point, depending on your game, you might not even need the raycastnonalloc, if for example your terrain was a single Terrain.

18 minutes ago, ferrous said:

You could drastically speed up your raycast/ground check by using a layermask.  If you only checked for objects on the ground layer, there would be no need to compare tags.

In fact, at that point, depending on your game, you might not even need the raycastnonalloc, if for example your terrain was a single Terrain.

Good point, I always overlook layermasks for raycast uses...  Probably because they confused the heck out of me for a while there.  That would certainly help in dense object scenarios.  But regardless of how many results there are, NonAlloc still results in less GC due to internal garbage generated by the normal raycast methods.  At least in my testing.

Posted (edited)

I mean that you no longer need to check for multiple hits, therefore don't need to use the Physics.Raycast that returns multiple hits, which should theoretically invoke much less garbage collection than yours,since no array is required.

Edited by ferrous

21 minutes ago, ferrous said:

I mean that you no longer need to check for multiple hits, therefore don't need to use the Physics.Raycast that returns multiple hits, which should theoretically invoke much less garbage collection than yours,since no array is required.

Like I stated at the top, some of these are unoptimized examples using local variables.  I reuse non-local arrays to keep from generating excess garbage in production code.  I started out using normal raycasts(w/o layermasks of course), it's also important to mention that there's really nothing to mask out in most of my use cases.  As the ground object is the only thing that can be collided with.  Profiling got me to my present methodologies. I shall now venture to test if layermasking reduces the GC as well.  As it stands I don't have any GC of measure.

I'm also curious if a single result raycast would guarantee the closest hit as my more resource intensive casts are not straight up and down and often return multiple ground hits that are never in a predictable order...  More tests, perhaps the topic of my next blog.

Posted (edited)

If your scene has 100 objects, your original code will raycast against those 100 objects, looking for hits, copy the results of up to twenty objects that hit the ray, and then you check if those 20 objects are ground objects.   By using layermasking, we first filter out the 100 objects for ground objects.  (Of which there might be only one!)  Then we do a ray test on those objects.  So even if there is no GC difference, there should be a order of magnitude difference, especially as object count and complexity goes up.  That and we eliminate the tag check as well.

I don't recall if the single hit method returns the closest or not, it would be pretty garbage if it didn't, but I wouldn't be surprised if it didn't, or if it generated more GC that the non-alloc.  Though I wonder how much it of a difference there is if compiling to their il2cpp code.

Edited by ferrous

Yeah, you've got me curious on a number of things too.  I noticed significant improvements when switching to this method, and not just in GC numbers.  So now I'm going to have to compare it to single result and/or layermasked methods.

And actually the more I think about it, I have currently over 100 ground objects(tiles), and maybe 2 others that "might" get in the way sometimes..  So it's actually backwards, I think layermasking would help(Me) most of I was looking for something other than the ground..   Hahaha!

Haha, now you have me curious what your scene is like, as you shouldn't be getting amazing improvements if you're mostly 'ground' objects.  Unless CompareTag is slow.  (Which I suspect it might be)  I'm also not sure what kind of spatial partitioning that unity does.  So it might be trying to check against more objects than you think.

Anyway, Physics layers and Layer masking are great ways to improve performance.  So definitely make sure you understand them.

1 hour ago, ferrous said:

Haha, now you have me curious what your scene is like

Quote

as you shouldn't be getting amazing improvements if you're mostly 'ground' objects.

To be honest my memory for how much something improved isn't always 100% accurate.  I do remember it improving things to some noticeable degree, and it may have just been the GC.

Ultimately though, so long as I'm getting close to 60fps on a web build, I call it optimized and move on to the next dumpster fire.

Premature optimization and all that.  And I got a lot of dumpster fires...

Quote

Unless CompareTag is slow.  (Which I suspect it might be)

It's better than a standard string comparison, so.. aside from switching to a layermask method(which I'm not against), only way to go.

Quote

I'm also not sure what kind of spatial partitioning that unity does.  So it might be trying to check against more objects than you think.

I'm currently set to 16 subdivisions on a 10000x10000x10000 world bounds.  (but I have no idea how those work either)  The terrain is approximately the same size(x & z).

Quote

Anyway, Physics layers and Layer masking are great ways to improve performance.  So definitely make sure you understand them.

For sure, I use layers for cameras and segregating colliders that shouldn't interact, just not raycasts, currently, in this game..

Create an account

Register a new account

• What is your GameDev Story?

In 2019 we are celebrating 20 years of GameDev.net! Share your GameDev Story with us.

• Similar Content

• So this is the problem that I have :- https://youtu.be/kU8Dm5bDJXg
This is the code i am using:-
using System.Collections; using System.Collections.Generic; using UnityEngine; namespace SA { public class AnimatorHook : MonoBehaviour { Animator anim; StateManager states; public void Init(StateManager st) { states = st; anim = st.anim; } void OnAnimatorMove() { if (!states.canMove) anim.ApplyBuiltinRootMotion(); states.rigid.drag = 0; float multiplier = 1; Vector3 delta = anim.deltaPosition; delta.y = 0; Vector3 v = (delta * multiplier) / states.delta; states.rigid.velocity = v; } } } For additional reference see the following code
using System.Collections; using System.Collections.Generic; using UnityEngine; namespace SA { public class StateManager : MonoBehaviour { [Header("Init")] public GameObject activeModel; [Header("Inputs")] public float vertical; public float horizontal; public float moveAmount; public Vector3 moveDir; public bool rt, rb, lt, lb; [Header("Stats")] public float moveSpeed = 5f; public float runSpeed = 8f; public float rotateSpeed = 20; public float toGround = 0.5f; [Header("States")] public bool onGround; public bool run; public bool lockOn; public bool inAction; public bool canMove; [Header("Other")] public EnemyTarget lockOnTarget; [HideInInspector] public Animator anim; [HideInInspector] public Rigidbody rigid; [HideInInspector] public AnimatorHook a_hook; [HideInInspector] public float delta; [HideInInspector] public LayerMask ignoreLayers; float _actionDelay; public void Init() { SetupAnimator(); rigid = GetComponent<Rigidbody>(); rigid.angularDrag = 999; rigid.drag = 4; rigid.constraints = RigidbodyConstraints.FreezeRotationX | RigidbodyConstraints.FreezeRotationZ; a_hook = activeModel.AddComponent<AnimatorHook>(); a_hook.Init(this); gameObject.layer = 8; ignoreLayers = ~(1 << 9); anim.SetBool("onGround",true); } void SetupAnimator() { if(activeModel == null) { anim = GetComponentInChildren<Animator>(); if(anim == null) { Debug.Log("no model found"); } else { activeModel = anim.gameObject; } } if(anim == null) { anim = activeModel.GetComponent<Animator>(); } //anim.applyRootMotion = false; } public void FixedTick(float d) { delta = d; rigid.drag = (moveAmount > 0 || !onGround) ? 0 : 4; DetectAction(); if (inAction) { // anim.applyRootMotion = true; _actionDelay += delta; if(_actionDelay > 0.3f) { inAction = false; _actionDelay = 0; } else { return; } } canMove = anim.GetBool("canMove"); if (!canMove) { return; } //anim.applyRootMotion = false; float targetSpeed = moveSpeed; if (run) targetSpeed = runSpeed; if(onGround) rigid.velocity = moveDir * (targetSpeed * moveAmount); /* if (run) lockOn = false; */ Vector3 targetDir = (lockOn == false) ? moveDir : lockOnTarget.transform.position - transform.position; targetDir.y = 0; if (targetDir == Vector3.zero) targetDir = transform.forward; Quaternion tr = Quaternion.LookRotation(targetDir); Quaternion targetRotation = Quaternion.Slerp(transform.rotation, tr, delta * moveAmount * rotateSpeed); transform.rotation = targetRotation; anim.SetBool("lockon", lockOn); if (lockOn == false) HandleMovementAnimations(); else HandleLockOnAnimations(moveDir); } public void DetectAction() { if (canMove == false) return; if (rb == false && rt == false && lt == false && lb == false) return; string targetAnim = null; if (rb) targetAnim = "Sword And Shield Attack"; if (rt) targetAnim = "Stable Sword Outward Slash"; if (lb) targetAnim = "Standing Melee Attack Horizontal"; if (lt) targetAnim = "Sword And Shield Slash (1)"; if (string.IsNullOrEmpty(targetAnim)) return; canMove = false; inAction = true; anim.CrossFade(targetAnim,0.2f); //rigid.velocity = Vector3.zero; } public void Tick(float d) { delta = d; onGround = OnGround(); anim.SetBool("onGround", onGround); } void HandleMovementAnimations() { anim.SetBool("run", run); anim.SetFloat("Vertical", moveAmount ,0.4f,delta); } void HandleLockOnAnimations(Vector3 moveDir) { Vector3 relativeDir = transform.InverseTransformDirection(moveDir); float h = relativeDir.x; float v = relativeDir.z; anim.SetFloat("Vertical", v, 0.2f, delta); anim.SetFloat("Horizontal", h, 0.2f, delta); } public bool OnGround() { bool r = false; Vector3 origin = transform.position + (Vector3.up * toGround); Vector3 dir = -Vector3.up; float dis = toGround + 0.3f; RaycastHit hit; if(Physics.Raycast(origin,dir,out hit,dis)) { r = true; Vector3 targetPosition = hit.point; transform.position = targetPosition; } return r; } } } I've been stuck on this for too long, any help will be highly appreciated

• In the attached picture an intersection block is shown. My goal is to have the player switch lanes and (within the red bounding box) turn direction on a different road while always keeping a forward movement. Therefor the only input is left and right. The changing of the lanes has to happen quick and snappy (dependant on the player's overall speed) as visualized by the black squigly line.
However I'm not sure how to implement this. I tried moving left and right with coroutines but got stuck at going back to the lane one came from mid-changing. I added those nodes at the ends with the intention to lerp/move towards depending on the lane and road the player changes to but that won't work with the aforementioned quick snappy movement. Another thought that passed was creating a bit of a node graph and dynamically make a spline when switching lanes but this seems a bit overkill for what it really is (?). Finally I figured creating an object that the player controls but doesn't see that the actual player objects follows that's slightly infront of the object but haven't gotten to that yet due to too much doubts of implementation.
The player should not be constrained. If they whish to change back to a lane halfways through changing lanes that should be possible.
Would like to hear some suggestions no mather how out there they might seem. Thanks!

• By jb-dev
This is a short .gif showing off visuals effects for the parrying mechanic

Can programmers art? How far can creativity and programming take you?
I have summarized what I learned in several months into 7 key techniques to improve the visual quality of your game.

"Programmer art" is something of a running joke. For those unfamiliar with the term, it refers to the "placeholder" or "throw-together" art that programmers tend to use while developing games.
Some of us don't have the necessary artistic skills, however, sometimes we just can't be bothered to put in the effort. We're concerned about the technical side of things working - art can come later.
Here's what this usually means -

I worked on a game jam with some new people a few months ago. I just wanted to make sure that my gameplay and AI code was doing what it was supposed to do. This would have to interface with code from other teammates as well, so it was important to test and check for bugs. This was the result.
That's not what I'm going to talk about today though.

I'm going to take a different angle on "programmer art" - not the joke art that programmers often use, but the fact that there's a LOT that a programmer can do to improve the visual appeal of a game. I believe some of this falls under "technical art" as well.

My current job kind of forced me to think in this capacity.
I was tasked with visualizing some scientific data. Though this data was the result of years of hard work on the part of scientists, the result was unimpressive to the untrained eye - a heap of excel files with some words and numbers.
There are very few people in the world who can get excited by seeing a few excel files.
My job? To make this data exciting to everyone else.
My task was to visualize connectome data for a famous worm known as C. Elegans, made available by the wonderful people working on the OpenWorm project.
Part of the data parsing to read and display the data as a worm's body with neurons on it was done by my teammate. My main task was to improve the visuals and the overall graphical quality.

The first thing that comes to mind is using HD textures, PBR materials and high-poly models. Add in a 3D terrain using a height map, some post-processing and HDR lighting, and BOOM! Gorgeous 3D scene. I'm sure you've all seen loads of those by now.
Except, almost none of that would really help me.
The idea was very abstract - neurons and connections visible in a zoomed-in, x-ray-like view of a worm. I don't think rolling hills would have helped me much.
I had no 3D modelling skills or access to an artist - even if I did, I'm not sure what kind of 3D models would have helped.

As a result, what I've made isn't a gorgeous 3D environment with foliage and god-rays and lens flares. So it's not applicable in every case or the perfect example of how a programmer can make a gorgeous game.
But, it does provide a distinct viewpoint and result. The special sets of constraints in the problem I had to solve led to this.
So here's what I actually did:

The 7 things I did to improve the visuals of my Unity game
1. Conceptualizing the look
This could be considered a pre-production step for art or any visual project. Ideally, what should it look like? What's the goal? What are your references?
In this case, the viewer had a hologram-like feel to it (also there were plans to port it to a HoloLens eventually). I liked the idea of a futuristic hologram. And the metaphor of "AI bringing us towards a better future".
So what were my references? Sci-fi of course!
My first pick was one of my favourite franchises - Star Wars. I love how the holo-comms look in the movies.

Holograms became a key component of my design.
This is a HUD design from Prometheus that I found on Google -

In this case, the colours appealed to me more than the design itself. I ended up basing the UI design on this concept.

Key takeaway - Your imagination is the very first tool that helps you create impressive art. Use references! It's not cheating - it's inspiration. Your references will guide you as you create the look that you want.

I had some shader programming experience from University - D3D11 and HLSL. But that work had been about building a basic graphics engine with features like lighting, shadows, and some light post-processing. I had done some light Shader programming in Unity before as well.
What I really needed now was impressive visual effects, not basic lighting and shadows.
I was really lucky that this was about the time Unity made Shader Graph available, which made everything much easier. I can write Shader code, but being able to see in real time what each node (Which can be considered a line of code) does makes it so much easier to produce the effects you want.
I familiarized myself with all the samples Unity had included with this new tool. That wouldn't have been enough though. Thankfully due to my previous experience with Shaders, I was able to make some adjustments and improvements to make them suit my needs.
Some tweaking with speed, scaling, colours, and textures led to a nice hologram effect for the UI panels.

I wanted the viewer to feel good to interact with as well, and some work implementing a glow effect (alongside the dissolve effects) led to this -

Key takeaway - Shaders are an extremely powerful tool in a Game Programmer's repertoire. Tools like Unity's Shader Graph, the old Shader Forge asset, and Unreal's material editor make Shaders more accessible and easier to tune to get the exact look you want.
PS - Step 5 below is also really important for getting a nice glow effect.

3. Visual Effects and Animations using Shaders
I was able to extend the dissolve and hologram shaders to fake some animation-like visual effects.
And a combination of some timed Sine curves let me create an animation using the dissolve effect -

The work here was to move the animation smoothly across individual neuron objects. The animation makes it look like they're a single connected object, but they're actually individual Sphere meshes with the Shader applied to them. This is made possible by applying the dissolve texture in World Space instead of Object Space.
A single shader graph for the neurons had functionality for colour blending, glow, and dissolve animation.
All of this made the graphs really large and difficult to work with though. Unity was constantly updating the Shader Graph tools, and the new updates include sub-graphs which make it much easier to manage.
Key takeaway - There is more to shaders than meets the eye. As you gain familiarity with them, there are very few limits to the effects you can create. You can create animations and visual effects using Shaders too.

4. Particle systems - more than just trails and sparks
I have no idea why I put off working with the particle systems for so long!
The "neurons" in the viewer were just spheres, which was pretty boring.
Once I started to understand the basics of the particle system, I could see how powerful it was. I worked on some samples from great YouTube tutorials - I'm sharing a great one by Gabriel Aguiar in the comments below.
After that, I opened up Photoshop and experimented with different brushes to create Particle textures.
Once again, I referred to my sources of what neurons should look like. I wanted a similar look of "hair-like" connections coming out of the neurons, and the core being bright and dense.
This is what it looked like finished, and the particle system even let me create a nice pulsating effect.

Part of my work was also parsing a ton of "playback data" of neurons firing. I wanted this to look like bright beams of light, travelling from neuron to neuron. This involved some pathfinding and multi-threading work as well.

Lastly, I decided to add a sort of feedback effect of neurons firing. This way, you can see where a signal is originating and where it's ending.

Key takeaway - Particle systems can be used in many ways, not just for sparks and trails. Here, I used them to represent a rather abstract object, a neuron. They can be applied wherever a visual effect or a form of visual "feedback" seems relevant.

5. Post-processing to tie the graphics and art together
Post-processing makes a HUGE difference in the look of a game scene. It's not just about colours and tone, there's much more to it than that. You can easily adjust colours, brightness, contrast, and add effects such as bloom, motion blur, vignette, and screen-space reflections.
First of all, Linear colour space with HDR enabled makes a huge difference - make sure you try this out.
Next, Unity's new post-processing stack makes a lot of options available without impacting performance much.
The glow around the edges of the sphere only appears with an HDR colour selected for the shader, HDR enabled, and Linear colour space. Post-processing helps bump this up too - bloom is one of the most important settings for this.
Colour grading can be used to provide a warm or cool look to your entire scene. It's like applying a filter on top of the scene, as you would to an image in Photoshop. You can completely override the colours, desaturate to black and white, bump up the contrast, or apply a single colour to the whole scene.

There is a great tutorial from Unity for getting that HD look in your scenes - if you want a visible glow you normally associate with beautiful games, you need to check this out.

Key takeaway - Post processing ties everything together, and helps certain effects like glows stand out.

6. Timing and animation curves for better "feel"
This is a core concept of animation. I have some training in graphic design and animation, which is where I picked this up. I'm not sure about the proper term for it - timing, animation curves, tween, etc.
Basically, if you're animating something, it's rarely best to do it with linear timing. Instead, you want curves like this -

Or more crazy ones for more "bouncy" or cartoon-ish effects.
I applied this to the glow effects on the neurons, as I showed earlier.
And you can use this sparingly when working with particle systems as well - for speed, size, and similar effects. I used this for the effect of neurons firing, which is like a green "explosion" outwards. The particles move outwards fast and then slow down.
Unity has Animation Curve components you can attach to objects. You can set the curve using a GUI and then query it in your C# scripts. Definitely worth learning about.
Key takeaway - Curves or tweens are an animation concept that is easy to pick up and apply. It can be a key differentiator for whether your animations and overall game look polished or not.

7. Colour Palettes and Colour Theory - Often overlooked
Colour is something that I tend to experiment with and work with based on my instincts. I like being creative, however, I really underestimated the benefits of applying colour theory and using palettes.
Here's the before -

Here are some of the afters -

I implemented multiple themes because they all looked so good.
I basically messed around with different types of "Colour harmony" - Monochrome, triad, complementary, and more. I also borrowed some colours from my references and built around that.
Key takeaway - Don't underestimate the importance of colour and colour theory. Keep your initial concept and references in mind when choosing colours. This adds to that final, polished look you want.

Bonus - consider procedural art
Procedural Generation is just an amazing technique. I didn't apply it on this project, but I learned the basics of it such as generating Value and Perlin noise, generating and using Height maps for terrains, and generating mazes.

Procedural art is definitely something I want to explore more.
A couple of interesting things (Links in the "extra resources" section below) -
Google deepdream has been used to generate art. There's an open-source AI project that can colour lineart. Kate Compton has a lot of interesting projects and resources about PCG and generative art. I hope this leads to tools that can be directly applied to Game Development. To support the creation of art for games. I hope I get the opportunity to create something like that myself too.
Conclusion
These 7 techniques were at the core of what I did to improve the visual quality of my project.
This was mostly the result of the unique set of constraints that I had. But I'm pretty sure some famous person said: "true creativity is born of constraints". Or something along those lines. It basically means that constraints and problems help channel your creativity.
I'm sure there is more that I could have done, but I was happy with the stark difference between the "before" and "after" states of my project.
I've also realized that this project has made me more of an artist. If you work on visual quality even as a programmer, you practice and sharpen your artistic abilities, and end up becoming something of an artist yourself.

Did I miss something obvious? Let me know in the comments!

Extra Resources
OpenWorm project
Great tutorial by Gabriel Aguiar
Unity breaks down how to improve the look of a game using Post processing
Another resource on post-processing by Dilmer Valecillos
Brackey's tutorial on post-processing
Adobe Colour wheel, great for colour theory and palettes
An open-source AI project that can colour lineart
A demo of generative art by Kate Compton