My magnum opus from the world of mobile gaming

Hello, Habr! Today, September 26, is my birthday, which means for me this is a great reason to roll out an article about the sequel to my puzzle. I warn you that I am an amateur, which means that there will be many errors in ALL aspects of the development (if you find it, write, I will gladly take into account). In this article I would like to tell everything (well, or almost everything) about how I made the sequel, how I went to this and what I came to.



In order not to get confused, here I mean the meanings of the terms that are in the article:

The original is the first part, a game with an underground drive of the techno demo. You can read about it here .



The sequel is the second part of the series, the game that is discussed in this article.



I will periodically compare the original game with the sequel to emphasize the difference between the two.



Briefly about development



I started work on the game in late January and by the end of March the technical part was completed (2 months). After I took up another game and returned to continue to develop this game in mid-May. I graduated clearly at the end of the summer and all this time (3.5 months) I filled the game with content. And as a result, the sequel was made even faster by me than the original game (6 months versus 5.5 months).



I made a game on the unity engine. I would like these guys to make their own engine and move forward in programming, but something went wrong and still decided to make the game on a standard, but tested by me instrument.



Between the original and the sequel



The idea of ​​creating a sequel came to me a month before the release of the original game (somewhere in August). Seeing the mistakes that I made, I wanted to delete everything and start working again with successful achievements. But I did not begin to change anything due to the fact that there was a lot of problem code, all the content was ready, and I just delayed the development. It was necessary to go to release.



After the release, I was again tormented by the idea of ​​a sequel. This time I did not start, because I was morally lazy, after the release it was completely soft. I wanted something new and interesting. Mass experiments began.



The next 3 months I tried to implement any idea, any setting, any concept in games. I did in spite of the scale of ambitions, the difficulties of execution, and also sometimes despite the logic and common sense. As a result, I got about 50 projects. They were all of different genres: from shooters to strategies, from platformers to rpg games.



So experiments would continue, until I got tired. And I'm tired of not making games, but the incompleteness of the games I made. I gave myself a goal: to make at least some game a week before the end. And so my second game appeared.



Pro 2 game
This game is very simple and complex at the same time. It is necessary to cut lines and not cut graphs. The meaning of the game is that each cut line was divided by 2, and a graph appeared in its center. The feature of the game was that all the geometry was dynamic. Graphs moved, and lines always connected certain graphs.

















After this, I was motivated (I'm motivated) and ready for a new project. I felt a surge of strength and still took up the sequel to my game.



Idea



Before starting to do something, I decided to take a look at the original in full. And horrified. From quality. The game most of all mowed down under standard puzzles: the need to unlock levels, collect stars, a timer, finish, only all this was done without a budget and very tasteless. The original really lacked animations! Although there was something original in it, and something, probably, sincere. Although even here they managed to get ahead of me.



I found something similar
It turns out there is a very similar game with an almost identical name. And she looks like a more successful variation of my game. I found out about her from this video .

After I found out that this game is an exclusive LG Smart TVs. It was created by the Russian division of LG R&D Lab in 2014:















It is controlled by arrows "left" and "right" on the remote control. In the same way as in my game (2 parts of the screen). What can I say, the angle of inclination is the same - 30 °. Purely technically, it can be said that my game is plagiarized by this. Although I found out about her about 2 months after the release of the first game.





















Understanding the very deplorable position of the original, I decided to revive the game with radical changes, to make it better. And then the fantasy flew: let there be a plot and there will be a choice in it, there will be bosses with thoughtful attacks, there will be a production that the original lacked so much, there will be the feeling of a single completed adventure, etc. In general, all the best ideas that came to me during the time between the original and the sequel. And everything that didn't work or worked badly was destined for me to throw out of the sequel.



First demo



It all started of course with him. I decided: "If you solve problems, then do it thoroughly." And the first victim of such changes was management. I could just steal it from a similar game (see above). This is exactly the management that I originally wanted, but did not know how to make it. A supplement would be quite simple: just add rotation animations each time you click. But that was not for me. At least for the control to be perceived as well as in a similar game, it was necessary to make the same static camera and obviously reduce the levels along with the pace of the game. But I wanted action, dynamics and speed, so I made a logical development of the original control. Now, instead of pressing and rotating a certain degree, there was a clamping and the degree of the final rotation was determined by its duration. It looked clearly better than the original.



It was because I normally controlled the control that the main bug of the original disappeared and now it was possible to make levels MUCH more loaded than in the original without fear of lags and friezes. And then came the experimental part.



Graphic demo



I never knew how to draw normal graphics and almost always it was replaced by the technological part, or rather its normal execution. And this game was no exception. Instead of simple normal sprites, realistic light appeared. It was an illusion of 2d light. In fact, this is three-dimensional light, against a metal surface, and all objects had materials with specific shaders. It looked pretty good:















In tests, it produced stable 60 fps, but on the phone, even on my sony xperia it was at around 20 fps and sank to 10 fps. And I ran into a performance ceiling. I had to go a different way, the path of destructibility ...



Destructible



Initially, I generally thought this was a bad idea. But I decided to try and now this is my main gaming show. According to the plan, I again wanted more realism, namely, destruction into generated fragments, depending on the direction and strength of the impact. But the plan again rested on the ceiling, this time my knowledge. I had to simplify to a simpler one.



Now, destructibility was based on a simpler principle of operation, namely, it created a copy of itself, only from physical fragments, and the original object removed the components of SpriteRenderer, Collider2D and, if there was one, disabled Rigidbody2D.



But another question arose - colliders. On the one hand, you could use PolygonCollider2D and not be tormented, but on the other, you would have to suffer later in game design and optimization. Therefore, all fragments of the destroyed blocks had BoxCollider2D (even fragments of round objects).



Also, a significant contribution to the optimization was made by the correct setting of the time fixedstep parameter (it became equal to 0.0 (3) or 30 per second). But now, at high speeds, the object flew through it, and this definitely affected the game design.



These elements brought optimization to an acceptable level and now there could be up to hundreds of physical objects on the stage! After the original, it was definitely a breakthrough, revolution, etc. Realizing that I was moving in the right direction, I decided to fix another long-standing gaming problem: overwhelming hardcore. In order to somehow provoke the game I made ...



Damage system



For me, this is the most obscure part of the development, which has been rewritten 2 times. Work on it was ongoing. As a result, an extremely sophisticated system came out, but it worked quite extensively.



But first, it’s worth mentioning how damage perception works here. It may seem that it works on the principle “the harder you hit, the stronger the damage”, but this is not so. In most cases, it works on the principle of “the longer the contact - the greater the damage”, where the place of such an important thing as “impact force” was replaced by a damage coefficient that was manually configured for each object that deals damage, depending on the situation. This happened due to the fact that the time fixedstep turned out to be so large that a powerful bug was created: the game did not manage to process Enter2D. And this created situations like: crashed at high speed - did not receive damage. Why didn’t I fix it? Even I can’t say that.



So where did the damage system begin? From health. The player has a health equal to 1 (later increased to 2). Yes, this is still not enough and at the first strong touch with the trap he will die, but at least at low speed there is a chance to survive (even several times). I did not want to change the original. “But what will cause damage to the player?” - I thought and came up with the main traps.



Main traps



The basis of my puzzle consisted of traps, but they contradicted the name of the game. From the name it follows that the game should be about balls that fall under the influence of gravity. But there were not so many of them. Instead, there were more standard puzzles.



The main and the first was a saw. Simple and clear puzzle. It was written not very optimally, during the post-production period I fixed it.









Saw Script
using UnityEngine; public class Saw : GlobalFunctions { public AudioClip setClip; private TypePlaying typePlaying = TypePlaying.Sound; private AudioBase audioBase; private float speed = 4f; private Transform tr; private void Awake() { audioBase = GameObject.FindWithTag("MainCamera").GetComponent<AudioBase>(); tr = transform; } private void Update() { float s = Time.fixedDeltaTime / 0.03f * (Time.deltaTime / 0.03f); tr.localEulerAngles = new Vector3(0f, 0f, tr.localEulerAngles.z - speed * s); } private void OnCollisionEnter2D(Collision2D collision) { if (collision.collider.tag == "Player") { audioBase.SetSound(setClip, 1, 0.2f, typePlaying, false); } } public float GetSpeed() { return speed; } }
      
      







Next was the laser, which, well, very heavily loaded everything. If you put 40 such pieces on the stage, the game will begin to lag significantly. But I also had a desire to add full-fledged physical laws of light, namely reflection or even refraction. But there was no time, I did not finish it. Although I optimized some things, it didn’t help much.









Laser script
 using UnityEngine; public class Laser : MonoBehaviour { public Vector2 vector2; public bool active = true; public GameObject laserActive; public LineRenderer lr1; public Transform tr; public BoxCollider2D bcl; public Damage dmg; private void Start() { lr1.startColor = lr1.endColor = LaserColor(); } public Color LaserColor() { Color c = new Color(0f, 0f, 0f, 1f); switch (dmg.GetTypeLaser().Type2int()) { case 1: c = new Color(1f, 0f, 0f, 1f); break; case 2: c = new Color(0f, 1f, 0f, 1f); break; case 3: c = new Color(0f, 0f, 0f, 0.4901961f); break; case 4: c = new Color(1f, 0.8823529f, 0f, 1f); break; case 5: c = new Color(0.6078432f, 0.8823529f, 0f, 1f); break; case 6: c = new Color(1f, 0.2745098f, 0f, 1f); break; } return c; } private void Update() { LaserUpdate(); } private void LaserUpdate() { if (active == true) { Vector2[] act1 = Points(tr.position, -tr.up); lr1.SetPosition(0, act1[0]); lr1.SetPosition(1, act1[1]); bcl.size = new Vector2(0.1f, 0.1f); bcl.offset = act1[2]; } return; } private Vector2[] Points(Vector2 start, Vector2 end) { Vector2[] ret = new Vector2[3]; RaycastHit2D hit = Physics2D.Raycast(tr.position, -tr.up, 200f); ret[0] = tr.position; ret[1] = hit.point; vector2 = ret[1]; float distance = Vector2.Distance(tr.position, hit.point); bcl.size = new Vector2(0.1f, 0.1f); if (hit.collider == bcl) { ret[2] = new Vector2(0f, 0.5f); } else { ret[2] = new Vector2(0f, -distance - 0.2f); } return ret; } }
      
      









The last trap was the bomb, and before adding it, I rewrote the damage system, in particular, transferred everything related to the player’s health to a separate HealthBar script (useful for other purposes). After the bomb still appeared, and its physics left much to be desired, in the process it was finished again. And in the end it turned out again quite worthily.









Explosion Script
 using System.Collections; using UnityEngine; public class Explosion : GlobalFunctions { public float power = 1f; public float radius = 5f; public float health = 20f; public float timeOffsetExplosion = 1f; public GameObject[] contacts = new GameObject[0]; public Animator expAnim; public bool writeContacs = true; public AudioClip setClip; private float timeOffsetExplosionCount; private float alphaTimer; private bool isTimerOn = false; private bool firstAPEvirtual = true; private Collider2D cl; private Rigidbody2D rb; private SpriteRenderer sr; private AudioBase audioBase; private Explosion explosion; private void Awake() { audioBase = GameObject.FindWithTag("MainCamera").GetComponent<AudioBase>(); cl = GetComponent<Collider2D>(); rb = GetComponent<Rigidbody2D>(); sr = GetComponent<SpriteRenderer>(); explosion = GetComponent<Explosion>(); } private void Start() { alphaTimer = sr.color.a; StartCoroutineTimerOffsetExplosion(); } private void OnCollisionEnter2D(Collision2D collision) { if (writeContacs == true) { int cont = contacts.Length; GameObject[] n = new GameObject[cont + 1]; if (cont != 0) { for (int i = 0; i < cont; i++) { n[i] = contacts[i]; } } n[cont] = collision.gameObject; contacts = n; } } private void OnCollisionExit2D(Collision2D collision) { if (writeContacs == true) { int cont = contacts.Length; if (cont != 1) { int counter = 0; GameObject[] n = new GameObject[cont - 1]; for (int i = 0; i < cont; i++) { if (contacts[i] != collision.gameObject) { n[counter] = contacts[i]; counter++; } } contacts = n; } else { contacts = new GameObject[0]; } } } public void ActionExplosionEmulation(GameObject obj) { float damage = 0f; if (obj.CompareTag("Laser")) { damage = obj.GetComponent<Damage>().senDamage; } else { damage = obj.GetComponent<Power>().power; } health = health - damage; StartCoroutineTimerOffsetExplosion(); return; } public void StartCoroutineTimerOffsetExplosion() { if (health <= 0f && isTimerOn == false) { isTimerOn = true; timeOffsetExplosionCount = timeOffsetExplosion; StartCoroutine(TimerOffsetExplosion(0.1f)); } } private IEnumerator TimerOffsetExplosion(float timeTick) { yield return new WaitForSeconds(timeTick); timeOffsetExplosionCount = timeOffsetExplosionCount - timeTick; if (timeOffsetExplosionCount > 0f) { float c = timeOffsetExplosionCount / timeOffsetExplosion; sr.color = new Color(1f, c, c, alphaTimer); StartCoroutine(TimerOffsetExplosion(timeTick)); } else { ExplosionAction(); } } private void ExplosionAction() { rb.gravityScale = 0f; rb.velocity = Vector2.zero; audioBase.SetSound(setClip, 2, 1f, TypePlaying.Sound, false); Destroy(cl); CircleCollider2D c = gameObject.AddComponent<CircleCollider2D>(); c.isTrigger = true; c.radius = radius; tag = "Explosion"; if (PlayerPrefs.GetString("graphicsquality") != "high") { Destroy(sr); StartCoroutine(Off()); } else { expAnim.enabled = true; StartCoroutine(Off2High()); } } public IEnumerator Off() { yield return new WaitForSecondsRealtime(0.1f); gameObject.SetActive(false); } public IEnumerator OffHigh(CircleCollider2D c) { yield return new WaitForSecondsRealtime(0.1f); c.enabled = false; } public IEnumerator Off2High() { yield return new WaitForSecondsRealtime(1.5f); gameObject.SetActive(false); } public void APEvirtual() { int cont = contacts.Length; if (cont != 0 && firstAPEvirtual == true) { firstAPEvirtual = false; for (int i = 0; i < cont; i++) { if (contacts[i] != null) { if (contacts[i].GetComponent<PhysicsEmulation>()) { contacts[i].GetComponent<PhysicsEmulation>().ExplosionPhysicsEmulation(explosion); } } } } } public void AnimFull() { sr.color = new Color(1f, 1f, 1f, 1f); sr.size = new Vector2(3f * radius, 3f * radius); return; } }
      
      







Having looked at the entire damage system, I decided to thoroughly rewrite it. And this time, Damage fit all possible damage variations into one Damage script, and made a similar ActionPhysicsEmulation method for destructible blocks (in the end, for each individual damage type, its own optimized method was written). Also, the intensity of damage was determined by the intensity of the "strength" of the object (the script was only on the player).



And in the end, only these 3 puzzles were a cut above the original. But this was not a reason to stop: I also did not forget to experiment throughout the development. So it appeared.



Force field (disables gravity, slows down and slowly kills)









Script VelocityField
 using UnityEngine; public class VelocityField : GlobalFunctions { public float percent = 10f; public float damage = 0.01f; public float heal = 0.01f; public GameObject[] contacts = new GameObject[0]; private HealthBar hb; private void Awake() { hb = GameObject.FindWithTag("MainCamera").GetComponent<Management>().healthBar; } private void FixedUpdate() { if (contacts.Length != 0) { for (int i = 0; i < contacts.Length; i++) { if (contacts[i] != null) { if (contacts[i].GetComponent<Rigidbody2D>()) { float s = Time.fixedDeltaTime / 0.03f; Vector2 vel = contacts[i].GetComponent<Rigidbody2D>().velocity; contacts[i].GetComponent<Rigidbody2D>().velocity = vel / 100f * (100f - percent * s); } } else { contacts = Remove(contacts, i); } } } } private void OnTriggerEnter2D(Collider2D collision) { if (collision.GetComponent<Rigidbody2D>()) { Rigidbody2D rb2 = collision.GetComponent<Rigidbody2D>(); if (rb2.isKinematic == false) { VelocityInput vi = collision.GetComponent<VelocityInput>(); vi.fields = Add(vi.fields, gameObject); rb2.gravityScale = 0f; rb2.freezeRotation = true; vi.inVelocityField = true; if (collision.GetComponent<Destroy>()) { collision.GetComponent<Destroy>().ActiveTimerDeleteChange(300f); } if (collision.tag == "Player") { hb.StartVFRad(damage); } contacts = Add(contacts, collision.gameObject); } } } public void OnTriggerExit2D(Collider2D collision) { if (collision.GetComponent<Rigidbody2D>()) { Rigidbody2D rb2 = collision.GetComponent<Rigidbody2D>(); if (rb2.isKinematic == false) { VelocityInput vi = collision.GetComponent<VelocityInput>(); vi.fields = Remove(vi.fields, gameObject); if (vi.fields.Length != 0) { rb2.gravityScale = 0f; rb2.freezeRotation = true; vi.inVelocityField = true; } else { rb2.gravityScale = 1f; rb2.freezeRotation = false; vi.inVelocityField = false; } if (collision.GetComponent<Destroy>()) { collision.GetComponent<Destroy>().ActiveTimerDeleteChange(60f); } if (collision.tag == "Player") { hb.EndVFRad(heal); } contacts = Remove(contacts, collision.gameObject); } } } }
      
      







Stomp (he killed players by crushing them)







Tramp Script
 using UnityEngine; public class TrampAnim : MonoBehaviour { public float speed = 0.1f; public float speedOffset = 0.01f; public float damage = 1f; private float sc; private float maxDis; public Vector3 start; public Vector3 end; public TrampAnim ender; public bool active = true; public bool trigPlayer = false; private AudioSet audioSet; private bool audioAct; private Transform tr; private HealthBar hb; public int count = 0; public void Start() { if (active) { tr = transform; maxDis = Vector2.Distance(start, end); sc = Vector2.Distance(tr.localPosition, start) / maxDis; hb = Camera.main.GetComponent<Management>().healthBar; audioAct = GetComponent<AudioSet>(); if (audioAct) { audioSet = GetComponent<AudioSet>(); } } } public void Update() { if (active) { float s = Time.fixedDeltaTime / 0.03f * (Time.deltaTime / 0.03f); if (count == 0) { tr.localPosition = Vector2.MoveTowards(tr.localPosition, end, (speed * sc + speedOffset) * s); if (tr.localPosition == end) { count = 1; if (trigPlayer && ender.trigPlayer) { hb.Damage(100f, tag, Vector2.zero); } if (audioAct) { audioSet.SetMusic(); } } } else { tr.localPosition = Vector2.MoveTowards(tr.localPosition, start, (speed * sc + speedOffset) * s); if (tr.localPosition == start) { count = 0; } } sc = Vector2.Distance(tr.localPosition, start) / maxDis; } } public void OnCollisionEnter2D(Collision2D collision) { Transform trans = collision.transform; string tag = trans.tag; if (tag == "Player") { trigPlayer = true; } else if (active == false) { if (trans.GetComponent<PhysicsEmulation>()) { trans.GetComponent<PhysicsEmulation>().TrampAnimPhysicsEmulation(GetComponent<TrampAnim>()); } } } public void OnCollisionExit2D(Collision2D collision) { string tag = collision.transform.tag; if (tag == "Player") { trigPlayer = false; } } }
      
      







Radiation (which slowly reduces health)









Script Radiation
 using System.Collections; using UnityEngine; public class Radiation : MonoBehaviour { public bool isActiveRadiation = false; private Management m; private HealthBar hb; private void Awake() { gameObject.SetActive(PlayerPrefs.GetString("ai") == "off"); m = GameObject.FindWithTag("MainCamera").GetComponent<Management>(); hb = m.healthBar; } private void Start() { StartCoroutine(RadiationDamage()); } public IEnumerator RadiationDamage() { yield return new WaitForSeconds(0.0002f); if (isActiveRadiation) { hb.StraightDamage(0.0002f, "Radiation"); } StartCoroutine(RadiationDamage()); } public void OnTriggerEnter2D(Collider2D collision) { if (collision.tag == "Player") { isActiveRadiation = true; hb.animator.SetBool("isVisible", true); } } public void OnTriggerExit2D(Collider2D collision) { if (collision.tag == "Player") { isActiveRadiation = false; hb.animator.SetBool("isVisible", false); if (hb.healthBarImage.fillAmount == 0f) { m.StartGraphics(); } } } public void OnCollisionEnter2D(Collision2D collision) { if (collision.transform.tag == "Player") { hb.animator.SetBool("isVisible", false); PlayerPrefs.SetString("ai", "on"); gameObject.SetActive(false); } } }
      
      







Trap (a blue ball that kills when touched, which is a reference to The World's Hardest Game)









Script DeathlessScript
 using UnityEngine; public class DeathlessScript : MonoBehaviour { private HealthBar hb; private void Awake() { hb = Camera.main.GetComponent<Management>().healthBar; } public void OnTriggerEnter2D(Collider2D collision) { if (collision.tag == "Player") { hb.Damage(10f, tag, Vector2.zero); } } }
      
      







I did not register all these types of damage in the Damage script, but they generally worked fine with crutches. After this, additional mechanics came in line.



Additional mechanics



They were made varied. There were quite a few of them, so that all of them were of interest and small enough to be functional for interaction with most game mechanics.



The first such mechanics were the gates. The very first and most functional of all. Definitely useful in all locations where functional barriers were needed. It also has additional functions: isActive for determining the start state and isState in order to fix the position after activation (the names are mixed up, but when it was noticed it was too late to fix).









Script Gate
 using UnityEngine; using System.Collections; public class Gate : MonoBehaviour { [Header("StartSet")] public Vector2 gateScale = new Vector2(1, 4); public float speed = 0.1f; public bool isReverse = false; public bool isEnd = true; public Vector2 animSetGateScale = new Vector2(); public Vector2 target = new Vector2(); [Header("SpriteEditor")] public Sprite mainSprite; [Header("Assets")] public GameObject door1; public GameObject door2; private IEnumerator fixUpdate; private void Start() { SpriteRenderer ds1 = door1.GetComponent<SpriteRenderer>(); SpriteRenderer ds2 = door2.GetComponent<SpriteRenderer>(); ds1.sprite = mainSprite; ds2.sprite = mainSprite; if (isReverse == false) { animSetGateScale = target = gateScale; } fixUpdate = FixUpdate(); SetGate(animSetGateScale); } private IEnumerator FixUpdate() { yield return new WaitForSeconds(0.03f); if (animSetGateScale != target) { float s = Time.fixedDeltaTime / 0.03f; animSetGateScale = Vector2.MoveTowards(animSetGateScale, target, speed * s); SetGate(animSetGateScale); StartCoroutine(FixUpdate()); } } private void SetGate(Vector2 scale) { SpriteRenderer ds1 = door1.GetComponent<SpriteRenderer>(); SpriteRenderer ds2 = door2.GetComponent<SpriteRenderer>(); Vector2 size = new Vector2(mainSprite.texture.width, mainSprite.texture.height); float k = size.x / size.y; ds1.size = new Vector2(gateScale.x, scale.y / 2f); ds2.size = new Vector2(gateScale.x, scale.y / 2f); BoxCollider2D d1 = door1.GetComponent<BoxCollider2D>(); BoxCollider2D d2 = door2.GetComponent<BoxCollider2D>(); d1.size = new Vector2(gateScale.x, scale.y / 2f); d2.size = new Vector2(gateScale.x, scale.y / 2f); door1.transform.localScale = new Vector3(1f, 1f, 1f); door2.transform.localScale = new Vector3(1f, 1f, 1f); door1.transform.localPosition = new Vector3(0f, (gateScale.y / 2f) - (scale.y / 4f), 0f); door2.transform.localPosition = new Vector3(0f, -(gateScale.y / 2f) + (scale.y / 4f), 0f); } public void OnTriggerEnter2D(Collider2D collision) { if (collision.CompareTag("Player")) { if (isReverse == false) { target = Vector2.zero; } else { target = gateScale; } StopCoroutine(fixUpdate); fixUpdate = FixUpdate(); StartCoroutine(fixUpdate); } } private void OnTriggerExit2D(Collider2D collision) { if (collision.CompareTag("Player") && isEnd == true) { if (isReverse == false) { target = gateScale; } else { target = Vector2.zero; } StopCoroutine(fixUpdate); fixUpdate = FixUpdate(); StartCoroutine(fixUpdate); } } }
      
      







Similar functionality was possessed by physical objects. No, these are not objects from destruction, they are just physical objects (although they could also be destroyed, but did not use this mechanics). There are not so many of them in puzzles, but they go well with other mechanics. For example, with a gate: when an object touches a gate trigger, the gate opens.



Even since I learned to “own power”, as many as three mechanics controlled it. These were triggers with the same code for interacting with objects, but each performed tasks in its own way. The first was a force field (it slowed down the object, multiplying the force by a certain factor). The second added strength in the direction of the point and the point had “gravity”. The third was made by accident: when the puzzle related to zero gravity did not work, this script saved it. In it, the object changes the direction of the force, without changing it itself, its intensity.









How does it work
First, by the Pythagorean theorem, the hypotenuse is calculated, which is the coefficient of the vector and is useful for restoring strength. The angle is then calculated using the Atan2 function. After that, offsetAngle is added to the corner and a new vector is constructed based on the sine and cosine, which is multiplied by a coefficient and a changed direction is obtained without a changed force.

 public Vector2 RotateVector(Vector2 a, float offsetAngle) { float power = Mathf.Sqrt(ax * ax + ay * ay); float angle = Mathf.Atan2(ay, ax) * Mathf.Rad2Deg - 90f + offsetAngle; return Quaternion.Euler(0, 0, angle) * Vector2.up * power; }
      
      







On this, my whole fantasy of extras dried up. Yes, there were ideas like a bomb on a rope, cable car, etc. But then a normal idea came up: you have to render the game again. Still, I’ll be honest with myself: the vast majority of people play mobile games, and hardly any of them will play my game if the game is unbearably complicated. I decided to start off with puzzles that killed the player with one hit, but I did not want to change the damage because of the destructibility. And then came the idea of ​​normal additional mechanics: boosters or modifiers.



According to the concept, they gave temporary improvements that are associated with some basic values. There were 5 boosters: treatment, immortality, time dilation (slow mo), a change in gravity and a change in the player’s mass.



But it seemed like some kind of standard: trigger balls scattered across the level to aid passing. And so I added these boosters to the laser. Changed the mechanics a bit and it worked.









Now the laser has 5 modes of interaction with the player: damage and healing, immortality, time dilation (slow mo), change in gravity and change in player’s mass. That is the same thing, but with one difference: the laser acts on the player constantly and if you leave the laser the effect will disappear immediately (or after a while). Yes, boosters have almost the same thing, but lasers are not standard (and so the whole game).



The physical theme of the game made it possible to create a trampoline, which is usually used to disperse the player with the subsequent destruction of the wall (although this is a simple BoxCollider2D with PhysicsMaterial, in which the bounce parameter was twisted for different bouncing forces).



And the sandiness of the game allowed you to create your own scripts for animation. Basically, they moved the object from point to point or rotated the object. Previously, they had significantly more functions: the ability to rotate an object animatedly (by points), scale (by points), more precise labels for the beginning and end of an object’s animation, etc. But due to the fact that these were atavisms, which in total frantically consumed productivity, I had to cut them out in the name of optimization. The animation script is used wherever you need to show a simple animation, because as I said: “The original was very lacking animations!” There are only two scripts:



BasicAnimation and PointsAnimation.



BasicAnimation Script
 using UnityEngine; using System.Collections; public class BasicAnimation : GlobalFunctions { public AnimationType animationType = AnimationType.Infinity; public float speedSpeed = 0.05f; public float rotation = 0f; private bool make = true; private bool animMake = false; private bool isMoved = false; private Transform tr; private float rotationActive = 0f; public void SetPos(bool pos, float m) { rotationActive = rotation * (pos ? 1 : m); } private void Start() { tr = transform; animMake = false; switch (animationType) { case AnimationType.Infinity: make = true; isMoved = true; rotationActive = rotation; break; case AnimationType.Start: make = false; isMoved = false; break; case AnimationType.End: make = true; isMoved = true; rotationActive = rotation; break; case AnimationType.All: make = false; isMoved = false; break; } } public void TimerAnim(float timer, bool anim) { StartAnim(anim); StartCoroutine(TimerTimerAnim(timer, anim)); } private IEnumerator TimerTimerAnim(float timer, bool anim) { yield return new WaitForSeconds(timer); EndAnim(anim); } public void StartAnim(bool anim) { make = true; if (anim == true) { animMake = true; isMoved = true; } else { rotationActive = rotation; } } public void EndAnim(bool anim) { if (anim == true) { animMake = true; isMoved = false; } else { make = false; rotationActive = 0f; } } private void FixedUpdate() { if (animMake == true) { if (isMoved == true) { if (rotationActive != rotation) { rotationActive = Mathf.MoveTowards(rotationActive, rotation, speedSpeed); } else { animMake = false; isMoved = false; } } else { if (rotationActive != 0f) { rotationActive = Mathf.MoveTowards(rotationActive, 0f, speedSpeed); } else { animMake = false; isMoved = true; } } } } private void Update() { if (make == true) { float rot = tr.localEulerAngles.z; float s = Time.fixedDeltaTime / 0.03f * (Time.deltaTime / 0.03f); tr.localEulerAngles = new Vector3(0f, 0f, rot + rotationActive * s); } } }
      
      





Script PointsAnimation
 using UnityEngine; using System.Collections; public class PointsAnimation : GlobalFunctions { public AnimationType animationType = AnimationType.Infinity; public float speedSpeedPosition = 0.001f; public float speedPosition = 0.1f; public Vector3[] pointsPosition = new Vector3[0]; public int counterPosition = 0; private float speedPositionActive = 0f; private int pointsPositionLength = 0; private bool make = true; private bool animMake = false; private bool isMoved = false; private Transform tr; public void SetPos(bool pos, float m) { speedPositionActive = speedPosition * (pos ? 1 : m); } private void Awake() { pointsPositionLength = pointsPosition.Length; tr = transform; switch (animationType) { case AnimationType.Infinity: make = true; isMoved = true; speedPositionActive = speedPosition; break; case AnimationType.Start: make = false; isMoved = false; break; case AnimationType.End: make = true; isMoved = true; speedPositionActive = speedPosition; break; case AnimationType.All: make = false; isMoved = false; break; } } public void TimerAnim(float timer, bool anim) { StartAnim(anim); StartCoroutine(TimerTimerAnim(timer, anim)); } private IEnumerator TimerTimerAnim(float timer, bool anim) { yield return new WaitForSeconds(timer); EndAnim(anim); } public void StartAnim(bool anim) { make = true; if (anim == true) { animMake = true; isMoved = true; } else { speedPositionActive = speedPosition; } } public void EndAnim(bool anim) { if (anim == true) { animMake = true; isMoved = false; } else { make = false; speedPositionActive = 0f; } } private void FixedUpdate() { if (animMake == true) { if (isMoved == true) { if (speedPositionActive != speedPosition) { Vector2 ends = new Vector2(-speedPosition, speedPosition); speedPositionActive = Mathf.MoveTowards(speedPositionActive, speedPosition, speedSpeedPosition); } else { animMake = false; isMoved = false; } } else { if (speedPositionActive != 0f) { Vector2 ends = new Vector2(-speedPosition, speedPosition); speedPositionActive = Mathf.MoveTowards(speedPositionActive, 0f, speedSpeedPosition); } else { animMake = false; isMoved = true; } } } } private void Update() { if (make) { if (tr.localPosition == pointsPosition[counterPosition]) { counterPosition++; if (counterPosition == pointsPositionLength) { counterPosition = 0; } } else { float s = Time.fixedDeltaTime / 0.03f * (Time.deltaTime / 0.03f); tr.localPosition = Vector3.MoveTowards(tr.localPosition, pointsPosition[counterPosition], speedPositionActive * s); } } } }
      
      







UI



Compared to the original, this is a real masterpiece.



For comparison, here is the original:









Here is the sequel:









Here is the original:









Here is the sequel:









Here is the original ... I think it’s clear. Minimalism in the sequel I brought to mind, and instead of the inappropriately colored pause button and frankly interfering timer, there is now a lokanic, somehow noticeable pause button in the lower left corner. The sequel still wins the menu. Unlike the original, there are animations everywhere, and the background is 11 shaders I accidentally wrote in the Shader Graph. The functionality is also getting better, there are graphics settings, separate settings for sound and music, a console that allows you to change the save - there is nothing of this in the original menu.



It turned out so good because I decided to look at other games. Here and there, in general, I took (rather stole) the best from everywhere. And here is what I took special:



  1. Play menu

    Taken from Alto's Adventure, only experiences turned into ridicule, jokes, ironic comments, etc.
  2. Pause

    Also from Alto, just not as functional, but it fits in the style and play more convenient.
  3. Settings

    Partially taken from Vector 2, namely the shape of the menu and volume sliders.

    He took a little in general, but otherwise did everything on his own.


Console



First, make a reservation about how conservation works. There are two variables responsible for global and local conservation: these are the numbers progress and elevatorsave, respectively. The progress variable is responsible for saving between scenes, and the elevatorsave variable is responsible for saving inside the scene. When you press the "Start" or "Re-start" button, the game transfers progress to the scene and spawns the player on saving under the elevatorsave number.



The console allows you to change or create any variable. Such a simple and powerful tool was very useful to me for testing the game and identifying bugs in it. The console itself is a hand-written command that mimics other consoles.



Script DebugConsole
 using UnityEngine; using UnityEngine.UI; using UnityEngine.SceneManagement; using System.Collections; public class DebugConsole : MonoBehaviour { public Animator animatorBlackScreen; public Language l; public InputField inputField; public Text textDebug; private bool access = false; public void AnalyzeText() { string txt = inputField.text.ToLower(); string[] output = new string[0]; string txtLoc = ""; for (int i = 0; i < txt.Length; i++) { if (txt[i] == ' ') { if (txtLoc != "") { output = Add(output, txtLoc); txtLoc = ""; } } else { txtLoc = txtLoc + txt[i]; } } if (txtLoc != "") { output = Add(output, txtLoc); txtLoc = ""; } Analyze(output); } public void Analyze(string[] commands) { switch (commands[0]) { case "playerprefs": if (access == true) { if (commands.Length < 2) { Log(l.ConsoleLanguage(1));//1 } else { switch (commands[1]) { case "f": case "float": float f = 0f; if (float.TryParse(commands[3], out f)) { PlayerPrefs.SetFloat(commands[2], float.Parse(commands[3])); Log(l.ConsoleLanguage(2, commands[2]));//2 } else { Log(l.ConsoleLanguage(3));//3 } break; case "i": case "int": int i = 0; if (int.TryParse(commands[3], out i)) { PlayerPrefs.SetInt(commands[2], int.Parse(commands[3])); Log(l.ConsoleLanguage(4, commands[2]));//4 } else { Log(l.ConsoleLanguage(5));//5 } break; case "s": case "string": PlayerPrefs.SetString(commands[2], commands[3]); Log(l.ConsoleLanguage(6, commands[2]));//6 break; case "clear": PlayerPrefs.DeleteAll(); SceneManager.LoadScene(0); break; default: Log(l.ConsoleLanguage(7, commands[1]));//7 break; } } } else { Log(l.ConsoleLanguage(8));//8 } break; case "next": if (access == true) { if (commands.Length > 1) { switch (commands[1]) { case "level": int p = PlayerPrefs.GetInt("progress"); PlayerPrefs.SetInt("progress", p + 1); Log("ok level"); break; case "save": int s = PlayerPrefs.GetInt("elevatorsave"); PlayerPrefs.SetInt("elevatorsave", s + 1); Log("ok save"); break; case "start": PlayerPrefs.SetInt("elevatorsave", 0); Log("ok start"); break; case "end": PlayerPrefs.SetInt("elevatorsave", 1); Log("ok end"); break; } } } else { Log(l.ConsoleLanguage(8));//8 } break; case "echo": if (commands.Length == 1) { Log(l.ConsoleLanguage(9));//9 } else { switch (commands[1]) { case "vertogpro"://echo vertogpro access = true; Log(l.ConsoleLanguage(10));//10 break; default: Log(l.ConsoleLanguage(11));//11 break; } } break; case "restart": if (access == true) { SceneManager.LoadScene(0); } else { Log(l.ConsoleLanguage(12));//12 } break; case "authors": Log(l.ConsoleLanguage(13));//13 break; case "discharge": animatorBlackScreen.SetBool("isActive", true); PlayerPrefs.SetString("start", "key"); PlayerPrefs.SetString("language", "nothing"); PlayerPrefs.SetString("graphicsquality", "medium"); PlayerPrefs.SetFloat("sound", 0.5f); PlayerPrefs.SetFloat("music", 0.5f); PlayerPrefs.SetFloat("rotatenextlevel", 0f); PlayerPrefs.SetInt("elevatorsave", 0); PlayerPrefs.SetInt("progress", 1); PlayerPrefs.SetInt("deaths", 0); PlayerPrefs.SetInt("discharge", PlayerPrefs.GetInt("discharge") + 1); PlayerPrefs.SetInt("lastmenueffect", -1); PlayerPrefs.SetString("isshotmode", "false"); PlayerPrefs.SetString("boss1", "life"); PlayerPrefs.SetString("boss2", "life"); PlayerPrefs.SetString("ai", "off"); PlayerPrefs.SetString("boss3", "life"); PlayerPrefs.SetString("end", "none"); StartCoroutine(StartGame()); break; case "clear": Clear(); break; case "info": if (access == false) { Log(l.ConsoleLanguage(14));//14 } else { Log(l.ConsoleLanguage(15));//15 } break; default: Log(l.ConsoleLanguage(16, commands[0]));//16 break; } } public void Log(object message) { textDebug.text = message.ToString(); } public void Clear() { inputField.text = ""; textDebug.text = ""; } public string[] Add(string[] old, string addComponent) { string[] n = new string[old.Length + 1]; if (old.Length != 0) { for (int i = 0; i < old.Length; i++) { n[i] = old[i]; } } n[old.Length] = addComponent; return n; } public IEnumerator StartGame() { yield return new WaitForSeconds(1f); SceneManager.LoadSceneAsync(0); } }
      
      







And especially for you, I will leave a list of liquid teams in it:



  1. discharge - resets game progress (and all other information too)
  2. echo vertogpro - a team for providing access to development teams
  3. playerprefs [type given (string, int, float)] [variable name] [data] - changes or creates any variable. Example: playerprefs int progress 14
  4. next - a subtype for simplified level navigation, with its own commands:

    • start - saves at the beginning of the level (next start)
    • end - saves at the end of the level (next end)
    • save - teleports to the next save (next save)
    • level - teleports to the next level (next level)


Graphic arts



For a year I did not learn how to draw, so I did almost the same as in the original: I downloaded about 30 texture packs for Maycraft, selected the best from each and so the main graphics turned out. The graphics didn’t differ much from the original, and it infuriated me, infuriated me so much that I still found different animated effects (explosions, fire, etc.) and pumped up various texture packs from the asset store. Even for a mobile game, the graphics are pretty bad, although progress is still being observed. Here is the original:









And here is the sequel:









Saving



If the principle of saving is simple, then their implementation is not very. The save system consists of 3 scripts:



  1. ElevatorBase is the foundation in which startup teams occur. In it, by the elevatorsave variable, the active save is selected from the save array.



    Script ElevatorBase
     using UnityEngine; using System.Collections; public class ElevatorBase : MonoBehaviour { public GameObject[] savers = new GameObject[0]; public float inputStartBlock = 1f; private GameUI gameUI; public void Awake() { int l = savers.Length; if (l != 0) { for (int i = 0; i < l; i++) { if (savers[i] != null) { if (savers[i].GetComponent<Saving>()) { Saving saving = savers[i].GetComponent<Saving>(); saving.isFirst = false; saving.idElevatorBase = i; } else if (savers[i].GetComponent<Elevator>()) { savers[i].GetComponent<Elevator>().isFirst = false; } } } int es = PlayerPrefs.GetInt("elevatorsave"); if (savers[es] != null) { if (savers[es].GetComponent<Saving>()) { savers[es].GetComponent<Saving>().isFirst = true; } else if (savers[es].GetComponent<Elevator>()) { savers[es].GetComponent<Elevator>().isFirst = true; } } else { gameUI = GameObject.FindWithTag("Canvas").GetComponent<GameUI>(); StartCoroutine(BlockEnabled()); GameObject.Find("TipsInput").GetComponent<TipsGamePlayInput>().active = true; } } else { gameUI = GameObject.FindWithTag("Canvas").GetComponent<GameUI>(); gameUI.ChangeisBlocked(); } } public IEnumerator BlockEnabled() { yield return new WaitForSeconds(inputStartBlock); GameObject block = gameUI.block.gameObject; block.SetActive(false); } }
          
          





  2. Saving — , , , elevatorsave id.



    Saving
     using System.Collections; using UnityEngine; public class Saving : MonoBehaviour { public Saving[] savings; public Vector2 startPos; public float startRot; public bool isActive = true; public bool isFirst = true; public int idElevatorBase = 0; public TipsGamePlayInput tgpi; private GameObject player; private GameObject cam; private Transform trp; private GameUI gameui; private Management m; private Saving self; private void Start() { self = GetComponent<Saving>(); cam = GameObject.FindWithTag("MainCamera"); m = cam.GetComponent<Management>(); gameui = GameObject.FindWithTag("Canvas").GetComponent<GameUI>(); player = m.player; trp = player.GetComponent<Transform>(); if (isFirst) { trp.position = startPos; m.Set(startRot); OfferSaves(); } isActive = !isFirst; tgpi.SetActive(!isFirst); StartCoroutine(BlockFalse()); } public IEnumerator BlockFalse() { yield return new WaitForSeconds(1f); gameui.block.gameObject.SetActive(false); } private void OnTriggerEnter2D(Collider2D collision) { if (collision.CompareTag("Player") && isActive == true) { isActive = false; PlayerPrefs.SetInt("elevatorsave", idElevatorBase); OfferSaves(); } } public void OfferSaves() { if (savings.Length != 0) { for (int i = 0; i < savings.Length; i++) { savings[i].isActive = false; savings[i].tgpi.SetActive(false); } } } }
          
          





  3. Elevator — , . : ( ).



    Elevator
     using System.Collections; using UnityEngine; public class Elevator : GlobalFunctions { public Vector2 endPos; public Vector2 startPos; public int nextScene = 1; public int nextElevatorSave = 0; public float speed = 0.1f; public bool isFirst = true; public bool isActive = true; public bool isReverse = false; public bool isMake = false; private GameObject player; private Rigidbody2D rb; private Transform tr; private Transform trp; private GameUI gameui; private AudioBase audioBase; private Transform cam; private void Start() { audioBase = GameObject.FindWithTag("MainCamera").GetComponent<AudioBase>(); gameui = GameObject.FindWithTag("Canvas").GetComponent<GameUI>(); player = gameui.m.player; rb = player.GetComponent<Rigidbody2D>(); trp = player.GetComponent<Transform>(); tr = GetComponent<Transform>(); cam = gameui.m.transform; startPos = tr.position; if (isFirst) { trp.position = startPos; rb.velocity = new Vector2(); rb.gravityScale = 0f; gameui.m.Set(); } else { tr.position = endPos; isMake = true; } isActive = isFirst; isReverse = false; } private void OnTriggerEnter2D(Collider2D collision) { if (collision.CompareTag("Player") && isMake == true) { isReverse = true; isActive = true; rb.velocity = new Vector2(); rb.gravityScale = 0f; gameui.block.gameObject.SetActive(true); PlayerPrefs.SetInt("elevatorsave", nextElevatorSave); gameui.animatorBlackScreenGame.SetBool("isActive", true); audioBase.LowerSound(0.05f, 16, 0, TypePlaying.Music); StartCoroutine(NumSaveRotate()); StartCoroutine(gameui.StartGame(1.5f, nextScene)); } } private IEnumerator NumSaveRotate() { yield return new WaitForSeconds(1.5f); PlayerPrefs.SetFloat("rotatenextlevel", Stable(cam.localEulerAngles.z, -180f, 180f)); } private void FixedUpdate() { if (isActive == true) { float s = Time.fixedDeltaTime / 0.03f; if (isReverse == false) { rb.velocity = new Vector2(); tr.position = Vector2.MoveTowards(tr.position, endPos, speed * s); trp.position = tr.position; if ((Vector2)tr.position == endPos) { isMake = true; isActive = false; rb.gravityScale = 1f; gameui.block.gameObject.SetActive(false); } } else if (isReverse == true) { tr.position = Vector2.MoveTowards(tr.position, startPos, speed * s); trp.position = tr.position; if (tr.position == (Vector3)startPos) { isActive = false; rb.gravityScale = 1f; } } } } }
          
          





Game design



It was a real mess. It was the game design that stretched the development cycle from 4 to 6 months. In total, the game has 34 levels: 30 regular, 3 bosses and 1 final (level). Each ordinary I did 2-3 days, each boss 2 weeks and the final level did a week. To balance it all, I built them like this: 10 levels => 1 boss => 10 levels => 2 boss => 10 levels => 3 boss => final level.



The local levels are my pride. They are unusual, varied and even a little interesting. The levels are designed in a specific form to create a sense of the open world. For this, I even drew a map:









The map is not the best drawing and information, but it gave important information for the necessary forms of levels. Initially, the plans were to make all the levels on the map, but I did not do those that were darkened. By the way, this is a map with a size of 1000x1000 pixels, and it was from this map that the scale came out: 1 block = 1 pixel = player size.



Between levels, the player goes through the elevator. It can deliver to any level, and therefore it is possible to travel between levels, creating a player with an even greater sense of the openness of the world. And also, in some places triggers are hidden to activate secret elevators, which can carry 10-15 levels forward.



For ordinary levels, there was a construction algorithm:



  1. A background that would have a shape and scale as on a map





  2. Exterior walls (triple thickness due to special physics)





  3. Walls are internal





  4. Levels themselves





  5. Elevators, Saves and Audio Triggers







It’s more complicated with bosses, because each boss presented at the same time different and similar behavior patterns. All bosses have 100 health and each level has something to destroy. It is better to talk about each separately:



1 boss is very simple in behavior: randomly moves around the room, waits 5 seconds and repeats. To be honest, this is a bad example of a boss: simple, lagging and not memorable. And he can be killed only by beating about him. But there is a defense in the form of 4 saws: 3 of them smartly move randomly around the room and one protects the boss when he moves. After death, it explodes.



Script BossManagement1
 using UnityEngine; using System.Collections; public class BossManagement1 : GlobalFunctions { public float hp = 100f; public float speed = 0.2f; public bool startActivated = false; public bool activated = false; public bool activatedSaw = false; public bool activatedAngle = false; public bool activatedCoroutine = true; private bool active; private float maxhp; public Vector2 target; public Vector2 targetSaw1; public Vector2 targetSaw2; public Vector2 minBorder; public Vector2 maxBorder; public DeadBoss1 deadBoss; public GameObject backGround; public GameObject healthBar; public Transform tr; public Transform sawMain; public Transform saw1; public Transform saw2; public Arrow arrow; public AudioSet setStart; public AudioSet setEnd; public Transform player; public Power playerPower; private Transform bg, hb; private float targethp = 0f; private Vector2 startMove = new Vector2(-20f, 0f); public void Awake() { maxhp = hp; bg = backGround.transform; hb = healthBar.transform; } public void Start() { if (PlayerPrefs.GetString("boss1") == "death") { Dead(false); } } public void FixedUpdate() { if (startActivated && !activatedCoroutine) { if ((Vector2)tr.position != startMove) { tr.position = Vector2.MoveTowards(tr.position, startMove, speed); saw1.position = Vector2.MoveTowards(saw1.position, startMove, speed); saw2.position = Vector2.MoveTowards(saw2.position, startMove, speed); } else { activatedCoroutine = true; startActivated = false; StartCoroutine(ActivatedOn()); } } if (activated) { if ((Vector2)tr.position != target) { tr.position = Vector2.MoveTowards(tr.position, target, speed); } else { activated = false; sawMain.localScale = new Vector2(0f, 0f); StartCoroutine(TargetRotate()); } } if (activatedSaw) { if ((Vector2)saw1.position != targetSaw1) { saw1.position = Vector2.MoveTowards(saw1.position, targetSaw1, speed); } else { float x = Random.Range(minBorder.x, maxBorder.x); float y = Random.Range(minBorder.y, maxBorder.y); targetSaw1 = new Vector2(x, y); } if ((Vector2)saw2.position != targetSaw2) { saw2.position = Vector2.MoveTowards(saw2.position, targetSaw2, speed); } else { float x = Random.Range(minBorder.x, maxBorder.x); float y = Random.Range(minBorder.y, maxBorder.y); targetSaw2 = new Vector2(x, y); } } if (activatedAngle) { Vector2 dir = player.position - tr.position; float angle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg; tr.localEulerAngles = new Vector3(0f, 0f, Mathf.LerpAngle(tr.localEulerAngles.z, angle, 0.1f)); } } public IEnumerator TargetRotate() { yield return new WaitForSeconds(3f + 3f * hp / maxhp); sawMain.localScale = new Vector2(6f, 6f); float x = Random.Range(minBorder.x, maxBorder.x); float y = Random.Range(minBorder.y, maxBorder.y); target = new Vector2(x, y); activated = true; } public IEnumerator ActivatedOn() { yield return new WaitForSeconds(3f); sawMain.localScale = new Vector2(6f, 6f); target = new Vector2(Random.Range(minBorder.x, maxBorder.x), Random.Range(minBorder.y, maxBorder.y)); targetSaw1 = new Vector2(Random.Range(minBorder.x, maxBorder.x), Random.Range(minBorder.y, maxBorder.y)); targetSaw2 = new Vector2(Random.Range(minBorder.x, maxBorder.x), Random.Range(minBorder.y, maxBorder.y)); activatedSaw = true; activated = true; arrow.isActive = true; } public IEnumerator ActivatedCoroutineOff() { yield return new WaitForSeconds(1f); activatedCoroutine = false; activatedAngle = true; } public void Update() { if (active == true) { if (hp != targethp) { float s = Time.fixedDeltaTime / 0.03f * (Time.deltaTime / 0.03f); hp = MoveToward(hp, targethp, speed * s, new Vector2(-0f, maxhp)); } else { active = false; if (targethp == 0f) { Dead(true); } } } UpdateHP(); } public void UpdateHP() { float h = hp / maxhp; bg.localScale = new Vector3(5f, 0.9f, 1f); hb.localScale = new Vector3(4.8f * h, 0.7f, 1f); hb.localPosition = new Vector3(-2.4f + 4.8f * h / 2f, 0f, 0f); } private bool oneTimeMusic = true; public void Damage(float damage) { if (oneTimeMusic == true) { oneTimeMusic = false; deadBoss.StartBoss(); deadBoss.Boom(); setStart.SetMusic(); startActivated = true; StartCoroutine(ActivatedCoroutineOff()); } if (hp != 0f) { targethp = Stable2(hp - damage, 0f, maxhp); speed = speed + damage * 0.02f; active = true; } } public void Dead(bool boom) { active = false; activated = false; activatedSaw = false; startActivated = false; activatedAngle = false; activatedCoroutine = false; backGround.SetActive(false); healthBar.SetActive(false); sawMain.gameObject.SetActive(false); saw1.gameObject.SetActive(false); saw2.gameObject.SetActive(false); setEnd.SetMusic(); arrow.obj.SetActive(false); PlayerPrefs.SetString("boss1", "death"); deadBoss.Dead(tr.position, boom); } public void OnCollisionEnter2D(Collision2D collision) { if (collision.transform.CompareTag("Player")) { Damage(playerPower.power); } } }
      
      







2 boss is already better in quality, but still far from ideal. His pattern is more complicated: he determines the location of the player and after the area in which he is. After the boss selects a random point in the area and moves to it. His defense is already more meaningful: the boss’s health has stages and different weapons for each stage:



  1. 2 saws in the distance
  2. 2 saws at a distance, when protected by a saw
  3. 2 laser-limited, protected by a saw during movement
  4. 2 lasers, when protected by a saw
  5. 2 lasers, when protected by a saw and 2 saws at a distance


Also, from the graphic part, the second boss is better than the first: inactivity time in the form of recovering stamines and third-party activity in the form of boss laser deactivation, if triggers are activated in the center of the room.



Script BossManagement2
 using System.Collections; using UnityEngine; public class BossManagement2 : GlobalFunctions { public float hp = 100f; public float speed = 0.5f; public float speedRotate = 0.5f; public int stage = 1; public bool isAlive = true; public bool isActivated = false; public bool isMove = false; public bool isWorkingLaser = true; private float timeStamina = 0f; private float timeRetarget = 0f; public Vector2 region = Vector2.zero; public Vector3 target = Vector3.zero; public GameObject player; public Transform saw; public Transform laser1; public Transform laser2; public Laser laserL1; public Laser laserL2; public Transform laserOffset1; public Transform laserOffset2; public Explosion explosion; public GameObject explosionAsset; public CircleCollider2D trigStart; public BoxCollider2D laserDetected1; public BoxCollider2D laserDetected2; public GameObject saw1; public GameObject saw2; public Transform health; public Transform stamina; public SpriteRenderer srStamina; private Transform pl; private Transform tr; public Transform state; public Laser state1; public Laser state2; public Laser state3; public Laser state4; private Coroutine coroutineStamina; public SpriteRenderer bossBase; public SpriteRenderer laserD1; public SpriteRenderer laserD2; public Gate gateStart; public Gate gateEnd; public GameObject blockWin; public GameObject physicsIn; public GameObject stateLasers; public GameObject expStart; public AudioSet setStart; public AudioClip setEnd; public AudioBase audioBase; public void Awake() { bool isDeath = PlayerPrefs.GetString("boss2") == "death"; blockWin.SetActive(false); if (isDeath) { isAlive = false; gateStart.isReverse = true; gateEnd.isReverse = true; physicsIn.SetActive(false); stateLasers.SetActive(false); expStart.SetActive(false); gameObject.SetActive(false); } else { tr = transform; pl = player.transform; timeStamina = 5.4f / speedRotate / 100f; timeRetarget = 5.4f / speedRotate; saw.localScale = Vector3.zero; stamina.localScale = Vector3.zero; srStamina.color = new Color(0f, 0.5f, 1f, 0f); saw1.SetActive(false); saw2.SetActive(false); LaserDisable(); LaserBlockEnable(); } } public void Update() { if (isAlive) { if (isActivated == true) { switch (stage) { case 1: if (isMove == true) { if (tr.position == target) { isMove = false; RotatePlayer(); saw1.SetActive(true); saw2.SetActive(true); stamina.localScale = Vector3.zero; srStamina.color = new Color(0f, 0.5f, 1f, 1f); if (coroutineStamina != null) { StopCoroutine(coroutineStamina); } coroutineStamina = StartCoroutine(StaminaAnim(timeStamina, 100)); StartCoroutine(Retarget1()); } else { tr.position = Vector2.MoveTowards(tr.position, target, speed); } } break; case 2: if (isMove == true) { if (tr.position == target) { isMove = false; RotatePlayer(); saw.localScale = Vector3.zero; saw1.SetActive(true); saw2.SetActive(true); stamina.localScale = Vector3.zero; srStamina.color = new Color(0f, 0.5f, 1f, 1f); if (coroutineStamina != null) { StopCoroutine(coroutineStamina); } coroutineStamina = StartCoroutine(StaminaAnim(timeStamina, 100)); StartCoroutine(Retarget2()); } else { tr.position = Vector2.MoveTowards(tr.position, target, speed); } } break; case 3: if (isMove == true) { if (tr.position == target) { isMove = false; RotatePlayer(); saw.localScale = Vector3.zero; LaserEnable(); stamina.localScale = Vector3.zero; srStamina.color = new Color(0f, 0.5f, 1f, 1f); if (coroutineStamina != null) { StopCoroutine(coroutineStamina); } coroutineStamina = StartCoroutine(StaminaAnim(timeStamina, 100)); StartCoroutine(Retarget3()); } else { tr.position = Vector2.MoveTowards(tr.position, target, speed); } } break; case 4: if (isMove == true) { if (tr.position == target) { isMove = false; RotatePlayer(); saw.localScale = Vector3.zero; LaserEnable(); stamina.localScale = Vector3.zero; srStamina.color = new Color(0f, 0.5f, 1f, 1f); if (coroutineStamina != null) { StopCoroutine(coroutineStamina); } coroutineStamina = StartCoroutine(StaminaAnim(timeStamina, 100)); StartCoroutine(Retarget4()); } else { tr.position = Vector2.MoveTowards(tr.position, target, speed); } } break; case 5: if (isMove == true) { if (tr.position == target) { isMove = false; RotatePlayer(); saw.localScale = Vector3.zero; LaserEnable(); saw1.SetActive(false); saw2.SetActive(false); stamina.localScale = Vector3.zero; srStamina.color = new Color(0f, 0.5f, 1f, 1f); if (coroutineStamina != null) { StopCoroutine(coroutineStamina); } coroutineStamina = StartCoroutine(StaminaAnim(timeStamina, 100)); StartCoroutine(Retarget5()); } else { tr.position = Vector2.MoveTowards(tr.position, target, speed); } } break; } } else { if (trigStart.enabled == false) { isActivated = true; float musicValue = PlayerPrefs.GetFloat("music"); audioBase.UpSound(0.01f, 5, 0, TypePlaying.Music); explosion.health = 0f; explosion.StartCoroutineTimerOffsetExplosion(); RegionDetected(); LaserDisable(); target = Target(); } } } } public void FixedUpdate() { if (!isMove && isActivated) { laserOffset1.localEulerAngles = new Vector3(0f, 0f, laserOffset1.localEulerAngles.z + speedRotate); laserOffset2.localEulerAngles = new Vector3(0f, 0f, laserOffset2.localEulerAngles.z + speedRotate); if (isWorkingLaser) { state.localEulerAngles = new Vector3(0f, 0f, state.localEulerAngles.z + speedRotate); } } } public void RotatePlayer() { Vector2 p = pl.position; float angle = Mathf.Atan2(py, px) * Mathf.Rad2Deg; laserOffset1.localEulerAngles = new Vector3(0f, 0f, angle); laserOffset2.localEulerAngles = new Vector3(0f, 0f, angle - 180f); } private Vector3[] posLasers = new Vector3[] { Vector3.zero, Vector3.zero}; public void TriggerLaserDefect(int id) { switch (id) { case 1: state1.active = false; state1.lr1.SetPositions(posLasers); break; case 2: state2.active = false; state2.lr1.SetPositions(posLasers); break; case 3: state3.active = false; state3.lr1.SetPositions(posLasers); break; case 4: state4.active = false; state4.lr1.SetPositions(posLasers); break; } if (!state1.active && !state2.active && !state3.active && !state4.active) { isWorkingLaser = false; state1.active = false; state2.active = false; state3.active = false; state4.active = false; laserL1.active = false; laserL2.active = false; laser1.localPosition = Vector2.zero; laser2.localPosition = Vector2.zero; } } public void OnCollisionEnter2D(Collision2D collision) { if (collision.transform.tag == "Player") { hp = hp - pl.GetComponent<Power>().power; health.localScale = new Vector2(hp / 50f, hp / 50f); stage = 5 - (int)(hp / 25f); if (stage == 4) { LaserBlockDisable(); } if (hp <= 0f && isAlive == true) { audioBase.LowerSound(0.1f, 50, 0, TypePlaying.Music); audioBase.SetSound(setEnd, 0, 0.8f, TypePlaying.Music, true, 1f); GameObject deadInside = Instantiate(explosionAsset, pl.position, Quaternion.identity); deadInside.GetComponent<Rigidbody2D>().isKinematic = true; deadInside.transform.localScale = new Vector2(2f, 2f); Explosion exp = deadInside.GetComponent<Explosion>(); exp.radius = 2f; exp.health = 0f; exp.timeOffsetExplosion = 3f; exp.StartCoroutineTimerOffsetExplosion(); gateStart.OnTriggerEnter2D(player.GetComponent<Collider2D>()); gateEnd.OnTriggerEnter2D(player.GetComponent<Collider2D>()); PlayerPrefs.SetString("boss2", "death"); blockWin.SetActive(false); gameObject.SetActive(false); } } } public void OnTriggerEnter2D(Collider2D collision) { if (collision.tag == "Player") { blockWin.SetActive(true); trigStart.enabled = false; } } public void LaserEnable() { if (isWorkingLaser) { laserL1.active = true; laserL2.active = true; state1.active = false; state2.active = false; state3.active = false; state4.active = false; } laser1.localPosition = new Vector2(0f, -1f); laser2.localPosition = new Vector2(0f, -1f); return; } public void LaserDisable() { if (isWorkingLaser) { state1.active = true; state2.active = true; state3.active = true; state4.active = true; laserL1.active = false; laserL2.active = false; } laser1.localPosition = Vector2.zero; laser2.localPosition = Vector2.zero; return; } public void LaserBlockEnable() { laserDetected1.enabled = true; laserDetected2.enabled = true; } public void LaserBlockDisable() { laserDetected1.enabled = false; laserDetected2.enabled = false; } public void RegionDetected() { Vector2 result = Vector2.zero; Vector2 pos = pl.position; if (pos.x > -45f & pos.x <= -30f) { result.x = 1; } else if (pos.x > -30f & pos.x < -5f) { result.x = 2; } else if (pos.x >= -5f & pos.x <= 5f) { result.x = 3; } else if (pos.x > 5f & pos.x <= 30f) { result.x = 4; } else if (pos.x >= 30f & pos.x < 45f) { result.x = 5; } if (pos.y > -45f & pos.y <= -30f) { result.y = 1; } else if (pos.y > -30f & pos.y < -5f) { result.y = 2; } else if (pos.y >= -5f & pos.y <= 5f) { result.y = 3; } else if (pos.y > 5f & pos.y <= 30f) { result.y = 4; } else if (pos.y >= 30f & pos.y < 45f) { result.y = 5; } region = result; return; } private readonly Vector2[] aroundCloser = new Vector2[] { new Vector2(2, 2), new Vector2(2, 3), new Vector2(2, 4), new Vector2(3, 2), new Vector2(3, 4), new Vector2(4, 2), new Vector2(4, 3), new Vector2(4, 4) }; public Vector2 Target() { Vector2 result = Vector2.zero; if (region == new Vector2(3, 3)) { region = aroundCloser[Random.Range(0, 8)]; } switch (region.x) { case 1: result.x = Random.Range(-45f, -32f); break; case 2: result.x = Random.Range(-29f, -5f); break; case 3: result.x = Random.Range(-5f, 5f); break; case 4: result.x = Random.Range(5f, 29f); break; case 5: result.x = Random.Range(32f, 45f); break; } switch (region.y) { case 1: result.y = Random.Range(-45f, -32f); break; case 2: result.y = Random.Range(-29f, -5f); break; case 3: result.y = Random.Range(-5f, 5f); break; case 4: result.y = Random.Range(5f, 29f); break; case 5: result.y = Random.Range(32f, 45f); break; } isMove = true; return result; } public IEnumerator StaminaAnim(float time, int count) { yield return new WaitForSeconds(time); float sc = hp * (100f - count) / 5000f; stamina.localScale = new Vector2(sc, sc); if (count > 1) { count = count - 1; coroutineStamina = StartCoroutine(StaminaAnim(time, count)); } } public IEnumerator Retarget1() { yield return new WaitForSeconds(timeRetarget); srStamina.color = new Color(0f, 0.5f, 1f, 0f); RotatePlayer(); saw1.SetActive(false); saw2.SetActive(false); RegionDetected(); target = Target(); } public IEnumerator Retarget2() { yield return new WaitForSeconds(timeRetarget); srStamina.color = new Color(0f, 0.5f, 1f, 0f); RotatePlayer(); saw.localScale = new Vector2(2f, 2f); saw1.SetActive(false); saw2.SetActive(false); RegionDetected(); target = Target(); } public IEnumerator Retarget3() { yield return new WaitForSeconds(timeRetarget); srStamina.color = new Color(0f, 0.5f, 1f, 0f); RotatePlayer(); saw.localScale = new Vector2(2f, 2f); LaserDisable(); RegionDetected(); target = Target(); } public IEnumerator Retarget4() { yield return new WaitForSeconds(timeRetarget); srStamina.color = new Color(0f, 0.5f, 1f, 0f); RotatePlayer(); saw.localScale = new Vector2(2f, 2f); LaserDisable(); RegionDetected(); target = Target(); } public IEnumerator Retarget5() { yield return new WaitForSeconds(timeRetarget); srStamina.color = new Color(0f, 0.5f, 1f, 0f); RotatePlayer(); saw.localScale = new Vector2(2f, 2f); saw1.SetActive(true); saw2.SetActive(true); LaserDisable(); RegionDetected(); target = Target(); } }
      
      







3 boss is the best in quality among bosses! He uses raycasts to move around. First, it randomly rotates to any angle, then among 12 raycasts launched in different directions, it selects the longest one and flies to the point of raycast. There are objects on the level, some of which are also being destroyed. And how do boss raycasts react to objects? Triggers were added to static objects, which are 2 times larger than the objects themselves, so that the raycast had a point where the boss would not fly in the air, would not be in the wall, but would be riveted to the wall. The boss has a special defense: at the beginning of the level with the boss (each boss is a separate large level without third-party puzzles) there are triggers, and they are set so that only one is activated.The boss has 5 trap blanks and each trigger leaves only 3-4 traps active. And he also had an improved system of areas, which consisted of predefined areas for each area (in which the player can be) and for each trap. And during the flight, the boss always kills the player.



Trap List:



  1. The laser in the center, which after each time the boss starts flying, begins to look at the player.
  2. 2 lasers that use the Lerp function to move to specified areas (depending on the location of the player) and are sent to the player before the movement (they should always be in front of the player, but something went wrong).
  3. A saw that always goes to the same area as the player.
  4. 2 saws, which are always directed to the left and right areas from the area where the player is.
  5. 4 trap balls moving symmetrically to the center


Script BossManagement3
 using System.Collections; using UnityEngine; using UnityEngine.SceneManagement; public class BossManagement3 : MonoBehaviour { public float health = 100f; public Vector4[] boxs = new Vector4[0]; public int[] saw1Fields = new int[0]; public int[] saw2Fields = new int[0]; public int[] saw3Fields = new int[0]; public int[] laser1Fields = new int[0]; public int[] laser2Fields = new int[0]; public Transform trBoss; public SpriteRenderer srBoss; public BossTracing3 bt; public Transform saw1; public Transform saw2; public Transform saw3; public Transform laser; public Transform laser1; public Transform laser2; public Transform trap1; public Transform trap2; public Transform trap3; public Transform trap4; public LineRenderer lr1; public LineRenderer lr2; public TrailRenderer trail; public GameObject exp; public GameObject terminal1; public GameObject terminal2; public GameObject LaserTarget; public GameObject LaserMover; public GameObject TrapsMover; public GameObject SawMover; public GameObject SawsAroundMover; public Explosion explosion; public SpriteRenderer sr; public CircleCollider2D cc; public Animator animatorEnd; public bool isMove = false; public bool isMoveSaw1 = false; public bool isMoveSaw2 = false; public bool isMoveSaw3 = false; public bool isMoveLaser1 = false; public bool isMoveLaser2 = false; public bool isMoveTraps = false; public int loadScene = 35; public int fieldPlayer = 0; private bool isActive = true; private float maxHealth; private Vector2 target = Vector2.zero; private Vector2 saw1target = Vector2.zero; private Vector2 saw2target = Vector2.zero; private Vector2 saw3target = Vector2.zero; private Vector2 laser1target = Vector2.zero; private Vector2 laser2target = Vector2.zero; private Vector2 traptarget1 = Vector2.zero; private Vector2 traptarget2 = Vector2.zero; private Vector2 traptarget3 = Vector2.zero; private Vector2 traptarget4 = Vector2.zero; private Vector2 border = new Vector2(47f, 44.5f); private Vector2 borderSaw = new Vector2(46f, 43.5f); private Management m; public GameObject p { get; private set; } private HealthBar hb; private Transform tr; private Power ppl; private int lengthBoxs = 0; private bool isLife = true; public void Awake() { isActive = !(PlayerPrefs.GetString("boss1") == "life" && PlayerPrefs.GetString("boss2") == "life"); terminal1.SetActive(!isActive); terminal2.SetActive(isActive); trail.enabled = PlayerPrefs.GetString("graphicsquality") != "low"; m = GameObject.FindWithTag("MainCamera").GetComponent<Management>(); lengthBoxs = boxs.Length; maxHealth = health; hb = m.healthBar; p = m.player; tr = p.transform; ppl = m.ppl; float c = health / maxHealth; srBoss.color = new Color(0f, 0f, c); } public void Start() { if (isActive == false) { return; } StartCoroutine(Mover()); fieldPlayer = bt.BoxPos(tr.position); if (fieldPlayer >= 0) { Vector4 r = boxs[saw1Fields[fieldPlayer]]; saw1target = new Vector2(Random.Range(rz, rx), Random.Range(rw, ry)); r = boxs[saw2Fields[fieldPlayer]]; saw2target = new Vector2(Random.Range(rz, rx), Random.Range(rw, ry)); r = boxs[saw3Fields[fieldPlayer]]; saw3target = new Vector2(Random.Range(rz, rx), Random.Range(rw, ry)); r = boxs[laser1Fields[fieldPlayer]]; laser1target = new Vector2(Random.Range(rz, rx), Random.Range(rw, ry)); r = boxs[laser2Fields[fieldPlayer]]; laser2target = new Vector2(Random.Range(rz, rx), Random.Range(rw, ry)); } else { Vector4 r = boxs[Random.Range(0, lengthBoxs)]; saw1target = new Vector2(Random.Range(rz, rx), Random.Range(rw, ry)); r = boxs[Random.Range(0, lengthBoxs)]; saw2target = new Vector2(Random.Range(rz, rx), Random.Range(rw, ry)); r = boxs[Random.Range(0, lengthBoxs)]; saw3target = new Vector2(Random.Range(rz, rx), Random.Range(rw, ry)); r = boxs[Random.Range(0, lengthBoxs)]; laser1target = new Vector2(Random.Range(rz, rx), Random.Range(rw, ry)); r = boxs[Random.Range(0, lengthBoxs)]; laser2target = new Vector2(Random.Range(rz, rx), Random.Range(rw, ry)); } TrapMover(); StartCoroutine(Laser1AIM()); StartCoroutine(Laser2AIM()); isMoveSaw1 = true; isMoveSaw2 = true; isMoveSaw3 = true; isMoveLaser1 = true; isMoveLaser2 = true; return; } public void SawMover1() { fieldPlayer = bt.BoxPos(tr.position); if (fieldPlayer >= 0) { Vector4 r = boxs[saw1Fields[fieldPlayer]]; saw1target = new Vector2(Random.Range(rz, rx), Random.Range(rw, ry)); } else { Vector4 r = boxs[Random.Range(0, lengthBoxs)]; saw1target = new Vector2(Random.Range(rz, rx), Random.Range(rw, ry)); } isMoveSaw1 = true; } public void SawMover2() { fieldPlayer = bt.BoxPos(tr.position); if (fieldPlayer >= 0) { Vector4 r = boxs[saw2Fields[fieldPlayer]]; saw2target = new Vector2(Random.Range(rz, rx), Random.Range(rw, ry)); } else { Vector4 r = boxs[Random.Range(0, lengthBoxs)]; saw2target = new Vector2(Random.Range(rz, rx), Random.Range(rw, ry)); } isMoveSaw2 = true; } public void SawMover3() { fieldPlayer = bt.BoxPos(tr.position); if (fieldPlayer >= 0) { Vector4 r = boxs[saw3Fields[fieldPlayer]]; saw3target = new Vector2(Random.Range(rz, rx), Random.Range(rw, ry)); } else { Vector4 r = boxs[Random.Range(0, lengthBoxs)]; saw3target = new Vector2(Random.Range(rz, rx), Random.Range(rw, ry)); } isMoveSaw3 = true; } public void LaserMover1() { fieldPlayer = bt.BoxPos(tr.position); if (fieldPlayer >= 0) { Vector4 r = boxs[laser1Fields[fieldPlayer]]; laser1target = new Vector2(Random.Range(rz, rx), Random.Range(rw, ry)); } else { Vector4 r = boxs[Random.Range(0, lengthBoxs)]; laser1target = new Vector2(Random.Range(rz, rx), Random.Range(rw, ry)); } StartCoroutine(Laser1AIM()); isMoveLaser1 = true; } public void LaserMover2() { fieldPlayer = bt.BoxPos(tr.position); if (fieldPlayer >= 0) { Vector4 r = boxs[laser2Fields[fieldPlayer]]; laser2target = new Vector2(Random.Range(rz, rx), Random.Range(rw, ry)); } else { Vector4 r = boxs[Random.Range(0, lengthBoxs)]; laser2target = new Vector2(Random.Range(rz, rx), Random.Range(rw, ry)); } StartCoroutine(Laser2AIM()); isMoveLaser2 = true; } public void TrapMover() { traptarget1 = new Vector2(Random.Range(-border.x, border.x), Random.Range(-border.y, border.y)); traptarget2 = new Vector2(-traptarget1.x, -traptarget1.y); traptarget3 = new Vector2(-traptarget1.x, traptarget1.y); traptarget4 = new Vector2(traptarget1.x, -traptarget1.y); isMoveTraps = true; } public IEnumerator Laser1AIM() { yield return new WaitForSeconds(0.5f); Vector2 diff = tr.position; float rot_z = Mathf.Atan2(diff.y, diff.x) * Mathf.Rad2Deg + 90f; laser1.rotation = Quaternion.Euler(0f, 0f, rot_z); } public IEnumerator Laser2AIM() { yield return new WaitForSeconds(0.5f); Vector2 diff = tr.position; float rot_z = Mathf.Atan2(diff.y, diff.x) * Mathf.Rad2Deg + 90f; laser2.rotation = Quaternion.Euler(0f, 0f, rot_z); } public IEnumerator Mover() { yield return new WaitForSeconds(7.5f); if (isLife) { Vector2 diff = tr.position; float rot_z = Mathf.Atan2(diff.y, diff.x) * Mathf.Rad2Deg + 90f; laser.rotation = Quaternion.Euler(0f, 0f, rot_z); target = bt.GetPosRaycast(); isMove = true; } } public void Update() { if (isActive == false) { return; } float s = Time.fixedDeltaTime / (0.03f / Time.timeScale); if (isMove) { trBoss.position = Vector2.MoveTowards(trBoss.position, target, s * 0.5f); if (trBoss.position == (Vector3)target) { isMove = false; if (isLife) { StartCoroutine(Mover()); } } } if (isMoveSaw1) { saw1.position = Vector2.MoveTowards(saw1.position, saw1target, s * 0.1f); if (saw1.position == (Vector3)saw1target) { isMoveSaw1 = false; if (isLife) { SawMover1(); } } } if (isMoveSaw2) { saw2.position = Vector2.MoveTowards(saw2.position, saw2target, s * 0.1f); if (saw2.position == (Vector3)saw2target) { isMoveSaw2 = false; if (isLife) { SawMover2(); } } } if (isMoveSaw3) { saw3.position = Vector2.MoveTowards(saw3.position, saw3target, s * 0.1f); if (saw3.position == (Vector3)saw3target) { isMoveSaw3 = false; if (isLife) { SawMover3(); } } } if (isMoveLaser1) { laser1.position = Vector2.Lerp(laser1.position, laser1target, s * 0.1f); if (laser1.position == (Vector3)laser1target) { isMoveLaser1 = false; if (isLife) { LaserMover1(); } } } if (isMoveLaser2) { laser2.position = Vector2.Lerp(laser2.position, laser2target, s * 0.1f); if (laser2.position == (Vector3)laser2target) { isMoveLaser2 = false; if (isLife) { LaserMover2(); } } } if (isMoveTraps) { trap1.position = Vector2.MoveTowards(trap1.position, traptarget1, s * 0.1f); trap2.position = Vector2.MoveTowards(trap2.position, traptarget2, s * 0.1f); trap3.position = Vector2.MoveTowards(trap3.position, traptarget3, s * 0.1f); trap4.position = Vector2.MoveTowards(trap4.position, traptarget4, s * 0.1f); lr1.SetPosition(0, trap1.position); lr1.SetPosition(1, trap2.position); lr2.SetPosition(0, trap3.position); lr2.SetPosition(1, trap4.position); if (trap1.position == (Vector3)traptarget1) { isMoveTraps = false; if (isLife) { TrapMover(); } } } } public void OnCollisionEnter2D(Collision2D collision) { if (collision.gameObject == p) { if (isActive == false) { isActive = true; Start(); } if (isMove == true) { hb.StraightDamage(10f, "Boss3"); } else { health = health - ppl.power; float c = health / maxHealth; srBoss.color = new Color(0f, 0f, c); trail.startColor = srBoss.color; if (health <= 0f) { isLife = false; isMove = false; saw1target = trBoss.position; saw2target = trBoss.position; saw3target = trBoss.position; isMoveSaw1 = true; isMoveSaw2 = true; isMoveSaw3 = true; sr.enabled = false; cc.enabled = false; exp.SetActive(true); explosion.health = 0f; explosion.StartCoroutineTimerOffsetExplosion(); Vector2 diff = trBoss.position; float rot_z = Mathf.Atan2(diff.y, diff.x) * Mathf.Rad2Deg + 90f; laser.rotation = Quaternion.Euler(0f, 0f, rot_z); int fieldBoss = bt.BoxPos(trBoss.position); Vector4 r = boxs[laser1Fields[fieldBoss]]; laser1target = new Vector2(Random.Range(rz, rx), Random.Range(rw, ry)); r = boxs[laser2Fields[fieldBoss]]; laser2target = new Vector2(Random.Range(rz, rx), Random.Range(rw, ry)); StartCoroutine(Ended()); } } } } public void EndedCoroutine() { if (!isActive) { //Debug.Log("End"); isActive = true; StartCoroutine(Ended()); } } public IEnumerator Ended() { yield return new WaitForSeconds(6.5f); if (hb.healthBarImage.fillAmount != 0f) { animatorEnd.SetBool("isActive", true); StartCoroutine(EndedFunction()); } } public IEnumerator EndedFunction() { yield return new WaitForSeconds(1.5f); if (hb.healthBarImage.fillAmount != 0f) { PlayerPrefs.SetInt("progress", 35); SceneManager.LoadSceneAsync(loadScene); } } public void ControlDamagers(bool lt, bool lm, bool tm, bool sm, bool sam) { LaserTarget.SetActive(lt); LaserMover.SetActive(lm); TrapsMover.SetActive(tm); SawMover.SetActive(sm); SawsAroundMover.SetActive(sam); } }
      
      





Audio and music



I also can’t write music, but I have enough musical taste to find the right music. In my plan, for each level it was necessary to choose a track. And for the most part I fulfilled the plan: I picked up 25 tracks. I searched all the tracks in the asset store. He took sounds for the rest on freesound.org or similar sites.



The sound from the technical part was made according to a simple principle: on the camera there were 5 disabled AudioSource and an AudioBase script to control the sound. In this script, there was the main function of SetSound with the parameters of volume, loop, type (music or sound) and the audio file itself. After the signal, the sound began to play and (if not looped) IEnumerator turned on with a time equal to the length of the track and after it expired, it turned off the component.



Script AudioBase
 using UnityEngine; using System.Collections; public class AudioBase : GlobalFunctions { public AudioSource[] layerSounds = new AudioSource[0]; public GameObject music; private float musicValue, soundValue; private int lengthLayerSounds = 0; private bool soundActive = true; private Coroutine offsetActive; private int lowerSoundCoroutineCounter = 100; private int upSoundCoroutineCounter = 0; public void Awake() { soundActive = PlayerPrefs.GetString("graphicsquality") != "low"; musicValue = PlayerPrefs.GetFloat("music"); soundValue = PlayerPrefs.GetFloat("sound"); lengthLayerSounds = layerSounds.Length; for (int i = 0; i < lengthLayerSounds; i++) { layerSounds[i].enabled = false; } } public void LowerSound(float timer, int upd, int id, TypePlaying typePlaying) { lowerSoundCoroutineCounter = upd; if (typePlaying == TypePlaying.Music) { StartCoroutine(LowerSoundCoroutine(timer, upd, id, musicValue)); } else { StartCoroutine(LowerSoundCoroutine(timer, upd, id, soundValue)); } } public void UpSound(float timer, int upd, int id, TypePlaying typePlaying) { upSoundCoroutineCounter = 0; if (typePlaying == TypePlaying.Music) { StartCoroutine(UpSoundCoroutine(timer, upd, id, musicValue)); } else { StartCoroutine(UpSoundCoroutine(timer, upd, id, soundValue)); } } public IEnumerator LowerSoundCoroutine(float timer, int upd, int id, float volumeSen) { yield return new WaitForSeconds(timer); layerSounds[id].volume = Stable2((layerSounds[id].volume / volumeSen - timer) * volumeSen, 0f, 1f); if (lowerSoundCoroutineCounter > 1) { StartCoroutine(LowerSoundCoroutine(timer, upd, id, volumeSen)); lowerSoundCoroutineCounter -= 1; } } public IEnumerator UpSoundCoroutine(float timer, int upd, int id, float volumeSen) { yield return new WaitForSeconds(timer); layerSounds[id].volume = Stable2((layerSounds[id].volume / volumeSen + timer) * volumeSen, 0f, 1f); if (upSoundCoroutineCounter < upd) { StartCoroutine(UpSoundCoroutine(timer, upd, id, volumeSen)); upSoundCoroutineCounter += 1; } } public void UpdateSound() { if (soundActive) { float time = Time.timeScale; for (int i = 0; i < lengthLayerSounds; i++) { AudioSource audioSource = layerSounds[i]; if (audioSource.enabled == true) { audioSource.pitch = time; } } } } public void SetSound(AudioClip audioClip, int layerSound, float volume, TypePlaying typePlaying, bool loop, float time) { StartCoroutine(SetSoundTime(audioClip, layerSound, volume, typePlaying, loop, time)); } public IEnumerator SetSoundTime(AudioClip audioClip, int layerSound, float volume, TypePlaying typePlaying, bool loop, float time) { yield return new WaitForSeconds(time); SetSound(audioClip, layerSound, volume, typePlaying, loop); } public void SetSound(AudioClip audioClip, int layerSound, float volume, TypePlaying typePlaying, bool loop) { if (volume == 0f) { return; } if (soundActive) { AudioSource audioSource = layerSounds[layerSound]; audioSource.enabled = true; audioSource.clip = audioClip; audioSource.loop = loop; if (typePlaying == TypePlaying.Sound) { audioSource.volume = soundValue * volume; } else { audioSource.volume = musicValue * volume; } audioSource.Play(); if (offsetActive != null) { StopCoroutine(offsetActive); offsetActive = null; } if (!loop) { offsetActive = StartCoroutine(Offet(layerSound, audioClip.length, audioSource)); } } } public IEnumerator Offet(int layerSound, float length, AudioSource audioSource) { yield return new WaitForSeconds(length); if (audioSource.clip == layerSounds[layerSound].clip) { AudioSource audioSource2 = layerSounds[layerSound]; audioSource2.Stop(); audioSource2.enabled = false; } } }
      
      







Also, the Tramp component (stamping) has its own sound system: when a player enters the stamping of a stamp, the component responsible for the sound is turned on. And if necessary, the product, he determines the distance to the player and after calculating the coefficient gives the necessary volume, sort of creating a realistic sound effect. But it doesn’t work as I wanted it, maybe it’s a matter of incorrectly written code.



Plot



Yes, there is a plot in this game. And he has 2 features: he is almost non-verbal and he has a choice that affects the ending of the game. It’s better to talk about variability (because, in fact, this variability is the whole plot).



The game has 3 choices: on the first two bosses and at level 32. The choice with the bosses is pretty obvious: they can be killed or not by starting an attack or reaching the next level, respectively. And at level 32, it’s a little more complicated: you can activate a trigger, which implies the awakening of the local story saving anchor (a character named AI). The choice on the first two bosses affects whether there will be a battle with 3 bosses. If you kill at least one of the first two bosses, there will be a battle with the third boss. If not, then no.



There are only 4 endings: good, bad, neutral and secret. They are influenced by 2 choices: AI activation and 3 boss kills. I will analyze the endings in order:



Good ending
It happens if 3 boss was not killed and AI was activated. An AI monologue takes place in it, in which he hints at a continuation and a burning eye is shown (from different game effects of fire).









Ending text
thank

You could revive me

And managed not to awaken the Ranger

Apparently you will be his only successful instance

You deserved a little rest

You won

See you


Bad ending
, 3 . («» ), ( ).















-1


Neutral ending
, 3 . , , …







Hello



,











Secret ending
, 3 . , - (!) . ( , )

-

, -

,



, ,



, , , , ...


But why is the plot almost nonverbal? I couldn’t make it completely non-verbal because of the endings. But there is enough text in the game. Indeed, in order to explain to the player “for the ENT of the game”, terminals with notes appeared in the game and they explain the script of the game in great detail.



Scenario The



scenario in this case is the prehistory of the world, revealed from the faces and characters of this game in the form of notes, logs, reports, monologues and dialogues: in general text. And this is such a graphomaniac delirium of a programmer that even Glukhovsky would be surprised (I have nothing against him, I love Metro). Unfortunately, I didn’t have much time to create full-fledged npc. Although the sprites for them in the game I found:









Again, due to circumstances, the script was written last, and already on it I came up with a plot according to which I completed the game. He wrote for 4 weeks every weekday, in a minibus, when I went to normal work. And even in such a short time I could write a lot.



If anything, in the original game there is no plot and no hints of it. And now it makes no sense for me to hide the plot (after all, no one will completely pass the game and read all the notes). There are three goals for this graphomania: add reasonable variability of the player’s actions, explain inexplicable game things and at least a little more interest the player in his game.



I wrote the script in a very simple way: first I wrote it in a story for 40-50 sentences in 2-3 weeks. Then, for each note, I chose according to the sentence, and on the basis of one sentence, I added 2-3 sentences to the note, changed them to monologues (or other forms of narration) and received ready-made balanced notes. As a result, from such a reception in all the notes a total of 160 sentences with information were accumulated.



And you need to understand: in my game there are enough illogical things, and in order to justify each truthfully in the format of a story, a lot of text is needed. Therefore, I tried not to pour water, and each sentence either tried to fill with meaning, or to close the plot hole, or to paint and reveal the characters of the story. But even so, the level of writing remains dubious.



So what is the script talking about? If it’s very simple, then this is the Portal plot, only with an open history of the world and slightly changed characters (more moody). By the way, this scenario has one feature: the sex of inanimate objects has become average despite the logic, common sense or the rules of the Russian language (and other languages ​​too). If someone is suddenly (well, suddenly) interested, then I will leave the full script and all the game notes here:



Scenario
:

, , (3 )

(1 )

, RLIS (2 )



:



[1] . [3] : , , , , .. . [3] , , , ([2] , , ). (four)



[4] , . [5] , . [6] . [6] , . [7] . (5)



[8] : . [9] (- ) . [10] ( ). (3)



[11] . [12] , , , . [13] . [13] , . (3)



[15] ([14] — , , ). [15] ( ) ([16] ). (four)



[17] . [18] «». [18] . [19]- . (four)



[20] «». [21] , . [22] , . (3)



[23] . [24] . [25] « ». (3)



[26] , . [27] , - , . [28] « ». [29] . (four)



[30] - ( ?) ( , ). [31] . [32] , ([33] , ), - , . [34] . [35] , . [36] ( ): 10 (10 = 1 ) . [X]- ( ) , ( 2 1 ?). [37] 2 . (9)



Scrapbook
():



1) {} «» , . , . - , .



2) {} RLIS (reasonable likeness in simulation) — . . RLIS ( ) — .



3) {} RLIS 100 : , , , , .. , , , . , .



4) {} , . , , , , . magnum opus .



5) {ARSotLotC} , . , . , .



6) {} -!!! - , . , , , . , , , . 2 : .



7) {} , backup , . : , . , , .



8) {} . , . , , , ( ).



9) {} , , -. ? . , . , . …



10) {} - , . . , …



11) {ARSotLotC} . , «» , , . , … .



12) {} , «» . Everything. «», . . .



13) {} , . , . , . .



14) {} , , ( ). — , . : , .



15) {} — . , . . , , .



16) {} ? , . .



17) {} . . , «». . , , .



18) {} «». , ? , , .



19) {} «» , . , , , , . . .



20) {} '' ''. , , '' '', , .



21) {} '' : , ''. , . .



22) {ARSotLotC} : , . , .



23) {} , . ' '. , .



24) {} , , . , . , .



25) {} . , - . , , ' '.



26) {} . , . . ' ' !



27) {} ' ' , . . : , , .



28) {ARSotLotC} - < > . . . , .



29) {ARSotLotC} . ? , (- , ) ARSotLotC (Automatic Recording System of the Logs of the Complex).



30) {ARSotLotC} «» , . , , . - , backup . , , .



31) {ARSotLotC} : . , . . backup.



32) {ARSotLotC} . . , . .



33) {ARSotLotC} ( backup'). .



34) {ARSotLotC} , . , , 10 . , . Ps: , , .



35.1) {} . . ' ' . , , ''. , - , . ' '.



35.2)



Code base



Since my specialty is a programmer, code was the main task for me. Compared to the original code base, the sequel code base increased by 2–3 times (even though the original contains 900 lines of code methods, since I was afraid to use bundles such as loops and arrays or GetChild () and loops )



Along with the quantity, the overall quality of the code also increased, but I could not avoid errors. As a result, there are plenty of errors in the code itself. And even despite my objectively meager knowledge, I can clearly see my mistakes. So, we will analyze my most important mistake. Take for example a simple code:



 public class VelocityRotate : MonoBehaviour { public float rotate = 0f; public bool oneTime = true; private bool active = true; public void OnTriggerEnter2D(Collider2D collision) { if (active == true) { if (oneTime == true) { active = false; } Rigidbody2D rb = collision.GetComponent<Rigidbody2D>(); Vector2 vel = rb.velocity; rb.velocity = RotateVector(vel, rotate); } } public Vector2 RotateVector(Vector2 a, float offsetAngle) { float power = Mathf.Sqrt(ax * ax + ay * ay); float angle = Mathf.Atan2(ay, ax) * Mathf.Rad2Deg - 90f + offsetAngle; return Quaternion.Euler(0, 0, angle) * Vector2.up * power; } }
      
      





Did you quickly understand what this script is responsible for? And if you make it like this:



 public class VelocityRotate : MonoBehaviour { //      public float rotate = 0f;//  public bool oneTime = true;//  private bool active = true;//  public void OnTriggerEnter2D(Collider2D collision) { if (active == true) { if (oneTime == true)//   { active = false; } //   Rigidbody2D rb = collision.GetComponent<Rigidbody2D>(); Vector2 vel = rb.velocity; rb.velocity = RotateVector(vel, rotate); } } public Vector2 RotateVector(Vector2 a, float offsetAngle)//   { float power = Mathf.Sqrt(ax * ax + ay * ay);//  float angle = Mathf.Atan2(ay, ax) * Mathf.Rad2Deg - 90f + offsetAngle; //    offset' return Quaternion.Euler(0, 0, angle) * Vector2.up * power; //        } }
      
      





The lack of comments is my very first and really biggest mistake when developing the game! In its entire code base there is not a single comment explaining what this or that branch of the code is responsible for. And perhaps this is not necessary for a small indie game. Well, firstly, I definitely can’t call this game small, and secondly, as a future developer, I will definitely have to work in a team and the absence of such a useful habit as commenting will ever play a trick on me. I just realized this mistake: it haunted me all my projects related to programming, and this time, I took this into account and next time I will make comments.



Bugs and flaws



There were a lot of bugs. Highly! For such a massive work, I allocated a whole month of corrections (August). It makes no sense to parse the examples, I just put a note with all of my documented bugs (although I haven’t documented most of them and fixed them in place):



GB2 Checklist
Designations:

// —

\ —



//1) , ,

//2) :

//3)

//4) TipsGamePlay

//5) ( )

//6) 0:

//7) 1: ()

//8)

//9) 2: 2

//10)

//11) 4:

//12) layer Player

//13) 7: ()

//14) 8: ( 1)

//15) 8:

\16) ( )

\17) 8: zero

//18)

//19) 1:

//20) ,

//21)

//22) timescale=0

//23) 6:

//24) 0:

//25)

//26) 7:

//27) 7:

//28) AspectRatio

\29)

//30)

//31) <EXfgpy)b> //32) 7: -

//33) ,

//34) 9:

//35) 9:

//36) 'loop'

//37) 10:

//38) 11: ()

//39) 11:

//40) 11:

//41) 11:

//42) 11:

//43) 11:

//44) ( )

/45) 12:

\46) Raycast

\47) ( static, dynamic, kinematic)

//48) (next level, next start, next end)

\49) 1: elevatorsave = 0

\50) offset angle,

//51) 2:

//52)

//53) 7:

//54) next save

//55) Dynamic Graph

//56) 11: ( )

57) 11:

//58) 9: ()

//59) 11: ( )

//60) 12: ( 2 . active , . .

61) :

//62) : -

//63) :

64)

//65) (. )

//66)

//67) HealthBar

68) 0:

//69) localposition position

70) 14: bool isPresentation

//71) 17: 2 4

72) ()

\73)

//74)

//75) layer,

//76)

//77) 2: 1

\78) ( )

//79)

//80) 3: ,

//81)

//82) 6: ,

//83) 6: 1

//84) 6:

//85) 7: 40. .

//86)

//87) 9:

//88) 32:

//89) offsetAngle elevator

//90) 11:

//91) ( )

//92)

//93)

//94)

//95) 13:

//96) 15:

/97) 3 isshotmode

//98) 17:

//99) 18: ,

//100) 19: ( )

/101) 20:

\102) Tramp

//103) 20:

\104)

//105) 11: ui

//106) text arial

\107)

//108)

//109) 3:

//110) 3:

//111) 3: ,

//112) ,

//113) ()

//114) 4:

//115) ( )

//116) ()

//117) pointsAnimation basicAnimation

//118) 7:

//119) 9:

//120) AudioBase

//121) pointsAnimation

//122) , ( )

//123) 13: HealthBar

//124) 13: ,

//125) 14: kinematic (. )

//126) 14:

//127) 14: ,

//128) velocityField ( , )

//129) 16: velocityField

//130) 22:

//131) 22:

\132) 25:

//133) 26:

//134) 27:

\135) ( )

//136)

//137) :

//138) ( )

//139)

//140) 8:

//141) ( 1.5-2, -oneshot'

\142) lerp

//143) , , ( , )

//144) 22:

//145) 11:

//146) 11:

//147) 11:

//148)

//149) «Home» «Menu»

//150)

//151)

//152)

\153) ( healthEnd)

//154) :

//155) 33: ,

//156) 15: ( 0.1)

//157) 15: velocityfield healthbar

//158)

//159) basicAnimation (27)

//160) (18, 27)

//161)

\162) 19: -

//163) ( trigger collision)

//164) 20: 50 250

//165) shotmode

//166) 27:

//167) 28:

//168) 17:

//169) tag boss3

\170) ( , )

//171) 35

//172) : , 600 «I'll come back»

//173) 33:



//174)

//175) HealthBar

//176) ( damage-

//177) 27:







0) (0)

1) (2)

2) (2)

3) (1)

4) (1)

5) (1)

6) (1)

7) (1)

8) (2)

9) (1)

10) (0)

11) (1)

(13)

12) (0)

13) (2)

14) (2)

15) (0)

16) (0)

17) (1)

18) (1)

19) (3)

20) (0)

21) (3)

22) (1)

(13)

23) (1)

24) (1)

25) (0)

26) (0)

27) (0)

28) (3)

29) (1)

30) (2)

31) (0)

32) (0)

33) (1)

34) (1)

(10)





And what makes sense to disassemble is the flaws. And not small ones that can be attributed to bugs, but large ones, which are the grossest errors in the game performance. I also want to note that by flaws I do not mean flaws. The game has a lot of drawbacks, this is understandable, but I want to make out those things that I could fix or prevent them from being created.



So what are my main flaws?



  1. . , . 2 3-4 . , , : 10 . , . .
  2. , . , , , , .
  3. . , « » 60% . , .


Localization



Due to the full-fledged scenario, the volume of localized text has grown by about 30 times. But the translation technique has not changed a bit: as I translated through Google Translate, I continue. Only at first I translated directly from Russian, and now I translate into English, correct errors and already from it into other languages. Also, the number of languages ​​was reduced: if the original game had 18 languages, and its page was translated into ALL languages ​​that google supported, the sequel was transferred to only 10 languages: what is in the game, what is on the page (and this is the only sequel in which inferior to the original).



For normal notes-terminals, I made a rather big enough scheme for working with text. In short, instead of simple strings, there was a special class for working with different languages:



Script StringLanguageMinimize
 [System.Serializable] public class StringLanguageMinimize { public string english = ""; public string spanish = ""; public string italian = ""; public string german = ""; public string russian = ""; public string french = ""; public string portuguese = ""; public string korean = ""; public string chinese = ""; public string japan = ""; public string GetString() { string ret = ""; switch (PlayerPrefs.GetString("language")) { case "english": ret = english; break; case "spanish": ret = spanish; break; case "italian": ret = italian; break; case "german": ret = german; break; case "russian": ret = russian; break; case "french": ret = french; break; case "portuguese": ret = portuguese; break; case "korean": ret = korean; break; case "chinese": ret = chinese; break; case "japan": ret = japan; break; } return ret; } }
      
      







And exactly the same class for terminals:

Script Terminal
 [System.Serializable] public class StringLanguage { [TextArea] public string english = ""; [TextArea] public string spanish = ""; [TextArea] public string italian = ""; [TextArea] public string german = ""; [TextArea] public string russian = ""; [TextArea] public string french = ""; [TextArea] public string portuguese = ""; [TextArea] public string korean = ""; [TextArea] public string chinese = ""; [TextArea] public string japan = ""; public string GetString() { string ret = ""; switch (PlayerPrefs.GetString("language")) { case "english": ret = english; break; case "spanish": ret = spanish; break; case "italian": ret = italian; break; case "german": ret = german; break; case "russian": ret = russian; break; case "french": ret = french; break; case "portuguese": ret = portuguese; break; case "korean": ret = korean; break; case "chinese": ret = chinese; break; case "japan": ret = japan; break; } return ret; } }
      
      







Next was the terminal trigger code:



Script Tips Input
 using UnityEngine; public class TipsInput : MonoBehaviour { public int idTips = 0; public bool isPress2Read = true; public bool oneTime = true; private bool active = true; public GameObject[] copys; private Data data; private Press2Read p2r; private TipsInput ti; private void Awake() { data = GameObject.FindWithTag("MainCamera").GetComponent<Data>(); p2r = GameObject.FindWithTag("Press2Read").GetComponent<Press2Read>(); ti = GetComponent<TipsInput>(); } public void OnCollisionEnter2D(Collision2D collision) { if (collision.transform.CompareTag("Player")) { if (isPress2Read == false && active == true) { Disable(); data.SetDialoge(idTips); if (copys.Length != 0) { for (int i = 0; i < copys.Length; i++) { copys[i].GetComponent<TipsInput>().Disable(); } } } else if (isPress2Read == true) { p2r.Active(ti); } } } public void OnCollisionExit2D(Collision2D collision) { if (isPress2Read == true) { p2r.DeActive(); } } public void Disable() { if (oneTime == true) { active = false; } return; } }
      
      







Important class Data:



Data
 using UnityEngine; using UnityEngine.UI; using System.Collections; public class Data : GlobalFunctions { public Dialoge[] dialoges; public DeadPhrases[] deadPhrases; public GamePlay[] gameplay; [Space] public Tips tips; public AudioBase audioBase; public TipsGamePlay gamePlayTips; public Image slowmobonus; public Text fpsText; public float scaleTips = 1f; public float scaleGameUI = 1f; public float scaleSlowMo = 1f; private float speed = 0f; private float target = 1f; private float timeDuration = 1f; private int updFPS = 0; public void Awake() { scaleTips = scaleGameUI = scaleSlowMo = 1f; slowmobonus.color = new Color(0f, 0f, 0f, 0f); } public void Start() { StartCoroutine(SecFPSUpdate()); } public void SetDialoge(int id) { if (dialoges.Length != 0) { tips.SetActiveTrue(dialoges[id].dialogeStrings, dialoges[id].name); } } public void FalseP2R() { tips.SetFalse(); } public string GetDeadPhrase(string typeDead) { int idType = -1; for (int i = 0; i < deadPhrases.Length; i++) { if (deadPhrases[i].typeDead == typeDead) { idType = i; break; } } if (idType == -1) { return typeDead; } int rand = Random.Range(0, deadPhrases[idType].deadPhrases.Length); return deadPhrases[idType].deadPhrases[rand].GetString(); } public string GetDeadPhrase2() { string ret = ""; switch (PlayerPrefs.GetString("language")) { case "english": ret = "Tap to continue"; break; case "spanish": ret = "Pulse para continuar"; break; case "italian": ret = "Tocca per continuare"; break; case "german": ret = "Tippen Sie, um fortzufahren"; break; case "russian": ret = "  "; break; case "french": ret = "Appuyez sur pour continuer"; break; case "portuguese": ret = "Clique para continuar"; break; case "korean": ret = "계속하려면 탭하세요"; break; case "chinese": ret = "点按即可继续"; break; case "japan": ret = "タップして続行します"; break; } return ret; } public void PauseGameUI(float time) { scaleGameUI = time; Update(); audioBase.UpdateSound(); } public void SetGamePlayTips(int id) { if (id == -1) { gamePlayTips.SetActiveTrueSaved(); } else { gamePlayTips.SetActiveTrue(gameplay[id]); } } public void SlowMo(float timeDuration2, float setSlowMo, float speed2) { speed = speed2; target = setSlowMo; timeDuration = timeDuration2; Update(); audioBase.UpdateSound(); } public void SlowMo(float timeDuration2) { scaleSlowMo = 0.1f; float sb = (1f - scaleSlowMo) * 0.3921569f; slowmobonus.color = new Color(0f, 0f, 0f, sb); Update(); audioBase.UpdateSound(); } public IEnumerator EndAnim(float timeDuration) { yield return new WaitForSeconds(timeDuration); End(); } public void End() { scaleSlowMo = 1f; float sb = (1f - scaleSlowMo) * 0.3921569f; slowmobonus.color = new Color(0f, 0f, 0f, sb); Update(); audioBase.UpdateSound(); } public void End2(float timeDuration2) { if (timeDuration2 == 0) { End(); return; } StartCoroutine(EndAnim(timeDuration2)); } private void Update() { Time.timeScale = scaleTips * scaleSlowMo * scaleGameUI; Time.fixedDeltaTime = 0.03f * scaleSlowMo * scaleTips; updFPS = updFPS + 1; return; } private IEnumerator SecFPSUpdate() { yield return new WaitForSeconds(1f); fpsText.text = "FPS: " + updFPS; updFPS = 0; StartCoroutine(SecFPSUpdate()); } }
      
      







And the main Tips class, which is responsible for the operation of the terminal:



Script Tips
 using System.Collections; using UnityEngine.UI; using UnityEngine; public class Tips : GlobalFunctions { public Data data; public Press2Read p2r; public GameUI gameUI; public GameObject obj; public AudioClip setClip; public Text nameText; public Text txt; private int textID = 0; private int textsID = 0; private AudioBase audioBase; private DialogeString textActive; private DialogeString[] textsActive; private bool isMass = false; [TextArea] public string end = ""; [TextArea] public string endPast = ""; public void Start() { audioBase = GameObject.FindWithTag("MainCamera").GetComponent<AudioBase>(); data.scaleTips = 1f; obj.SetActive(false); txt.text = ""; } public void SetActiveTrue(DialogeString text, StringLanguageMinimize name) { data.scaleTips = 0.1f; audioBase.layerSounds[0].volume /= 10f; obj.SetActive(true); nameText.text = name.GetString(); gameUI.pauseButton.SetActive(false); textActive = text; isMass = false; StartCoroutine(TimerFalse()); } public void SetActiveTrue(DialogeString[] texts, StringLanguageMinimize name) { data.scaleTips = 0.1f; audioBase.layerSounds[0].volume /= 10f; obj.SetActive(true); nameText.text = name.GetString(); gameUI.pauseButton.SetActive(false); textsActive = texts; isMass = true; StartCoroutine(TimersFalse()); } public IEnumerator TimerFalse(float time = 0.02f) { yield return new WaitForSecondsRealtime(time); string ds = textActive.dialogeString.GetString(); if (textID < ds.Length && ds != end) { audioBase.SetSound(setClip, 1, 0.5f, TypePlaying.Sound, false); end = end + ds.Substring(textID, 1); txt.text = endPast + end; textID = textID + 1; if (textID + 1 != ds.Length && ds != end) { if (ds.Substring(textID + 1, 1) == ",") { StartCoroutine(TimersFalse(0.1f)); } else if (ds.Substring(textID + 1, 1) == ".") { StartCoroutine(TimersFalse(0.15f)); } else if (ds.Substring(textID + 1, 1) == "?") { StartCoroutine(TimersFalse(0.15f)); } else if (ds.Substring(textID + 1, 1) == ".") { StartCoroutine(TimersFalse(0.15f)); } else { StartCoroutine(TimersFalse()); } } else { StartCoroutine(TimersFalse()); } } else { endPast = txt.text; if (textActive.isSkip) { if (textActive.skipOffset == 0f) { SetActiveFalse(); } else { IsSkip(textActive.skipOffset); } } } } public IEnumerator TimersFalse(float time = 0.02f) { yield return new WaitForSecondsRealtime(time); string ds = textsActive[textsID].dialogeString.GetString(); if (textID < ds.Length && ds != end) { audioBase.SetSound(setClip, 1, 0.5f, TypePlaying.Sound, false); end = end + ds.Substring(textID, 1); txt.text = endPast + end; textID = textID + 1; string ds1 = textsActive[textsID].dialogeString.GetString(); if (textID + 1 != ds1.Length && ds1 != end) { if (ds1.Substring(textID + 1, 1) == ",") { StartCoroutine(TimersFalse(0.1f)); } else if (ds1.Substring(textID + 1, 1) == ".") { StartCoroutine(TimersFalse(0.15f)); } else if (ds1.Substring(textID + 1, 1) == "?") { StartCoroutine(TimersFalse(0.15f)); } else if (ds1.Substring(textID + 1, 1) == "!") { StartCoroutine(TimersFalse(0.15f)); } else { StartCoroutine(TimersFalse()); } } else { StartCoroutine(TimersFalse()); } } else { endPast = txt.text; if (textsActive[textsID].isSkip) { if (textsActive[textsID].skipOffset == 0f) { SetActiveFalse(); } else { IsSkip(textsActive[textsID].skipOffset); } } } } public IEnumerator IsSkip(float time) { yield return new WaitForSecondsRealtime(time); SetActiveFalse(); } public void SetFalse() { obj.SetActive(false); gameUI.pauseButton.SetActive(true); end = ""; endPast = ""; txt.text = ""; textID = textsID = 0; data.scaleTips = 1f; audioBase.layerSounds[0].volume *= 10f; } public void SetActiveFalse() { if (isMass == false) { if (textActive.dialogeString.GetString() != end) { end = textActive.dialogeString.GetString(); if (textActive.isSkip) { SetActiveFalse(); } } else { obj.SetActive(false); gameUI.pauseButton.SetActive(true); end = ""; data.scaleTips = 1f; audioBase.layerSounds[0].volume *= 10f; } } else { if (textsActive[textsID].dialogeString.GetString() != end) { if (textsActive[textsID].isStep == true) { txt.text = end = textsActive[textsID].dialogeString.GetString(); if (textsActive[textsID].isSkip) { SetActiveFalse(); } } else { end = textsActive[textsID].dialogeString.GetString(); txt.text = endPast + end; } } else { if (textsID != textsActive.Length - 1) { textsID = textsID + 1; textID = 0; end = ""; if (textsActive[textsID].isStep == true) { endPast = ""; } StartCoroutine(TimersFalse()); } else { obj.SetActive(false); gameUI.pauseButton.SetActive(true); p2r.UnTap(); end = ""; endPast = ""; txt.text = ""; textID = textsID = 0; data.scaleTips = 1f; audioBase.layerSounds[0].volume *= 10f; } } } } }
      
      







I decided that it would be depressing if the text was just shown, and therefore with the help of IEnumerator I made an emulation of writing the text (exactly the same effect in the end).



Release date



Initially, my plan was to put the game on September 1. And so I did: at the last moment it turned out that I had 4 bugs in the ending (and also it was not translated), quickly fixed it and laid out the game in the evening. Unfortunately, the check was delayed for 7 days, because I decided to check the offer with something manually. Most likely the matter is in the account, which has become "defined" and it is already checked by moderation manually.



PR was much more difficult for me than preparing for the release, because there was no money and connections, but I wanted to distribute the game. Therefore, I used simple methods: I threw it all to friends in VK, created posts on Reddit, threw it into the offer to sites on mobile games, tried to contact the authors of music, etc. And this gave a little result:









Total



Surprisingly, it was on the day when I posted this article that I spent 3 years in IT! And despite my 16 years of age, on that very day, when I was 13 years old, I set a goal: to learn programming and create a dream game. And from that moment, to some extent, my dream came true.



What about the game? I am pleased with her. No, really, I haven’t received as much useful information and experience from anything as from this project. Well, the quality of the game could be clearly higher, but even that which is already good for me. Also, for me this game is something personal and it would be disrespectful to monetize this game first of all. Therefore, in it there is no advertising, donation and it does not have a paid version



After this, I would like to continue to be in game dev. But life circumstances are such that it is no longer possible. And in order to start becoming a programmer normally, I need development, personal growth over myself. I don’t know what to study now and where should I go, but one thing I know for sure: this is most likely my last game on the unity engine.



Thanks for at least some attention. If my story turned out chaotic ask questions, I will specify that I can.



PS: Someone liked the previous trailer:









And so here is the trailer for this game:






All Articles