DevLog - Hagal - Plataformas 2D

B

Tipo de juego y objetivos:

La idea es hacer un plataformas sencillo con assets de 8x8 pixels, cuyo objetivo final sea escapar de una serie de mazmorras, consiguiendo la llave y abriendo la puerta. Como objetivo opcional está la activación de runas, que todavía tengo que pensar si hago algo con eso a parte de contabilizar las runas activadas (cada cierto nº de runas contar algo de historia o algo así? no sé...)

El motor es Unity, para los assets Aseprite.

La paleta de colores usada es la siguiente: https://lospec.com/palette-list/grafxkid-gameboy-pocket-gray
A esos 4 colores hay que añadirle un quinto, el negro #000000 que uso de fondo.

Vista general:

Activación de runas:

Recolección de llave y muerte:

Enemigo simple:

Activación de puerta de salida y cambio de nivel:

De momento os cuento esto, que tengo una intervención en 5 minutos. Iré ampliando a medida que se me ocurran cosas!

7
n3krO

Pole

1
Encofrado

Vamooosssss!!! :muscle:

1
totespare

A favs, dale caña!

1
carra

El concepto no pinta mal, así que buena suerte y esperamos que avances el proyecto :grin:. De todas formas yo por mi parte te aconsejaría algo.

Soy consciente de que dibujar pixel art puede llevar mucho curro. Pero personalmente se me hace difícil jugar a juegos con una resolución tan baja. Te recomiendo al menos plantearte usar tiles de 16x16 pixels. Esto sigue sin ser una resolución muy alta, y como además usas pocos colores no te llevaría tanto trabajo. De todas formas, este cambio que sería puramente gráfico también lo podrías hacer más adelante.

1 1 respuesta
B

#5 Mi idea es hacerlo sí o sí con los tiles de 8x8, en el próximo proyecto seguramente pase a 16x16 y con una paleta de colores de 8.

Aprovecho para comentaros que de buena mañana @n3krO me ha metido en un marrón:

Y a partir de ahí me he liado a mirar mi código y a recordar eso de que si tocas físicas debe ir en FixedUpdate y el input recojerlo en Update. Pues no, no lo tenía como toca y además no es un tema baladí. He buscado un poco online cómo atacar ese tema y he encontrado un cacho código que me ha venido de lujo, además explica la problemática perfectamente:

What the problem is:
In Unity, the only way to get input from the user's keyboard/joysticks/etc. is to poll the Input system in the Update() function. However, there are often times where you need to actually get their input in the FixedUpdate() function (such as when dealing with the physics engine).

Why is this a problem?
If you use the Input system in FixedUpdate things like GetButton/GetButtonDown will miss key-presses. This is because FixedUpdate runs at 50fps (by default) while the Input system runs at however-fast vsync-is-set. Because of this, you can press and release a key between FixedUpdate calls and it'll never register if used there.

How does this fix it?
It gathers input in the Update frames and then stores it until FixedUpdate, after which it will reset the state. It preserves the state of buttons (by only updating if they have been pressed) so even single frame long presses of a key will remain 'true' until FixedUpdate is run, instead of the default behaviour where it turns true and then false again before FixedUpdate is ever run.

La clase es esta: https://gist.github.com/LordNed/e83fa1f8c64eec592d6b

Y la pongo aquí por si la quitan del repo:

using UnityEngine;
using System.Collections;

/// <summary>
/// What the problem is:
///     In Unity, the only way to get input from the user's keyboard/joysticks/etc. is to poll the Input
///     system in the Update() function. However, there are often times where you need to actually get
///     their input in the FixedUpdate() function (such as when dealing with the physics engine).
///     
/// Why is this a problem? /// If you use the Input system in FixedUpdate things like GetButton/GetButtonDown will miss key-presses. /// This is because FixedUpdate runs at 50fps (by default) while the Input system runs at however-fast /// vsync-is-set. Because of this, you can press and release a key between FixedUpdate calls and /// it'll never register if used there. ///
/// How does this fix it? /// It gathers input in the Update frames and then stores it until FixedUpdate, after which it will reset /// the state. It preserves the state of buttons (by only updating if they have been pressed) so even /// single frame long presses of a key will remain 'true' until FixedUpdate is run, instead of the default /// behaviour where it turns true and then false again before FixedUpdate is ever run. /// </summary> public class CharacterInput { private class InputState { public float Horizontal; public float Vertical; public bool Jump; public bool Fire; } public float Horizontal { get { return m_currentState.Horizontal; } } public float Vertical { get { return m_currentState.Vertical; } } public bool JumpDown { get { return !m_previousState.Jump && m_currentState.Jump; } } public bool Jump { get { return m_currentState.Jump; } } public bool FireDown { get { return !m_previousState.Fire && m_currentState.Fire; } } public bool Fire { get { return m_currentState.Fire; } } // The current state of the input that is updated on the fly via the Update loop. private InputState m_currentState; // Store the previous state of the input used on the last FixedUpdate loop, // so that we can replicate the difference between GetButton and GetButtonDown. private InputState m_previousState; // Have we been updated since the last FixedUpdate call? If we haven't been updated, // we don't reset. Otherwise, FixedUpdate being called twice in a row will cause // JumpDown/FireDown to falsely report being reset. private bool m_updatedSinceLastReset; public CharacterInput() { m_currentState = new InputState(); m_previousState = new InputState(); } public void OnUpdate(float horizontal, float vertical, bool jump, bool fire) { // We always take their most up to date horizontal and vertical input. This way we // can ignore tiny bursts of accidental press, plus there's some smoothing provided // by Unity anyways. m_currentState.Horizontal = horizontal; m_currentState.Vertical = vertical; // However, for button presses we want to catch even single-frame presses between // fixed updates. This means that we can only copy across their 'true' status, and not // false ones. This means that a single frame press of the button will result in that // button reporting 'true' until the end of the next FixedUpdate clearing it. This prevents // the loss of very quick button presses which can be very important for jump and fire. if (jump) { m_currentState.Jump = true; } if (fire) m_currentState.Fire = true; m_updatedSinceLastReset = true; } public void ResetAfterFixedUpdate() { // Don't reset unless we've actually recieved a new set of input from the Update() loop. if (!m_updatedSinceLastReset) return; // Swap the current with the previous and then we'll reset the old // previous. InputState temp = m_previousState; m_previousState = m_currentState; m_currentState = temp; // We reset the state of single frame events only (that aren't set continuously) as the // continious ones will be set from scratch on the next Update() anyways. m_currentState.Jump = false; m_currentState.Fire = false; m_updatedSinceLastReset = false; } } [RequireComponent(typeof(CharacterMovement), typeof(CharacterWeaponInventory))] public class Character : MonoBehaviour { private CharacterMovement m_characterMovement; private CharacterWeaponInventory m_weaponInventory; private CharacterInput m_input; private void Awake() { m_input = new CharacterInput(); m_characterMovement = GetComponent<CharacterMovement>(); m_characterMovement.InputController = m_input; m_weaponInventory = GetComponent<CharacterWeaponInventory>(); m_weaponInventory.InputController = m_input; } private void Update() { m_input.OnUpdate(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"), Input.GetButton("Jump"), Input.GetButton("Fire")); } private void FixedUpdate() { // Script Execution order has been modified to ensure that Character should do FixedUpdate last, so it doesn't get // reset until all other components have executed their FixedUpdate calls. m_input.ResetAfterFixedUpdate(); } }

Así que gracias @n3krO por hacer saltar la liebre!

1 1 respuesta
n3krO

#6 Me he leido el codigo y hay 2 cosas que me hacen trigger (aparte de que tengo una duda):

  1. en OnUpdate tiees el if (jump) con { } para solo una linea de codigo y justo despues if (fire) sin nada :psyduck:
  1. en ResetAfterFixedUpdate que necesidad tiene de asignar el m_previousState a m_currentState? No era mas facil simplesmente resetear el jump y fire como hace en las lineas mas abajo e ya? (la posicion horizontal y vertical se van a reescribir si o si).
  1. Que utilidad tiene guardar m_previousState?

    Lo unico que leo al respeto es:
    // so that we can replicate the difference between GetButton and GetButtonDown.
    private InputState m_previousState;
    Entiendo que es por si una mecanica requiere que mantengas el boton pulsado para comparar el estado actual y el anterior?
1 1 respuesta
B

#7 Exacto, es para mecánicas con lo de mantener el botón pulsado, como el Jump de Hollow Knight por ejemplo. No sé si lo usaré, pero la clase está así implementada. Lo de los brackets... bueno los hay que prefieren por legibilidad meterlos siempre, otros sólo cuando es necesario y este tipo lo mezcla, ni me había fijado. Yo en C# los pongo siempre xD

B

Me he semi-inventado un sistema numérico en runas para iluminar al entrar por la puerta de salida del mapa. Y he añadido un frame para cuando el pj abra la puerta:

Y digo semi-inventado porque lo he pillado por ahí y al pasarlo a pixel-art lo he tenido que modificar. El sistema numérico de la puerta dice a dónde te lleva la misma, en la imagen al mapa 0/1 (estando en el 0/0)

1 respuesta
carra

#9 Ese tipo de cosas molan mucho. Por un lado le dan historia al mundo del juego, y por otro lado hacen pensar un poco al jugador. De hecho algunos sistemas de ese tipo a veces rompen la cuarta pared.

1
B

Por cierto, el nombre del juego viene de esta canción:

Que es una runa:

Que simboliza, entre otras cosas, la fuerza destructora de la naturaleza.

Luego os enseño lo que he añadido: Plataformas móviles sacadas del proyecto del reto de portales y una palanca para abrir compuertas.

1
n3krO

Estas a topisimo nezbo! sigue asi!

1 respuesta
B

#12 En Mallorca tenemos un dicho: "Arrancada de cavall, arribada d'ase". Que no sé si se dice en otros lados, pero me suele pasar a menudo, empezar algo a tope de ganas pim pam y al cabo de un tiempo:

A ver si esta vez no es así xD

2 respuestas
n3krO

#13 Eso me pasa cada vez que cojo la guitarra, que duró 2 semanas a topisimo y despues bajona :(

1
totespare

#13 ahora me entero de que eres de Mallorca :O

1 respuesta
B

#15 Más mallorquín que una ensaimada!

B

Plataformas móviles, script rescatado del reto de los portales, funcionan en vertical, horizontal y diagonal:

Sistema de lever y puertas, en este caso para acceder a la runa:

Aparte, recordando lo que hablaron @AikonCWD y @Sokar92 para el devlog de este último sobre el Coyote Time, he implementado uno. Vendría a ser algo así:

    private int maxCoyoteFrames = 50;
    private int actualCoyoteFrames = 0;
    private bool isJumping, alreadyJumped = false;

private void SetCoyoteTime() // Se llama en cada Update
{
    if (isJumping)
    {
        actualCoyoteFrames++;
    }        
    else
    {
        actualCoyoteFrames = 0;
    }
}

private bool CheckCoyoteTime() // Se llama al intentar saltar
{
    return actualCoyoteFrames < maxCoyoteFrames;
}

EDIT: Y acabo de darme cuenta de que mi forma de implementar Coyote Time no está como es debido, ya que alguien con pocos FPS tendría mucha ventaja. Le voy a dar unas vueltas a ver qué se me ocurre...

n3krO

Si lo llamas en FixedUpdate solucionas el problema, porque corre a 50 fps el FixedUpdate, no?

2 respuestas
B

#18 A ver, que me corrijan si me equivoco pero no era que el Update salta 1 vez por frame y el FixedUpdate puede saltar varias veces o ninguna dependiendo de varias cosas, el framerate entre ellas. Por lo que ninguna de las dos me sirve.

@totespare @kesada7 ¿Nos sacáis de duda? xD

n3krO

Por lo que entendi el FixedUpdate corre a 50 hz (se llama 50 veces por segundo) en intervalos lo mas regulares posibles (ya que Windows no es un sistema de RT no pueden asegurar que es el intervalos fijos de 20 ms xDDDD

Mientras que el Update es lo que se llama continuamente asi que lo que tarde el Update en ejecutarse decide el frametime (a no ser que tengas cosas que limiten los fps)

Si el fixed va a 50 hz, y tienes el juego a 60 fps, obviamente habrá, a veces, 2 frames consecutivos que no tengan actualizacion del FixedUpdate, pero es la unica forma de calcular las cosas con contadores en vez de usar delta time.

Igualmente que nos saquen de dudas los entendidos que yo no soy ni programador, cuanto mas gamedev xDDDD

1 respuesta
B

#20 Tiene mucho sentido lo que dices, lo estoy probando y funciona. Todavía me lío con estas cosas, enseguida entendí que llamarlo en Update me daría problemas, pero luego ya me lié. Además el valor que tenia de Coyote Frames ya me parecía demasiado alto (50) ahora usándolo en FixedUpdate lo tengo a 5, porque más es una chetada xD

A ver si nos contestan los experts, pero tiene pinta de que tenías razón. Ya te debo dos cafés!

1 respuesta
totespare

Pero qué feature es el CoyoteTime? xD

2 respuestas
B

#22 No sabes que soy mallorquín, no sabes lo que es el Coyote Time... Te me estás cayendo tote!

Lo del coyote time son esos frames extra que tienes para saltar al haber salido de la plataforma.

P.D: Ojo, que yo lo aprendí el otro día xDD

2 respuestas
n3krO

#22 El nombre es bastante explicativo para los milenials. Lo que te deja como boomer o Y.

Prefieres ser viejo o ratkid?

#23 Palmerino.

1 respuesta
totespare

#23 #24 he visto los dibujos pero vamos, esos eran de TVE, a penas los veía xD. Soy milenial creo, soy del 90 xDD

Por qué no haces una corrutina que se ejecute al dejar el collider del suelo, le metes un WaitForFrames(50), y despues le pones el flag de que ya no está tocando el suelo?

1 respuesta
B

#25 Pensé en una corrutina, pero luego lo hice como lo tengo. Lo he probado jugando con Application.targetFrameRate y funciona como es debido.

Lo primero que se me ocurrio fue hacer más grande el collider de los pies con el que compruebo el grounding del PJ y más o menos funcionaba. Pero también permitia wall jumping. Si es que cuando uno es vago... xD

1 respuesta
B

Esto es lo último que he añadido:

Un fundido de entrada para cuando se inicia la escena:

Y el primer y, supongo, único NPC que tendrá el juego. El Rune Master:

1
totespare

#26 pero no creo que te puedas fiar del Application.targetFrameRate 100%, que yo sepa no fuerza los fps, solo intenta llegar a ellos xD. Qué problema hay con la corrutina? Pruebalooo cabezón xD

1 respuesta
B

#28 No, problema ninguno, pero si ya tengo un cachocódigo que me funciona no le voy a dar más vueltas!

Por cierto, mira el último gif, el del rune master. El diálogo es el siguiente:

  • Hola Rune Master.
  • Hola, no sabía que eras mallorquín ni lo del coyote time.
  • Adiós mundo cruel...

Todavía no tengo sistema de diálogos, sino lo hubiera puesto xDD

1 respuesta
totespare

#29 lo mio son 3 lineas literal :(

IEnumerator CoyoteTime()
{
yield return new WaitForFrames(50);
flagDeTocarElSuelo = false;
}

Y ojo que no sólo te suicidas. también te abres de piernas para que entren mejor los pinchos! Jajaja

1 2 respuestas