What I learned from SOMA

First of all, SPOILERS. This whole post is spoilers, so go and play SOMA first, if you haven’t yet, I highly recommend it.

The story felt compelling, interesting and unobtrusive. This was achieved by not removing control from the player during dialogue, except for the beginning dream sequence cutscene, which is about 30 seconds. You wake up in your apartment, answer a phone and are told that you have to find and drink a bottle of tracer fluid, and then make your way to an appointment. The apartment is the perfect safe zone for the player to learn the controls and basic mechanics of the game (find the thing, use the thing. This covers every objective in the game, along with: find the exit and avoid the baddies). The next scene provides you with an option to answer or decline a call, showing the player that they have options. These options were fairly infrequent throughout the game and I found that they did not seem to have any consequence at all, even the large one at the end where you choose whether or not you kill the last human or killing your own copy. I did only play through once, but these choices I made, along with others, never came up in dialogue again.

That having been said, the story itself was fantastic, although that may just be because I am interested in high science concepts and their philosophical repercussions.

The use of light to direct the player towards objectives was often very strong in this game, keeping that in mind during play made finding my way around very simple and helped lower my stress, although I had to make the conscious choice to look for these areas at times, as there were some areas with a lot of other sources of light, or they were obscured by scenery, however these areas never featured time constraints, giving me the time I required to figure them out.

The anxiety/boredom flow was used pretty well in this game, safe areas in the early game felt very safe at all times, but in later parts of the game had constant creepy or dangerous noises playing to make those areas feel much less safe than the earlier ones.

SomaPC12There was a small problem with the increase of enemy difficulty, where I moved from one area with an enemy that teleports around, and kills you if you look at it (which could see and hear you) and kills you if you are too close to it, to an area with an enemy that could not see you and you could look at, that seemed to only respond to sound. This was an enemy that was less dangerous than the previous one you encountered, in an area that you had more ability to move around (the previous was in a sunken ship, with tight corridors and was completely flooded so it felt like you had less move speed. You were expected to find your way through the ship and although the level itself was fairly linear, you had to spend most of the time not looking around or moving as quick as you can through the level, making it difficult for the player to map out the area. In contrast, the next level was just very dark. It was also a bit maze-like, but because of the nature of the enemy you had plenty of time to take the area in and get your bearings, not to mention the darkness meant that the use of lights to illuminate the goal were very obvious in contrast.

This situation could have been used to allow the player more confidence in themselves and may have been required to keep players interested. I myself felt quite hopeless when facing the previous enemy, if this difficulty had continued trending upwards, I may have given up. The problem I had was that there was a severe build up of this second monsters difficulty, much more than the previous, through constant ambient noise and story elements. This monster did not really need to be less difficult than the previous one though, as there was a whole level in between these monsters without any threat and much less ambient build up of stress.

355020960I noticed the possible use of an FSM in one of the enemies AI when I got it
stuck in one of its states. From what I could tell, it had 7 states; Idle with / without target, Patrol, Search for not – visible target, Search for lost target, Found target – Submissive / Aggressive behaviour. This particular AI (the small corrupted submarine with red lights) seemed to have a medium range of vision and hearing (you could sneak behind it but not run). If it spotted you, and then lost you, it would move to the location it last saw you, move around the area (possibly towards sounds made by the player?), then eventually return to its last position in its patrol (unless it heard or saw you). If spotted, the AI would move towards you, but in a submissive state, would attempt to keep a short distance between itself and the player (the player has a faster move speed than it). It had an extensive list of dialogue audio clips, in this state it would beg the player for their ‘structural gel’ (reinforcing the recent discovery that the player was not human). After a short time, the AI would move into the aggressive state, and advance on the player, attempting to collide with them. This was accompanied with aggressive dialogue and a change in the model (tentacles erupted out of its hull, reaching and wriggling towards the player).

I somehow caused the FSM on this AI to fall into its submissive state, but fail to move into its aggressive state (it just sat there begging me for structure gel). I’m pretty sure I was crouching and half hidden at the time, but I somehow caused its timer to never count down or to go outside its expected range, or possibly it skipped the exact number it was required to hit to move state, but that’s very unlikely. I held it there for a few minutes until I decided to move towards it, this caused it to move straight into its aggressive state and it killed me.

I hadn’t had any other issues with the AI in this game, so I believe that this was a freak occurrence and therefore nothing to worry about. This is also something that would be picked up in rigorous play testing.

The ending felt a little strange to me, as the copy of the player that was left behind could make its way back to the other sites, with Catherine’s copy (your AI ‘friend’ throughout the game), and just continue on with their lives, maybe working on the WAU to become something less horrible. I also don’t understand why the whole site did not try to continue the human race (and maybe nip the WAU in the bud, before everything got so nasty) and instead chose to only preserve the brain scans of a few people for a few hundred thousand years instead. This is just a prolonged but now inevitable death.

Speed, Juice and Optimization

This week, I made a game called CLEARCUTTER

0640e66bd401445e445aac4f6c368bb1

It’s a game where you take control of two rockets simultaneously, and try to cut down as many trees as possible in 2 minutes, while not hitting them with the rockets. the closer your rockets are together the faster you accelerate, but if they touch, you explode and lose speed.

There were several tasks that I aimed to achieve for this game that I had not attempted before

  • Modifying terrain at runtime with minimal frame loss
  • Creating an endless obstacle course with a scaling challenge over time
  • Optimization of program
  • Creation of an effective high score table that persist across play sessions
  • Use of visual and aural feedback systems (like juice)
  • Creation of a ‘trail renderer’ tool, that could use shapes as its starting point
  • Use of visuals instead of text to describe mechanics to the player

I excelled in the areas of simple coding or setting up structures that I had previously set up in other projects. This sounds like it should go without saying, but there is a startling difference in the time taken between this and something new. A problem that I had was with transitioning from the planning stage to the coding stage when I had the idea, but not the knowledge of implementing it and I had to spend a lot of time researching the subject. I also had issues with clumsily bodging a solution together. This is normally good enough for simple code or a small project but becomes an issue when other structures begin to depend on it, or when refactoring the code is required. It looks like spaghetti and pressing changes to a single line can cause huge issues if you can even understand what it does. I of course also had issues with getting stuck on a problem that I did not know the solution to. It meant hours of research over things that were often simple or silly mistakes I had made earlier.

When compared to work I had experience in, there were hours of difference. This was made obvious to me when setting up the high score table. It is just three arrays of data stored into player prefs, and then set and shifted under correct circumstances. Here is an example of that code.


 void Start()
 {



 // check if this is first play
 if (PlayerPrefs.GetInt("HasScore") < 1)
 {
 WriteNewScores();
 }

 //clear out the restart option text until it is available
 restartText.text = " ";
 // fade in the scene from black
 GameObject.FindWithTag("ScreenFade").GetComponent<ScreenFade>().FadeIn();

 LoadScores();

 input.interactable = false;
 newHighscore.enabled = false;

 // if the gamecontroller exists, get the score and speed from it
 if (GameObject.FindGameObjectWithTag("GameController") != null)
 {
 gameController = GameObject.FindGameObjectWithTag("GameController");
 gameScore = gameController.GetComponent<UI>().GetTreeCutScore();
 topSpeed = gameController.GetComponent<UI>().topSpeed * 100;
 CheckHighScore(gameScore);
 }
 //if not, set up from title scene transition
 else
 {
 fromTitle = true;
 newHighscore.enabled = true;
 newHighscore.text = "HIGHSCORES";
 restartText.enabled = true;
 restartText.text = "Press A to return";
 }

 // now load the new scores in case of highscore and display them for the player
 LoadScores();
 DisplayScores();

 // destroy the game controller, as it is no longer needed (and to stop it from looping through the game with restart)
 Destroy(gameController);


 }

 void Update()
 {
 // if the player presses A, and is able, restart the game
 if (Input.GetButtonDown("Fire1") && inputDone)
 {
 StartCoroutine(FadeTimer());
 GameObject.FindWithTag("ScreenFade").GetComponent<ScreenFade>().FadeOut();
 }
 }

 IEnumerator FadeTimer()
 {
 // destroy the screen fade object so it doesnt loop with restart, and load the title scene after 1 second
 yield return new WaitForSeconds(1);
 Destroy(GameObject.FindWithTag("ScreenFade"));
 Application.LoadLevel("StartScene");

 }

 void WriteNewScores()
 {
 // (for first play) set up the highscore table with 10 scores so it is not empty
 for (int index = 0; index <= 9; index++)
 {
 PlayerPrefs.SetInt("Score" + index.ToString(), 120 - (index * 12 + 20));
 PlayerPrefs.SetString("Name" + index.ToString(), "MAX");
 PlayerPrefs.SetString("Speed" + index.ToString(), (400 - (index * 9 + Random.Range(-200, 100))).ToString("0.00"));
 PlayerPrefs.SetInt("HasScore", 1);
 }
 }

 void DisplayScores()
 {
 // clear the score text field
 scoreText.text = "";

 // and write the scores stored in the arrays to screen
 for (int index = 0; index < scoreArray.Count; index++)
 {
 scoreText.text += (index + 1) + " " + nameArray[index] + " " + scoreArray[index].ToString() + " " + speedArray[index] + "GM/H" + "\n";
 }
 }

 void LoadScores()
 {
 //clear out the arrays
 scoreArray.Clear();
 nameArray.Clear();
 speedArray.Clear();

 // and add the player pref stored scores back into them
 for (int index = 0; index <= 9; index++)
 {
 scoreArray.Add(PlayerPrefs.GetInt("Score" + index.ToString()));
 nameArray.Add(PlayerPrefs.GetString("Name" + index.ToString()));
 speedArray.Add(PlayerPrefs.GetString("Speed" + index.ToString()));
 }
 }

 public void GetInput()
 {
 // receive the input from the input field and convert to upper case;
 nameInput = input.text.ToUpper();

 // if the input is not blank, to stop clicking auto restarting, then set the players input into the name player pref, using the stored int as the index key
 if (nameInput != "")
 {
 input.interactable = false;
 PlayerPrefs.SetString("Name" + tempInt.ToString(), nameInput);
 input.text = "";
 }

 // refresh the text display to show the new score & input, show restart info & allow restart
 LoadScores();
 DisplayScores();
 inputDone = true;
 restartText.text = "Press A to restart";
 }

 void CheckHighScore(int value)
 {
 // if we came from the title, dont do this
 if (!fromTitle)
 {
 foreach (int item in scoreArray)
 {
 // go through each score in the array, and if we have one that is higher, run update score & set text to reflect this to player
 if (value > item)
 {
 inputDone = false;
 UpdateScore(value);
 input.ActivateInputField();
 input.interactable = true;
 newHighscore.enabled = true;
 newHighscore.text = "!! NEW HIGHSCORE !!";
 break;
 }
 else
 {
 //if not, display the players score and info, and allow restart
 newHighscore.enabled = true;
 restartText.enabled = true;
 newHighscore.text = "You cut " + gameScore.ToString() + " trees, with top speed of: " + topSpeed.ToString("0.00") + "GM/H" + "\n";
 restartText.text = "Press A to restart";
 }
 }
 }
 }

 void UpdateScore(int value)
 {
 // run through array bottom to top (lowest to highest scores)
 for (int index = 9; index >= 0; index--)
 {
 // the first score that is higher than the players score will cause the score below it to be replaced
 if (value > scoreArray[index])
 {
 PlayerPrefs.SetInt("Score" + (index + 1).ToString(), PlayerPrefs.GetInt("Score" + index.ToString()));
 PlayerPrefs.SetString("Name" + (index + 1).ToString(), PlayerPrefs.GetString("Name" + index.ToString()));
 PlayerPrefs.SetString("Speed" + (index + 1).ToString(), PlayerPrefs.GetString("Speed" + index.ToString()));
 LoadScores();
 DisplayScores();
 }

 else
 {
 // if the score is lower than the players' then it will be shifted down a slot.
 PlayerPrefs.SetInt("Score" + (index + 2).ToString(), PlayerPrefs.GetInt("Score" + (index + 1).ToString()));
 PlayerPrefs.SetString("Name" + (index + 2).ToString(), PlayerPrefs.GetString("Name" + (index + 1).ToString()));
 PlayerPrefs.SetString("Speed" + (index + 2).ToString(), PlayerPrefs.GetString("Speed" + (index + 1).ToString()));
 // set the players score to the correct slot
 PlayerPrefs.SetInt("Score" + (index + 1).ToString(), gameScore);
 PlayerPrefs.SetString("Speed" + (index + 1).ToString(), topSpeed.ToString("0.00"));
 PlayerPrefs.SetString("Name" + (index + 1).ToString(), " ");
 tempInt = index + 1;
 // load and display the new score, with a gap for the players name
 LoadScores();
 DisplayScores();
 break;


 }
 if (index == 0)
 {
 // if we hit the top slot, shift it down and set the players score there
 PlayerPrefs.SetInt("Score" + (index + 1).ToString(), PlayerPrefs.GetInt("Score" + index.ToString()));
 PlayerPrefs.SetString("Name" + (index + 1).ToString(), PlayerPrefs.GetString("Name" + index.ToString()));
 PlayerPrefs.SetString("Speed" + (index + 1).ToString(), PlayerPrefs.GetString("Speed" + index.ToString()));

 PlayerPrefs.SetInt("Score" + index.ToString(), gameScore);
 PlayerPrefs.SetString("Speed" + index.ToString(), topSpeed.ToString("0.00"));
 PlayerPrefs.SetString("Name" + index.ToString(), " ");
 tempInt = index;

 LoadScores();
 DisplayScores();
 break;
 }
 }
 }

}

This is messy and not very optimal, but at under 200 lines, it should have taken an hour or so, not several.

Another issue I had was choosing a solution that I previously worked with and knew pretty well, that was not suited to the job. To check for collisions with trees, and cut them down as the player flew past them, I did a raycast check every frame from one rocket to the other. It worked fine, right up to the point where the rockets were moving. The problem was that the area of detection would eventually move so fast as to start on one side of an object you are trying to detect, and in the next frame, have moved to the other side of it, missing it completely. This issue is known as tunneling, which you get when you introduce high speeds or small objects to your game. There are several complex and elegant solutions that will avoid a tunneling issue, but what I chose to do was create a collision box whose size, rotation and position would change to reflect the amount of movement that would occur in each frame. In short, its depth was equal to the player’s speed, it was always looking at one of the rockets to keep its rotation, and its position was in between the rockets, and half their speed ahead of them.

This setup is so effective that I am confident it would detect collisions at any speed.

I tried to add feedback loops into the game, or juice and a lot of these were based on speed. To help the player feel as though they were moving fast, I made the field of view scale up to a point as they gained speed. It seems fairly subtle until you have a sudden loss in speed from collision. Once the speed reached 275, a particle emitter switched on and created shockwave like rings around the rocket. I also scaled the sizes of the rockets exhaust trail and light to reflect the acceleration from the rockets proximity to one another. In testing, this was usually noticed by the player a minute into their first run or less. This, coupled with the high score table led most players to try another run, so they could try and implement their idea of the best strategy. This best strategy was almost always the one I had wanted the player to attempt from the design stage. Along with camera effects like bloom, sun shafts, audio effects for knocking down trees and a high energy BGM, these were the positive juice items that I included.

Negative juice items were things like warning signals and explosions. The warning signals seemed reasonably effective in the early game at conveying to the player that they should not collide with trees and seemed to raise stress levels in the late game with an almost constant warning alarm going off. There was an issue with a lack of collisions at high speed, as the detection is from a single raycast forwards. I chose to leave this in, as the difficulty of reaching extreme speeds would be impossible, and would remove some of the enjoyable experience that could be had in my game. The explosions happen at the end of the run, which was received well, and also when the rockets collide with each other, which created a problem. The explosion takes up a large amount of the screen, is very bright and very loud: it is an extremely strong feedback. At the start of the game, as the rockets are constantly drawn together, they will collide. To a new player, this causes stress and the player learns that the rockets must be kept apart. Because of this, many new players held the rockets as far apart from each other for the whole run, missing the lesson of proximity = acceleration. This causes a slow and boring run, when the player realizes they were playing it wrong (from the high scores, which are now daunting multitudes greater than the players score) they will give up. This might be fixed by ramping up the strength of that attraction over the first 10 seconds, so there is no explosion at the start unless they collide the rockets themselves (but may make the controls confusing) or by better explaining this mechanic before the player is allowed to play the game (for the first run).

While creating this game, I had some serious optimization issues, which were highly problematic due to the large variation in speed, and immense top speed (~100 units/frame is the highest I have seen). The FPS would often drop below 30, when at these speeds I needed it to sit at 60. On some computers, it would even sit well above 90 for a while, which also made the game unplayable as the controls were way too touchy and the player had no time to react to anything. Moving terrains, spawning in hundreds of trees despite speed and fading out every material on a tree as you destroyed it, lots of grass and poorly optimized code were causing these issues. I received help to identify what was causing the severe loss in framerate, cleared up the code to make it as light as possible, changed the way my trees were spawned in (instead of just every half second, it also waited till the player had moved forward 5 units, so that if you were moving slowly there wasn’t a huge wall of trees spawning) and reduced the quality of the models, as since you are moving so fast, there is no time to notice model detail. Reducing the size of the terrains helped with the hang time of moving them, and once the other frame rate issues were fixed, there was plenty of time left for the operation to complete before causing a hang.

I am fairly happy with how this game turned out and if there is a good reason to keep working on it I have some plans with more things I could do to it.

How to create spherical gravity in Unity

In my previous blog posts, I talked about a project I was working on, a Katamari Damacy clone, called Clump Soul. What I chose to do was use a sphere for the play area instead of a plane, or the built in terrain that unity uses.

If you are here looking for instructions on creating a 3D world using unity’s built in terrain generator, let me tell you now, it is not built for that, and you will create a near endless amount of work for yourself trying to make it do this. If this does not deter you, check here, here or here. If that wont work for you, then use a smooth sphere as your terrain and just place your own grass and trees, or even create your own sphere with hills on it in Blender, for example. What I’m going to go over here is the problem of gravity and the player camera.

There are two main problems to work out when making a controllable character that will move across a spherical world. The spherical gravity and a controllable 3rd person camera that will orbit the player smoothly, while keeping them centered.

Problem #1: unity physics uses linear gravity, and has no spherical gravity built into it. What you need to do is have a point in space that everything is drawn towards, and treat that direction as downwards, so that each object also rotates as it travels around the source of gravity.

Think of v as the forward direction, and a as the downward force of gravity
Think of v as the forward direction, and a as the downward force of gravity

To the right is a visual example of what we want to achieve. The black orb is moving around the planet’s surface (or large ring around the planet which I am pretending is the surface), and is always pulled towards the center.

To achieve this, the first thing we need to do, is create the gravity. There are two types we can have, gravity that increases in strength as you get closer to the center of mass (great for a game featuring multiple celestial bodies, like KSP), and gravity that is applied at the same rate no matter the distance from the center of mass (like in Super Mario Galaxy). Picking the right one is very important, but the difference in the code is minimal, a diminishing gravity strength can be applied based on the distance between the object and the center of mass.

rigidbody.AddForce((planet.position - transform.position).normalized * acceleration);

What this code does is add force to the object this script is attached to. That force is in the direction of the ‘planet’ (get the gameobject’s transform for this), because we get a magnitude by subtracting the objects’ current point in space from the planets point in space. This is automatically normalized, and we then multiply it by acceleration (any number you put in, this will be how fast you are pulled towards the planet).

Now, getting the object to treat it as down (as if it is falling towards it).

transform.rotation=Quaternion.LookRotation(planet.position-transform.position,transform.up)

What this does is rotate the object to face towards the planet, using the same method to get a magnitude as before for the direction we want to face. We also use the objects’ own up direction for the new up direction, this stops the object flipping upside down when we move to the bottom of the planet.

In my next post, I will cover the spherical planet 3rd person camera.

APA’s for spherical terrain links:

Spherical procedural terrain shader based on slope. (n.d.). Retrieved July 21, 2015.

Procedural planets. (n.d.). Retrieved July 21, 2015.