Mi magnum opus del mundo de los juegos móviles

Hola Habr! Hoy, 26 de septiembre, es mi cumpleaños, lo que significa que para mí esta es una gran razón para lanzar un artículo sobre la secuela de mi rompecabezas. Te advierto que soy un aficionado, lo que significa que habrá muchos errores en TODOS los aspectos del desarrollo (si lo encuentras, escribe, con mucho gusto lo tendré en cuenta). En este artículo me gustaría contar todo (bueno, o casi todo) sobre cómo hice la secuela, cómo llegué a esto y a qué llegué.

Para no confundirme, aquí me refiero a los significados de los términos que están en el artículo:
El original es la primera parte, un juego con un impulso subterráneo de la tecnodemo. Puedes leer sobre esto aquí .

La secuela es la segunda parte de la serie, el juego que se trata en este artículo.

Periódicamente compararé el juego original con la secuela para enfatizar la diferencia entre los dos.

Brevemente sobre el desarrollo

Comencé a trabajar en el juego a fines de enero y para fines de marzo se completó la parte técnica (2 meses). Después de que tomé otro juego y regresé para continuar desarrollando este juego a mediados de mayo. Terminé claramente al final del verano y todo este tiempo (3.5 meses) llené el juego con contenido. Y como resultado, la secuela fue hecha aún más rápido por mí que el juego original (6 meses versus 5.5 meses).

Hice un juego en el motor de la unidad. Me gustaría que estos muchachos hicieran su propio motor y avanzaran en la programación, pero algo salió mal y aún así decidí hacer el juego en un estándar, pero probado por mi instrumento.

Entre el original y la secuela

La idea de crear una secuela surgió un mes antes del lanzamiento del juego original (en algún lugar de agosto). Al ver los errores que cometí, quise eliminar todo y comenzar a trabajar nuevamente con logros exitosos. Pero no comencé a cambiar nada debido al hecho de que había muchos códigos de problemas, todo el contenido estaba listo y solo retrasé el desarrollo. Era necesario ir a liberar.

Después del lanzamiento, nuevamente me atormentó la idea de una secuela. Esta vez no comencé, porque era moralmente flojo, después del lanzamiento fue completamente suave. Quería algo nuevo e interesante. Comenzaron experimentos masivos.

Los siguientes 3 meses intenté implementar cualquier idea, configuración o concepto en los juegos. Lo hice a pesar de la escala de ambiciones, las dificultades de ejecución, y también a veces a pesar de la lógica y el sentido común. Como resultado, obtuve unos 50 proyectos. Eran de diferentes géneros: desde tiradores hasta estrategias, desde plataformas hasta juegos de rol.

Entonces los experimentos continuarían hasta que me cansara. Y estoy cansado de no hacer juegos, sino de lo incompleto que hice. Me propuse un objetivo: hacer al menos algún tipo de juego una semana antes del final. Y así apareció mi segundo juego.

Pro 2 juego
Este juego es muy simple y complejo al mismo tiempo. Es necesario cortar líneas y no gráficos. El significado del juego es que cada línea de corte se dividió por 2, y apareció un gráfico en su centro. La característica del juego era que toda la geometría era dinámica. Los gráficos se movían y las líneas siempre conectaban ciertos gráficos.




Después de esto, estaba motivado (estoy motivado) y listo para un nuevo proyecto. Sentí una oleada de fuerza y ​​aún tomé la secuela de mi juego.

Idea

Antes de comenzar a hacer algo, decidí echar un vistazo completo al original. Y horrorizado. De calidad. El juego, sobre todo, se redujo a rompecabezas estándar: la necesidad de desbloquear niveles, recoger estrellas, un temporizador, terminar, pero todo esto se hizo sin un presupuesto y muy insípido. ¡El original realmente carecía de animaciones! Aunque había algo original en él, y algo, probablemente, sincero. Aunque incluso aquí lograron adelantarse a mí.

Encontré algo similar
Resulta que hay un juego muy similar con un nombre casi idéntico. Y ella parece una variación más exitosa de mi juego. Me enteré de ella en este video .
Después descubrí que este juego es un exclusivo LG Smart TV. Fue creado por la división rusa de LG R&D Lab en 2014:



Está controlado por las flechas "izquierda" y "derecha" en el control remoto. De la misma manera que en mi juego (2 partes de la pantalla). ¿Qué puedo decir? El ángulo de inclinación es el mismo: 30 °. Puramente técnicamente, se puede decir que mi juego está plagiado por esto. Aunque me enteré de ella unos 2 meses después del lanzamiento del primer juego.




Entendiendo la posición muy deplorable del original, decidí revivir el juego con cambios radicales, para mejorarlo. Y luego la fantasía voló: que haya una trama y que haya una opción en ella, habrá jefes con ataques reflexivos, habrá una producción de la que el original careció, habrá la sensación de una sola aventura completada, etc. En general, todas las mejores ideas que se me ocurrieron durante el tiempo entre el original y la secuela. Y todo lo que no funcionó o funcionó mal fue destinado a tirar de la secuela.

Primera demo

Todo comenzó, por supuesto, con él. Decidí: "Si resuelve problemas, hágalo a fondo". Y la primera víctima de tales cambios fue la administración. Podría robarlo de un juego similar (ver arriba). Esta es exactamente la gestión que originalmente quería, pero no sabía cómo hacerlo. Un suplemento sería bastante simple: solo agregue animaciones de rotación cada vez que haga clic. Pero eso no fue para mí. Al menos para percibir el control, así como en un juego similar, era necesario hacer la misma cámara estática y obviamente reducir los niveles junto con el ritmo del juego. Pero quería acción, dinámica y velocidad, así que hice un desarrollo lógico del control original. Ahora, en lugar de presionar y girar un cierto grado, hubo una sujeción y el grado de rotación final se determinó por su duración. Se veía claramente mejor que en el original.

Debido a que normalmente controlaba el control, el error principal del original desapareció y ahora era posible hacer que los niveles MUCHO más cargados que en el original sin temor a retrasos y frisos. Y luego vino la parte experimental.

Demo gráfica

Nunca supe cómo dibujar gráficos normales y casi siempre fue reemplazado por la parte tecnológica, o más bien su ejecución normal. Y este juego no fue la excepción. En lugar de simples sprites normales, apareció una luz realista. Era una ilusión de luz 2D. De hecho, esta es una luz tridimensional, contra una superficie de metal, y todos los objetos tenían materiales con sombreadores específicos. Se veía bastante bien:



En las pruebas, mostró 60 fps estables, pero en el teléfono, incluso en mi Sony Xperia, era de alrededor de 20 fps y se hundió a 10 fps. Y me encontré con un techo de rendimiento. Tenía que ir por un camino diferente, el camino de la destructibilidad ...

La destructibilidad

Inicialmente, todo me pareció una mala idea. Pero decidí probar y ahora este es mi programa principal de juegos. Según el plan, nuevamente quería más realismo, es decir, la destrucción de los fragmentos generados, dependiendo de la dirección y la fuerza del impacto. Pero el plan nuevamente descansaba en el techo, esta vez que yo sepa. Tuve que simplificar a uno más simple.

Ahora, la destructibilidad se basaba en un principio de operación más simple, es decir, creaba una copia de sí mismo, solo a partir de fragmentos físicos, y el objeto original eliminaba los componentes de SpriteRenderer, Collider2D y, si había uno, desactivaba Rigidbody2D.

Pero surgió otra pregunta: colisionadores. Por un lado, podrías usar PolygonCollider2D y no ser atormentado, pero por otro lado, tendrías que sufrir más tarde en el diseño y la optimización del juego. Por lo tanto, todos los fragmentos de los bloques destruidos tenían BoxCollider2D (incluso fragmentos de objetos redondos).

Además, se realizó una contribución significativa a la optimización mediante la configuración correcta del parámetro de tiempo fijo paso a paso (se convirtió en igual a 0.0 (3) o 30 por segundo). Pero ahora, a altas velocidades, el objeto voló a través de él, y esto definitivamente afectó el diseño del juego.

¡Estos elementos llevaron la optimización a un nivel aceptable y ahora podría haber hasta cientos de objetos físicos en el escenario! Después del original, definitivamente fue un gran avance, revolución, etc. Al darme cuenta de que me estaba moviendo en la dirección correcta, decidí solucionar otro problema de juego de larga data: el abrumador hardcore. Para provocar de alguna manera el juego que hice ...

Sistema de daños

Para mí, esta es la parte más oscura del desarrollo, que se ha reescrito 2 veces. Trabajar en ello estaba en curso. Como resultado, salió un sistema extremadamente sofisticado, pero funcionó bastante ampliamente.

Pero primero, vale la pena mencionar cómo funciona la percepción del daño aquí. Puede parecer que funciona según el principio "cuanto más golpeas, más fuerte es el daño", pero esto no es así. En la mayoría de los casos, funciona según el principio de "cuanto más largo es el contacto, mayor es el daño", donde el lugar de algo tan importante como la "fuerza de impacto" fue reemplazado por un coeficiente de daño que se configuró manualmente para cada objeto que causa daño, dependiendo de la situación. Esto sucedió debido al hecho de que el paso fijo del tiempo resultó ser tan grande que se creó un poderoso error: el juego no logró procesar Enter2D. Y esto creó situaciones como: se estrelló a alta velocidad, no recibió daño. ¿Por qué no lo arreglé? Ni siquiera yo puedo decir eso.

Entonces, ¿dónde comenzó el sistema de daños? De la salud. El jugador tiene una salud igual a 1 (luego aumentó a 2). Sí, esto todavía no es suficiente, y al primer contacto fuerte con la trampa morirá, pero al menos a baja velocidad existe la posibilidad de sobrevivir (incluso varias veces). No quise cambiar el original. "¿Pero qué causará daño al jugador?" - Pensé y se me ocurrieron las trampas principales.

Trampas principales

La base de mi rompecabezas consistía en trampas, pero contradecían el nombre del juego. Del nombre se deduce que el juego debe ser sobre bolas que caen bajo la influencia de la gravedad. Pero no había tantos de ellos. En cambio, había más acertijos estándar.

El principal y el primero fue una sierra. Rompecabezas simple y claro. Fue escrito de manera no muy óptima, durante el período de postproducción lo arreglé.


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; } } 


El siguiente fue un láser, que, bueno, cargó todo muy pesadamente. Si pones 40 piezas en el escenario, el juego comenzará a retrasarse significativamente. Pero también tenía el deseo de agregar leyes físicas completas de la luz, es decir, reflexión o incluso refracción. Pero no hubo tiempo, no lo terminé. Aunque optimicé algunas cosas, no ayudó mucho.


Script láser
 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; } } 



La bomba fue la última trampa, y antes de agregarla, reescribí el sistema de toma de daños, en particular, transfirí todo lo relacionado con la salud del jugador a un script separado de HealthBar (útil para otros fines). Después de que la bomba todavía apareció, y su física dejó mucho que desear, en el proceso se terminó nuevamente. Y al final resultó ser bastante digno.


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; } } 


Después de mirar todo el sistema de daños, decidí reescribirlo a fondo. Y esta vez, Damage puso todas las variaciones de daño posibles en un script de Damage, e hizo un método similar de ActionPhysicsEmulation para bloques destructibles (al final, para cada tipo de daño individual, se escribió su propio método optimizado). Además, la intensidad del daño estaba determinada por la intensidad de la "fuerza" del objeto (el guión estaba solo en el jugador).

Y al final, solo estos 3 acertijos estaban por encima del original. Pero esta no fue una razón para parar: tampoco me olvidé de experimentar durante todo el desarrollo. Así apareció.

Campo de fuerza (desactiva la gravedad, ralentiza y mata lentamente)


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); } } } } 


Pisotear (mató a los jugadores, aplastándolos)

Vagabundo 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; } } } 


Radiación (que reduce lentamente la salud)


Radiación de guión
 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); } } } 


Trampa (una bola azul que mata cuando se toca, que es una referencia al juego más difícil del mundo)


Guión inmortal
 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); } } } 


No registré todos estos tipos de daños en el script de Daño, pero generalmente funcionaban bien con muletas. Después de esto, la mecánica adicional entró en línea.

Mecánica adicional

Se hicieron variados. Había bastantes, por lo que todos eran de interés y no lo suficiente como para ser funcionales para la interacción con la mayoría de las mecánicas del juego.

La primera de esas mecánicas fueron las puertas. El primero y más funcional de todos. Definitivamente útil en todos los lugares donde se necesitaban barreras funcionales. También tiene funciones adicionales: isActive para determinar el estado de inicio e isState para fijar la posición después de la activación (los nombres están mezclados, pero cuando noté que era demasiado tarde para arreglarlos).


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); } } } 


Funcionalidad similar poseía los objetos físicos. No, estos no son objetos de la destrucción, son solo objetos físicos (aunque también podrían destruirse, pero no usaron esta mecánica). No hay muchos de ellos en rompecabezas, pero se combinan bien con otras mecánicas. Por ejemplo, con una puerta: cuando un objeto toca un gatillo de la puerta, la puerta se abre.

Desde que aprendí a "poseer poder", hasta tres mecánicos lo controlaron. Estos fueron disparadores con el mismo código para interactuar con los objetos, pero cada uno realizó tareas a su manera. El primero era un campo de fuerza (ralentizaba el objeto, multiplicando la fuerza por un cierto factor). El segundo agregó fuerza en la dirección del punto y el punto tenía "gravedad". El tercero se hizo por accidente: cuando el rompecabezas relacionado con la gravedad cero no funcionó, este script lo salvó. En él, el objeto cambia la dirección de la fuerza, sin cambiarla, su intensidad.


Como funciona
Primero, según el teorema de Pitágoras, se calcula la hipotenusa, que es el coeficiente del vector y es útil para restaurar la fuerza. El ángulo se calcula utilizando la función Atan2. Después de eso, se agrega offsetAngle a la esquina y se construye un nuevo vector basado en el seno y el coseno, que se multiplica por un coeficiente y se obtiene una dirección cambiada sin una fuerza cambiada.
 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; } 


En esto, toda mi fantasía de extras se secó. Sí, había ideas como una bomba en una soga, un teleférico, etc. Pero entonces surgió la idea normal: tienes que renderizar el juego nuevamente. Aún así, seré sincero conmigo mismo: la gran mayoría de las personas juegan juegos móviles, y casi ninguno de ellos jugará mi juego si el juego es insoportablemente complicado. Decidí comenzar con acertijos que mataran al jugador con un solo golpe, pero no quería cambiar el daño debido a la destructibilidad. Y luego surgió la idea de una mecánica adicional normal: refuerzos o modificadores.

Según el concepto, dieron mejoras temporales que están asociadas con algunos valores básicos. Hubo 5 refuerzos: tratamiento, inmortalidad, dilatación del tiempo (mo lento), cambio en la gravedad y cambio en la masa del jugador.

Pero parecía una especie de estándar: bolas de disparo dispersas por el nivel para ayudar a pasar. Y entonces agregué estos refuerzos al láser. Cambió un poco la mecánica y funcionó.


Ahora el láser tiene 5 modos de interacción con el jugador: daño y curación, inmortalidad, dilatación del tiempo (mo lento), cambio en la gravedad y cambio en la masa del jugador. Es lo mismo, pero con una diferencia: el láser actúa constantemente sobre el jugador y si abandonas el láser, el efecto desaparecerá inmediatamente (o después de un tiempo). Sí, los potenciadores tienen casi lo mismo, pero los láseres no son estándar (y, por lo tanto, todo el juego).

El tema físico del juego hizo posible crear un trampolín, que generalmente se usa para dispersar al jugador y luego destruir la pared (aunque este es un simple BoxCollider2D con PhysicsMaterial, que tenía el parámetro de rebote torcido para diferentes fuerzas de rebote).

Y la arena del juego te permitió crear tus propios guiones para la animación. Básicamente, movieron el objeto de un punto a otro o lo giraron. Anteriormente, tenían muchas más funciones: la capacidad de animar (por puntos) rotar un objeto, cambiar la escala (por puntos), etiquetas más precisas para el comienzo y el final de una animación de un objeto, etc. Pero debido al hecho de que estos eran atavismos, que en total consumieron frenéticamente la productividad, tuve que eliminarlos en nombre de la optimización. El guión de animación se usa donde sea necesario para mostrar una animación simple, porque como dije: "¡El original carecía de animaciones!" Solo hay dos guiones:

Animación básica y Puntos de animación.

Script de animación básica
 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); } } } 

Puntos Script de animación
 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

En comparación con el original, esta es una verdadera obra maestra.

A modo de comparación, aquí está el original:


Aquí está la secuela:


Aquí está el original:


Aquí está la secuela:


Aquí está el original ... Creo que está claro. Minimalismo en la secuela que recordé, y en lugar del botón de pausa de color inapropiado y el temporizador que interfiere francamente, ahora hay un botón de pausa lokanico, de alguna manera notable en la esquina inferior izquierda. La secuela aún gana el menú. A diferencia del original, hay animaciones en todas partes, y el fondo es de 11 sombreadores que escribí accidentalmente en el Gráfico de sombreadores. La funcionalidad también está mejorando, hay una configuración de gráficos, configuraciones de sonido y música separadas, una consola que le permite cambiar el guardado; no hay nada de esto en el menú original.

Resultó tan bueno porque decidí mirar otros juegos. Aquí y allá, en general, tomé (en lugar de robar) lo mejor de todas partes. Y esto es lo que tomé especial:

  1. Jugar menú
    Tomado de Alto's Adventure, solo las experiencias se convirtieron en ridículo, bromas, comentarios irónicos, etc.
  2. Pausa
    También de Alto, pero no tan funcional, pero encaja más convenientemente y juega más convenientemente.
  3. Configuraciones
    Parcialmente tomado del Vector 2, es decir, la forma del menú y los controles deslizantes de volumen.
    Tomó un poco en general, pero por lo demás hizo todo por su cuenta.

Consola

Primero, haga una reserva sobre cómo funciona la conservación. Hay dos variables responsables de la conservación global y local: estos son los números de progreso y ahorro de ascensor, respectivamente. La variable de progreso es responsable de guardar entre escenas, y la variable de guardar ascensor es responsable de guardar dentro de la escena. Cuando presionas el botón "Comenzar" o "Reiniciar", el juego transfiere el progreso a la escena y genera el jugador al guardar con el número de ahorro de ascensor.

La consola le permite cambiar o crear cualquier variable. Una herramienta tan simple y poderosa me fue muy útil para probar el juego e identificar errores en él. La consola en sí es un comando escrito a mano que imita otras consolas.

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); } } 


Y especialmente para ti, te dejaré una lista de equipos líquidos:

  1. descarga: restablece el progreso del juego (y toda otra información también)
  2. echo vertogpro: un equipo para proporcionar acceso a equipos de desarrollo
  3. playerprefs [tipo dado (string, int, float)] [nombre de la variable] [datos] - cambia o crea cualquier variable. Ejemplo: playerprefs int progress 14
  4. siguiente: un subtipo para una navegación de nivel simplificada, con sus propios comandos:
    • inicio: guarda al comienzo del nivel (siguiente inicio)
    • final: guarda al final del nivel (siguiente final)
    • guardar: se teletransporta al siguiente guardado (siguiente guardado)
    • nivel: teletransporta al siguiente nivel (siguiente nivel)

Gráficos

Durante el año no aprendí a dibujar, así que hice casi lo mismo que en el original: descargué unos 30 paquetes de texturas para Maycraft, seleccioné lo mejor de cada uno y así resultaron los gráficos principales. Los gráficos no diferían mucho del original y me enfureció, me enfureció tanto que todavía encontré varios efectos animados (explosiones, fuego, etc.) y bombeé varios paquetes de texturas de la tienda de activos. Incluso para un juego móvil, los gráficos son bastante malos, aunque todavía se está observando progreso. Aquí está el original:


Y aquí está la secuela:


Ahorro

Si el principio de ahorro es simple, entonces su implementación no lo es tanto. El sistema de guardado consta de 3 scripts:

  1. ElevatorBase es la base en la que se producen los equipos de inicio. En él, mediante la variable ascensor salvador, el guardado activo se selecciona de la matriz de guardado.

    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; } } } } } 

Diseño del juego

Fue un verdadero desastre. Fue el diseño del juego lo que alargó el ciclo de desarrollo de 4 a 6 meses. En total, el juego tiene 34 niveles: 30 regulares, 3 jefes y 1 final (nivel). Cada ordinario hice 2-3 días, cada jefe 2 semanas y el nivel final hizo una semana. Para equilibrarlo todo, los construí así: 10 niveles => 1 jefe => 10 niveles => 2 jefe => 10 niveles => 3 jefe => nivel final.

Los niveles locales son mi orgullo. Son inusuales, variados e incluso un poco interesantes. Los niveles están diseñados en una forma específica para crear un sentido del mundo abierto. Para esto, incluso dibujé un mapa:


El mapa no es el mejor dibujo e información, pero dio información importante para las formas necesarias de niveles. Inicialmente, los planes eran hacer todos los niveles en el mapa, pero no hice los que estaban oscuros. Por cierto, este es un mapa con un tamaño de 1000x1000 píxeles, y fue de este mapa que salió la escala: 1 bloque = 1 píxel = tamaño del jugador.

Entre niveles, el jugador pasa por el ascensor. Puede llegar a cualquier nivel y, por lo tanto, es posible viajar entre niveles, creando un jugador con un sentido aún mayor de la apertura del mundo. Y también, en algunos lugares, los disparadores están ocultos para activar los ascensores secretos, que pueden llevar de 10 a 15 niveles.

Para los niveles ordinarios, había un algoritmo de construcción:

  1. Un fondo que tendría una forma y escala como en un mapa
  2. Paredes exteriores (triple espesor debido a la física especial)
  3. Las paredes son internas.
  4. Niveles ellos mismos
  5. Elevadores, salvadores y disparadores de audio

Es más complicado con los jefes, porque cada jefe presenta al mismo tiempo patrones de comportamiento diferentes y similares. Todos los jefes tienen 100 de salud y cada nivel tiene algo que destruir. Es mejor hablar de cada uno por separado:

1 jefe tiene un comportamiento muy simple: se mueve aleatoriamente por la habitación, espera 5 segundos y repite. Para ser honesto, este es un mal ejemplo de un jefe: simple, rezagado y no memorable. Y solo puede ser asesinado golpeándolo. Pero hay una defensa en forma de 4 sierras: 3 de ellas se mueven de forma inteligente al azar por la habitación y una protege al jefe cuando se mueve. Después de la muerte, explota.

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 ya es mejor en calidad, pero aún lejos de ser ideal. Su patrón es más complicado: determina la ubicación del jugador y después del área en la que se encuentra. Después de que el jefe selecciona un punto aleatorio en el área y se mueve hacia él. Su defensa ya es más significativa: la salud del jefe tiene etapas y diferentes armas para cada etapa:

  1. 2 sierras en la distancia
  2. 2 sierras a distancia, cuando están protegidas por una sierra
  3. 2 láser limitado, protegido por una sierra durante el movimiento
  4. 2 láseres, cuando están protegidos por una sierra
  5. 2 láseres, cuando se mueven protegidos por una sierra y 2 sierras a distancia

Además, desde la parte gráfica, el segundo jefe es mejor que el primero: tiempo de inactividad en forma de recuperación de estaminas y actividad de terceros en forma de desactivación de los láseres de jefe, si los activadores se activan en el centro de la habitación.

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 es la mejor calidad entre los jefes! Él usa rayos para moverse. Primero, gira aleatoriamente a cualquier ángulo, luego, entre 12 transmisiones de rayos lanzadas en diferentes direcciones, selecciona la más larga y vuela hasta el punto de emisión de rayos. Hay objetos en el nivel, algunos de los cuales también están siendo destruidos. ¿Y cómo reaccionan los rayos de jefe a los objetos? Se agregaron disparadores a los objetos estáticos, que son 2 veces más grandes que los objetos mismos, de modo que la emisión de rayos tenía un punto donde el jefe no volaba en el aire, no estaría en la pared, sino que estaría clavado en la pared. El jefe tiene una defensa especial: al comienzo del nivel con el jefe (cada jefe es un nivel grande separado sin acertijos de terceros) hay activadores, y se configuran para que solo uno se active.El jefe tiene 5 trampas en blanco y cada gatillo deja solo 3-4 trampas activas. Y también tenía un sistema mejorado de áreas, que consistía en áreas predefinidas para cada área (en la que el jugador puede estar) y para cada trampa. Y durante el vuelo, el jefe siempre mata al jugador.

Lista de trampas:

  1. El láser en el centro, que después de cada vez que el jefe comienza a volar, comienza a mirar al jugador.
  2. 2 láseres que usan la función Lerp para moverse a áreas específicas (dependiendo de la ubicación del jugador) y se envían al jugador antes del movimiento (siempre deben estar frente al jugador, pero algo salió mal).
  3. Una sierra que siempre va a la misma área que el jugador.
  4. 2 sierras, que siempre se dirigen a las áreas izquierda y derecha del área donde se encuentra el jugador.
  5. 4 bolas trampa que se mueven simétricamente al centro

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 y música

Tampoco puedo escribir música, pero tengo suficiente gusto musical para encontrar la música correcta. En mi plan, para cada nivel era necesario elegir una pista. Y en su mayor parte cumplí el plan: recogí 25 canciones. Busqué todas las pistas en la tienda de activos. Tomé sonidos para el resto en freesound.org o sitios similares.

El sonido de la parte técnica se realizó de acuerdo con un principio simple: en la cámara había 5 AudioSource deshabilitados y un script AudioBase para controlar el sonido. En este script, existía la función principal de SetSound con los parámetros de volumen, bucle, tipo (música o sonido) y el archivo de audio en sí. Después de la señal, el sonido comenzó a reproducirse y (si no estaba en bucle) IEnumerator se activó con un tiempo igual a la duración de la pista y, una vez que expiró, apagó el componente.

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; } } } 


Además, el componente Vagabundo (estampado) tiene su propio sistema de sonido: cuando un jugador ingresa al estampado del estampado, el componente responsable del sonido se activa. Y si es necesario, el producto determina la distancia al jugador y, después de calcular el coeficiente, le da el volumen necesario, creando una especie de efecto de sonido realista. Pero no funciona como lo quería, tal vez es una cuestión de código incorrectamente escrito.

La trama

Sí, este juego es la historia. Y tiene 2 características: es casi no verbal y tiene una opción que afecta el final del juego. Es mejor contar sobre la variabilidad (porque, de hecho, esta variabilidad es toda la trama).

El juego tiene 3 opciones: en los dos primeros jefes y en el nivel 32. La elección con los jefes es bastante obvia: pueden ser asesinados o no al comenzar un ataque o alcanzar el siguiente nivel, respectivamente. Y en el nivel 32, un poco más complicado: puedes activar el gatillo, lo que implica el despertar del ancla de salvamento de la historia local (un personaje llamado AI). La elección de los dos primeros jefes afecta si habrá una batalla con 3 jefes. Si matas al menos a uno de los dos primeros jefes, habrá una batalla con el tercer jefe. Si no, entonces no.

Solo hay 4 finales: bueno, malo, neutral y secreto. Se ven afectados por 2 opciones: activación de IA y matar a 3 jefes. Analizaré las terminaciones en orden:

Buen final
Ocurre si 3 jefes no fueron asesinados y se activó la IA. En él tiene lugar un monólogo de IA, en el que insinúa una continuación y se muestra un ojo ardiente (de los diferentes efectos del fuego en el juego).


Texto final
Gracias
Podrías revivirme
y lograr no despertar al Ranger.
Aparentemente serás su única instancia exitosa.
Te merecías un pequeño descanso
.
Ganaste.

Mal final
, 3 . («» ), ( ).





-1

Final neutro
, 3 . , , …



,





Final secreto
, 3 . , - (!) . ( , )
-
, -
,

, ,

, , , , ...

Pero, ¿por qué la trama es casi no verbal? No pude hacerlo completamente no verbal debido a los finales. Pero hay suficiente texto en el juego. De hecho, para explicarle al jugador "para el ENT del juego", aparecieron terminales con notas en el juego y el guión del juego se explica con gran detalle.

Escenario El

escenario en este caso es la prehistoria del mundo, revelada a partir de las caras y los personajes de este juego en forma de notas, registros, informes, monólogos y diálogos: en texto general. Y este es un delirio tan grafomaníaco de un programador que incluso Glukhovsky se sorprendería (no tengo nada en contra de él, me encanta Metro). Desafortunadamente, no tuve mucho tiempo para crear npc completo. Aunque los sprites para ellos en el juego que encontré:


Nuevamente, debido a las circunstancias, el guión fue escrito en último lugar, y ya en él se me ocurrió una trama según la cual completé el juego. Escribió durante 4 semanas todos los días de la semana, en un minibús, cuando fui al trabajo normal. E incluso en tan poco tiempo pude escribir mucho.

En todo caso, el juego original no tiene trama ni pistas. Y ahora no tiene sentido ocultar la trama (después de todo, nadie pasará completamente el juego y leerá todas las notas). Hay tres objetivos para esta grafomanía: agregar una variabilidad razonable de las acciones del jugador, explicar cosas inexplicables del juego y al menos un poco más de interés para el jugador en su juego.

Escribí el guión de una manera muy simple: primero lo escribí en una historia para 40-50 oraciones en 2-3 semanas. Luego, para cada nota, elegí de acuerdo con la oración, y en base a una oración, agregué 2-3 oraciones a la nota, las cambié a monólogos (u otras formas de narración) y recibí notas balanceadas listas para usar. Como resultado, de tal recepción en todas las notas se acumuló un total de 160 oraciones con información.

Y debes entender: en mi juego hay suficientes cosas ilógicas, y para justificar cada una de ellas con la verdad en el formato de una historia, se necesita mucho texto. Por lo tanto, traté de no verter agua, y cada oración intentó llenar de significado, o cerrar el agujero de la trama, o pintar y revelar los personajes de la historia. Pero aun así, el nivel de escritura sigue siendo dudoso.

Entonces, ¿de qué está hablando el guión? Si es muy simple, entonces esta es la trama del Portal, solo con una historia abierta del mundo y personajes ligeramente modificados (más cambiante). Por cierto, este escenario tiene una característica: el sexo de los objetos inanimados se ha vuelto promedio a pesar de la lógica, el sentido común o las reglas del idioma ruso (y otros idiomas también). Si alguien de repente (bueno, de repente) se interesó, entonces dejaré el guión completo y todas las notas del juego aquí:

El guión
:
, , (3 )
(1 )
, RLIS (2 )

:

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

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

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

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

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

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

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

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

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

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

Libro de recuerdos
():

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) {} , «» . . «», . . .

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)

Base de código

Dado que mi especialidad es un programador, el código fue la tarea principal para mí. En comparación con la base de código original, la base de código secuela aumentó de 2 a 3 veces (aunque el original contiene 900 líneas de métodos de código, ya que tenía miedo de usar paquetes como bucles y matrices o GetChild () y bucles )

Junto con la cantidad, la calidad general del código también aumentó, pero no pude evitar errores. Como resultado, hay muchos errores en el código mismo. E incluso a pesar de mi conocimiento objetivamente escaso, veo perfectamente mis errores. Entonces, analizaremos mi error más importante. Tome por ejemplo un código simple:

 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; } } 

¿Entendiste rápidamente de qué es responsable este script? Y si lo haces así:

 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; //        } } 

¡La falta de comentarios es mi primer y más grande error al desarrollar el juego! En toda su base de código no hay un solo comentario que explique de qué es responsable esta o aquella rama del código. Y quizás esto no sea necesario para un pequeño juego independiente. Bueno, en primer lugar, definitivamente no puedo llamar a este juego pequeño, y en segundo lugar, como futuro desarrollador, definitivamente tendré que trabajar en un equipo y la ausencia de un hábito tan útil como comentar alguna vez me engañará. Acabo de darme cuenta de este error ahora: me perseguía todos mis proyectos relacionados con la programación y esta vez, tomé esto en cuenta y la próxima vez haré comentarios.

Errores y fallas

Hubo muchos errores. Muy! Para un trabajo tan masivo, asigné un mes entero de correcciones (agosto). No tiene sentido analizar los ejemplos, solo puse una nota con todos mis errores documentados (aunque no documenté la mayoría de ellos y los corregí en su lugar):

Lista de verificación de GB2
:
// —
\ —

//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)


Y lo que tiene sentido desmontar son los defectos. Y no los pequeños que se pueden atribuir a los errores, sino los grandes, que son los errores más graves en el rendimiento del juego. También quiero señalar que por fallas no me refiero a fallas. El juego tiene muchos inconvenientes, esto es comprensible, pero quiero distinguir aquellas cosas que podría solucionar o evitar que se creen.

¿Cuáles son mis principales defectos?

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

Localización

Debido al escenario completo, el volumen de texto localizado ha crecido aproximadamente 30 veces. Pero la técnica de traducción no ha cambiado un poco: a medida que traduje a través de Google Translate, continúo. Solo al principio traduje directamente del ruso, y ahora traduzco al inglés, corrijo los errores y ya de él a otros idiomas. Además, la cantidad de idiomas disminuyó: si el juego original tenía 18 idiomas y su página se tradujo a TODOS los idiomas que admite Google, la secuela se transfirió a solo 10 idiomas: qué hay en el juego, qué hay en la página (y esta es la única secuela inferior al original).

Para los terminales de notas normales, hice un esquema bastante grande para trabajar con texto. En resumen, en lugar de cadenas simples, había una clase especial para trabajar con diferentes idiomas:

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; } } 


Y exactamente la misma clase para terminales:
Terminal de script
 [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; } } 


El siguiente fue el código de activación del terminal:

Entrada de sugerencias de script
 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; } } 


Datos importantes de la clase:

Datos
 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()); } } 


Y la clase principal de Tips, que es responsable del funcionamiento de la terminal:

Consejos para guiones
 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; } } } } } 


Decidí que sería deprimente si el texto solo se mostrara, y por lo tanto, con la ayuda de IEnumerator, hice una emulación de escribir el texto (exactamente el mismo efecto al final).

Lanzamiento

Inicialmente, mi plan era poner el juego el 1 de septiembre. Y así lo hice: en el último momento resultó que tenía 4 errores al final (y tampoco estaba traducido), lo arreglé rápidamente y presenté el juego por la noche. Desafortunadamente, el cheque se retrasó por 7 días, porque decidí verificar la oferta con algo manualmente. Lo más probable es que el asunto esté en la cuenta, que se ha "definido" y ya se verifica con moderación manualmente.

Las relaciones públicas fueron mucho más difíciles para mí que prepararme para el lanzamiento, porque no había dinero ni conexiones, pero quería distribuir el juego. Por lo tanto, utilicé métodos simples: lo envié todo a mis amigos en VK, creé publicaciones en Reddit, lo lancé a la oferta de sitios en juegos móviles, intenté contactar a los autores de música, etc. Y esto dio un pequeño resultado:


El resultado es

sorprendente, pero fue el día en que publiqué este artículo que pasé 3 años en TI. Y a pesar de mis 16 años, ese mismo día, cuando tenía 13 años, me propuse el objetivo: aprender a programar y crear un juego de ensueño. Y desde ese momento, hasta cierto punto, mi sueño se hizo realidad.

¿Qué hay del juego? Estoy contento con ella No, realmente, no he recibido tanta información y experiencia útil de nada como de este proyecto. Bueno, la calidad del juego podría ser claramente mayor, pero incluso lo que ya es bueno para mí. Además, para mí este juego es algo personal y sería irrespetuoso monetizar este juego antes que nada. Por lo tanto, en él no hay publicidad, donación y no tiene una versión paga

Después de esto, me gustaría continuar en el desarrollo del juego. Pero las circunstancias de la vida son tales que ya no es posible. Y para comenzar a convertirme en programador normalmente, necesito desarrollo, crecimiento personal sobre mí mismo. No sé qué estudiar ahora ni a dónde ir, pero una cosa estoy segura: este es probablemente mi último juego en el motor de la unidad.

Gracias por al menos un poco de atención. Si mi historia resultó ser caótica, haga preguntas, aclararé que puedo.

PD: A alguien le gustó el último avance:


Y aquí está el trailer de este juego:

Source: https://habr.com/ru/post/468363/


All Articles