Developer's Diary
First Ideas
From the very beginning it was clear that the game should revolve around the idea of capturing and being captured – a theme taken over from Keystone Kapers. Keeping with the idea of dodging obstacles (from Jungle Hunt), the level was supposed to consist of moving platforms, that would, through slightly shifted phases, create oscillating forms and movement. Because of that, the first name for the game was born: weve.
Moving Platforms
Soon, a first sketch of a script to move the platforms was made:
private var counter = 0.0; private var randomSeed:float; function Start () { randomSeed = Random.Range(-2,2); } function FixedUpdate () { sineMovement (0.5,randomSeed,0.08); // Update the counter (only here, because otherwise it will get confused) counter = counter + 0.1; } // A swinging motion. function sineMovement (scale:float, delay:float, amplitude:float) { transform.Translate(Mathf.Sin(counter * scale + delay) * amplitude, Mathf.Sin(counter * scale + delay) * amplitude, Mathf.Sin(counter * scale + delay) * amplitude); }
Clearly, it could be done better, but it served its purpose: mostly to show me, that the character-controlled enemy objects were literally unmoved by these platforms. And obstacles that do not obstacle a path are pointless. I assume I would have had to work with applying forces to the NPCs, but I did not had the idea back then. Therefore, I decided that the obstacles were fixed.
Finding Your Way
I also realised that in order to jump over gaps and find ways around moving objects, the enemy behaviour would have a considerable amount of "intelligence" – in fact, I would have to use some sort of pathfinding solution. Clearly, having only worked with Unity 3D for barely a month at this time, I did not feel confident enough that I could pull this off.
While there are some solutions available (one by AngryAnt as well as an A* pathfinding solution by Aaron Granberg), I decided that I would not have time implement them correctly – even more so as all of these are built for fixed targets and not (as in my case) a moving one.
Again, I decided that while the game-AI of the enemy characters might be rather bare-bones simple, this fact could be mitigated by clever level design.
Enemy Behaviour
While the enemy behaviour for the NPC that follows the player is a hardly modified version of René's NPCFollower script, the behaviour for the NPCs that evade the player took a bit longer to model.
Evading the Player, But Not Too Much
At first, I just changed the movement direction in the NPCFollower script, so the NPCs would move away from the player character. Of course, they tried to be as far away from the player character as possible, piling up in the corners of the level, sometimes even falling out of it.
In an attempt to solve that, I placed invisible triggers in the corners of the level, to move them away from there as well – to no avail (possibly because there was no rigidbody component attached to them; as a matter of fact, I do not exactly know why it did not work, but then again, I had to rethink my approach anyway).
At first, I limited the range of influence the PC had: as long as the PC was not within a defined radius, the NPCs would not try to run from it.
// [...] var followThisObject:GameObject; var radius = 8.0; // [...] function Start() { // Basically, the following line is redundant, since the player // character can be defined in the Unity editor. In early versions // during delevopment, sometimes no object was defined or got destroyed // during game play and was then re-instanced through a script, // which is why this is necessary. followThisObject = GameObject.FindWithTag("Player"); // [...] } // [...] function FixedUpdate() { // [...] var dist = Vector3.Distance(this.transform.position, followThisObject.transform.position); if (dist < radius) { moveDirection = this.transform.position-followThisObject.transform.position; // get the direction moveDirection = moveDirection.Normalize(moveDirection); // normalize the direction moveDirection = transform.TransformDirection(moveDirection); moveDirection = moveDirection * (1/dist) * speed * 10; // apply speed. } // [...] } // [...]
As an added bonus, while the player is following this NPC, the radius gets continually langer and is only reset when the player has found a new target.
Moving Aimlessly and Casually
Since the NPCs would not move while there were out of the PC's range, they had to move in a different way.
While this could be done by making them move in a random direction, this first experiment lead to an unsatisfactory jittery movement. Since I did not knew about the Time class back then, I was not able to prolong the time the NPC would move in one direction.
Instead, I first thought about adding invisible target moving around in the level, which the NPCs would follow. This lead to the problem that I would have to script the movement of those targets as well; and, what's more, find a way to limit them to the extents of the level.
Then it dawned on me: I could just make the NPCs follow other NPCs. Since they pick any random other NPC, they follow it until they reach it, and then choose another to follow. This way, I would have a kind of movement that would seem both chaotic and with a certain sense of purpose.
// [...] private var otherEvader:GameObject; function Start() { chooseOtherEvader(); // Since this is used all over the place, // it has it's own function. // [...] } function FixedUpdate (){ // [...] if (dist < radius) { // As seen above: move NPC away from player } else { if (otherEvader == null) { chooseOtherEvader(); } // Follow the other evader, as above, but with opposite direction moveDirection = this.transform.position-otherEvader.transform.position; moveDirection = moveDirection.Normalize(moveDirection); moveDirection *= -1; moveDirection = moveDirection * speed; } } function OnControllerColliderHit (hit: ControllerColliderHit) { if (hit.gameObject.tag == "Evader") { chooseOtherEvader(); } // [...] } function chooseOtherEvader() { var evaders:GameObjects[]; // create an empty array evaders = GameObject.FindGameObjectsWithTag("Evader"); // Choose another random evader otherEvader = evaders[Random.Range(0,evaders.length-1)]; } // [...]
As can be seen from the script, I tend to rely heavily on tags to find available objects in the scene. I do not know yet how wise this is performance-wise, but for this game it seems to work fine. The only downside is the fact that every GameObject can only have one tag assigned – choosing names is therefore elemental, since GameObjects cannot have "properties" added by simply assigning additional tags.
Unstucking
The first obvious problem with this script was its interaction with the level. Since I decided the level obstructions to be fixed and designed, to a degree, to trap the NPCs, they could end up sitting behind a wall indefinitely. Therefore, I added an additional behaviour pattern, that would make them choose a new NPC to follow after being stuck a certain time – in the hopes, of course, that this new NPC would be in a position to un-stuck the former.
private var stuckForThisLong = 0; private var pastPosition:Vector3 = Vector3.zero; // [...] function FixedUpdate() { // [...] stuckDist = Vector3.Distance(this.transform.position, pastPosition); // Actually, this line should throw an error, right? if (stuckDist < 0.05) { stuckForThisLong++; if (stuckForThisLong > 20) { chooseOtherEvader(); stuckForThisLong = 0; } } pastPosition = this.transform.position; // this is at the end of the // FixedUpdate function. }
Unpiling
Another, not so obvious problem (for me at least) appeared later while playtesting.
Since the NPCs follow each other and are (in most cases) thrown randomly over the entire level at the start, they will cluster up in the middle of the level after some time, piling up. While this behaviour can be used in some cases for comedic effect, in most levels it will be unwanted.
This is solved by adding invisible trigger objects tagged with "Evader" at the corners of the level. Since they are tagged as possible targets, but are not moving, NPCs will move out to these corners, dragging along their followers as well. The dispersion of NPCs is improved enough to prevent piling up.
Interaction With the Player Character
Interaction with the player character is currently rather limited. Upon hitting an NPCEvader, it will be destroyed (leaving behind a satisfactory particle explosion). Scripting is applied to the player character.
var evaderExplosion:GameObject; // [...] function OnControllerColliderHit (hit:ControllerColliderHit) { // [...] if (hit.gameObject.tag == "Evader") { var explosionClone : GameObject; explosionClone = Instantiate(evaderExplosion,hit.transform.position,hit.transform.rotation); Destroy(hit.gameObject); // Transmit message of success; the floor is used as a // container for the basic level-related controller // scripts. // (A floor is always required for the character controller) GameObject.Find("floor").BroadcastMessage("CaughtEnemy", 1, SendMessageOptions.DontRequireReceiver); } // [...] }
When being hit by an NPCFollower character, the player character temporarily vanishes, and a message is being shown.
private var visual:GameObject; // This is the visual // representation of the player private var bummer:GameObject; // This is the message shown // to the player upon touching var playerExplosion:GameObject; // [...] function Start() { visual = GameObject.Find("Player"); bummer = GameObject.Find("Bummer"); // deactivate the message at first bummer.gameObject.SetActiveRecursively(false); } function OnControllerColliderHit (hit:ControllerColliderHit) { // [...] if (hit.gameObject.tag == "Follower") { // As before, broadcast message of failure GameObject.Find("floor").BroadcastMessage("GotCaught", 1, SendMessageOptions.DontRequireReceiver); // Deactivate visual representation visual.gameObject.SetActiveRecursively(false); // Activate message – this automatically // plays the first animation bummer.gameObject.SetActiveRecursively(true); // Make things sparkly var playerExplosionClone : GameObject; playerExplosionClone = Instantiate(playerExplosion, visual.transform.position, visual.transform.rotation); transform.root.transform.position.y = 20; yield WaitForSeconds(3); bummer.animation.Play("fadeOut"); visual.gameObject.SetActiveRecursively(true); yield WaitForSeconds(5); bummer.gameObject.SetActiveRecursively(false); } // [...] }
Important realisation: In order to properly interact with other character controllers and triggers, the NPCs and the PC needed an additional rigidbody component.
As an interesting side note: retaining the visual appearance of an NPC while using a different behaviour script can give the player the impression of a new behaviour pattern, even if he has seen it before.
From a Visual Identity to a Way to Tell a Story
If developing this game showed me something, then it was the importance of iteration.
The first versions of the game were nothing more than sandboxes to test out the NPC's behaviour. They already sported the look I wanted to give the game – since the undulating platforms were discarded, at least the textures should show a moiré effect. While this was possible, it pushed the game pretty close to some kind of art game: the flickering was rather headache-inducing.
During the time I pondered about the game's look, I started to play World of Goo with its Sign Painter, and decided to do something similar. Since the use of controls were limited, I placed the text directly into the level – and suddenly, a new style was born: turning the whole level into text. The simple black-and-white look was still here, but in a world of photo copiers, of endless masses of replicated text, it made far more sense. Of course, this required a change in name: weve was re-christened xeophin's CarbonCopy.
The first attempts at re-texturing the existing sandbox level proved successful. I created a first tutorial level – and ran into problems, since the 2D letters I used now had to face the camera. Fortunately, the Unify Community Wiki had a mono script ready, that solved (after some more tinkering with it) the problem.
using UnityEngine; using System.Collections; public class CameraFacingBillboard : MonoBehaviour { private Camera m_Camera; void Start() { m_Camera = Camera.main; } void Update() { transform.LookAt(transform.position + m_Camera.transform.rotation * Vector3.down, m_Camera.transform.rotation * -Vector3.forward); } }
This first level was to show the player how to move around and the fact that the NPCs would try to evade the player. It did not feel right, so I separated those two facts into two different levels: one to educate about movement, the other about the properties of the NPCEvaders. This time, it worked better.
Interesting to note is the how wrongly one can estimate the "psychological" size of the level. Since one works so long on one piece, one is convinced it is really huge – until it is being playtested and one manages to rush through in less than 15 seconds ...
At the time of writing, the game consists of five different levels that all introduce different aspects of play and show how the basic underlying mechanics could be variated. More ideas are in store, but have not yet been executed.
Problems, Pitfalls & Missing Things
Currently, the ways of penalising a player for hitting an NPCFollower are rather limited – apart from its visual appearance vanishing, nothing dire happens.
The only exception is the last level, where touching an NPCFollower lowers the number of NPCEvaders the player has already caught. Since only the visual representation of the player vanishes, but not its rigidbody, the NPCFollower keeps triggering this function, lowering the number in an alarming rate and making the level much harder to complete. I have not yet decided whether this behaviour is wanted or unwanted – as it is, it is rather out of my control and therefore tends to draw my displeasure ...
Except from this example, there is no actual penalty, and this should be solved – either through some kind of hard reset (which I would hate to do, since this is not the kind of game mechanic I would have to use) or the use of a score, which is not implemented yet – not the least because creating a score would not be that simple, since all levels provide rather different challenges, and would have to use a separate scoring system for every single level.
Also missing are sounds. While they would add to the atmosphere of the game, I felt those were an entirely new can of worms dangerous to open. I preferred to improve the game play instead of brooding over the perfect sound, which would have undoubtedly happened.
While the game in general is rather wordy, it fails to communicate what the player is doing wrong whenever a level restarts (as of now, the player can not finish the level anymore, since the required number of NPCEvaders is not available anymore). This should be improved as well. Also the last screen that marks the end of the game can be misread.