Noticias:

Los cartagineses se acercan a Roma. En respuesta, los romanos se meten en sus casas al grito de: "No estamos". Seguro que Aníbal no se lo traga y entra en la ciudad a saco.

Menú Principal

Curso de Programación Lúdica. Actualmente: Tetris funcionando

Iniciado por Bill, 13 de Mayo de 2009, 15:08

0 Miembros y 1 Visitante están viendo este tema.

Bill

El código fuente, imágenes y compilado subido a megaupload por cortesía de Clave:

http://www.megaupload.com/?d=TU5T0B6C

Lo siguiente que vamos a hacer va a ser poner un personaje que se mueva, con animación para simular los pasos.

Bill

4. Añadiendo un personaje a la escena.

Para este capítulo meteremos un personaje con animación. Como el scroll lo estamos haciendo en ambas direcciones, meteremos dos animaciones del personaje: una caminando hacia la izquierda y otra caminando hacia la derecha. En este caso las siguientes:




Ambos son pngs con transparencia que contienen una animación en 10 frames del personaje caminando, cada png en un sentido. Cada frame ocupa un ancho de 60px, aquí es dónde los diseñadores gráficos tienen el papelón gordo de diseñar un personaje, sus posibles movimientos, gestos, etc. y guardarlos bien guardaditos en pngs de forma que se pueda extraer un frame en concreto de ellos sabiendo el cuadrado que debemos cortar mediante fórmulas matemáticas. En nuestro caso tendremos una animación del frame 0 al frame 9 en la que el corte de cada frame se iniciará en la x = 60*frame con un ancho de 60px.

Comenzamos definiendo los campos que utilizaremos y cargando los dos pngs:

Citarprivate Bitmap btmChar1WalkR = new Bitmap("Char1_walk_right.png");
        private Bitmap btmChar1WalkL = new Bitmap("Char1_walk_left.png");
        private Bitmap btmActualChar;
        private int widthCharFrame = 60;
        private int numFramesChar;
        private int actualCharFrame;

Como en el punto anterior, estamos cargando las dos animaciones en bitmaps llamados btmChar1WalkR y btmChar1WalkL, definimos un campo btmActualChar que indicará cual de los dos estamos utilizando en este momento para saber de cual de los dos pintar según la dirección hacia la que se vaya, widthCharFrame indica el ancho en píxeles de cada frame dentro del png, numFramesChar se calculará en función del ancho de los pngs y el ancho de cada frame, y actualCharFrame nos indica el número de frame en el que nos encontramos ahora mismo, que irá desde 0 hasta numFramesChar-1.

Hacemos las inicializaciones en el constructor de Form1 de acuerdo con esto último que hemos dicho, inicializando la dirección del personaje hacia la derecha:

CitarnumFramesChar = btmChar1WalkR.Width / widthCharFrame;
            actualCharFrame = 0;
            btmActualChar = btmChar1WalkR;

Igual que tenemos un método para pintar una capa definiremos un método para pintar un frame del personaje, en el que utilizaremos un rectángulo para cortar el trozo que nos interesa del png del personaje y otro rectángulo para saber dónde ponerlo:

Citarprivate void PaintChar(Graphics graphics, Bitmap layer)
        {
            Rectangle layerRect = new Rectangle(actualCharFrame * widthCharFrame, 0, widthCharFrame, layer.Height);
            Rectangle outputRect = new Rectangle(60, viewRect.Height-layerRect.Height-60, layerRect.Width, layerRect.Height);
            graphics.DrawImage(layer, outputRect, layerRect, GraphicsUnit.Pixel);
        }

El primer rectángulo, el de la capa, se inicia en la x = frameActual*anchoDeFrame, con y=0, con ancho = anchoDeFrame y con altura el del png.
El segundo rectángulo es dónde pintamos, y hacemos que sea en la x = 60 (desplazarlo un poco desde el borde), calculamos una y adecuada con respecto al alto de la vista menos el alto del personaje y menos un poco para que se vea bien en el suelo,y el ancho y alto será el del trozo que cortamos del personaje, es decir, el de un frame, y dibujamos igual que hacíamos con la capa.

En el evento Paint del Form1, donde pintamos las capas, al final añadimos que pinte al personaje:

CitarPaintChar(e.Graphics, btmActualChar);

Ahora vamos a por las teclas. Ahora cuando se pulsa la tecla izquierda, si el personaje no mira hacia la izqueirda tenemos que hacer que mire, es decir, asignar btmActualChar = btmChar1WalkL. Recíproco para la derecha. Además debemos incrementar el número de frame y si nos pasamos regresar al frame 0 para hacer el loop de la animación:

Citarprivate bool ProcessKeyDown(Keys keyCode)
        {
            switch (keyCode)
            {
                case Keys.Left:
                    {
                        if (actualX > 5)
                        {
                            if (btmActualChar != btmChar1WalkL)
                            {
                                btmActualChar = btmChar1WalkL;
                            }
                            actualCharFrame++;
                            if (actualCharFrame >= numFramesChar)
                            {
                                actualCharFrame = 0;
                            }
                            actualX -= 5;
                            Invalidate();
                        }
                        return true;
                    }
                case Keys.Right:
                    {
                        if (actualX < referenceRect.Width - viewRect.Width-5)
                        {
                            if (btmActualChar != btmChar1WalkR)
                            {
                                btmActualChar = btmChar1WalkR;
                            }
                            actualCharFrame++;
                            if (actualCharFrame >= numFramesChar)
                            {
                                actualCharFrame = 0;
                            }
                            actualX += 5;
                            Invalidate();
                        }
                        return true;
                    }
            }
            return false;
        }

Y ya estaría. Es una animación muy primitiva debido a dos motivos:
1) No estamos comprobando los ciclos de procesador ni FPS, así que va a su bola.
2) No soy diseñador gráfico, así que el sprite no es todo lo bueno que debería. Este siempre será el mayor problema: los programadores no son lo mismo que los diseñadores gráficos, y un juego es un gran tanto por ciento visual.



Código, gráficos y ejecutable de esta lección:

http://www.megaupload.com/?d=V5605202

neoprogram





たとえばここには愛すべき声があってあたしはそれすた守れなくて

Thylzos

¡Ah!, ¡mola!, ¡sigue!

A ver si encuentro el disco de mi máquina virtual.

Gracias freyi *.*


Cita de: Gambit en 26 de Enero de 2010, 10:25
Follar cansa. Comprad una xbox 360, nunca le duele la cabeza, no discute, no hay que entenderla, la puedes compartir con tus amigos...

Clave

Que mamonazo por culpa de eso me ha entrado curiosidad xD

Voy a volver a intentarlo en tener un segundo fale? *.*
[spoiler=Citas][23:59:06] petyr dice: A mí el olor a porro me marea mucho[0:33:39] rayd dice: que yo tuve novia
Cita de: Ningüino Flarlarlar en 12 de Agosto de 2011, 12:08Felicidades, Logan. Ya no tendré que darme prisa para contestarte los sms.
Cita de: Rayd en 04 de Octubre de 2011, 16:00
Cita de: Clave en 04 de Octubre de 2011, 15:57
Cita de: ayrendor en 04 de Octubre de 2011, 15:51
Cita de: Clave en 04 de Octubre de 2011, 15:37
Cita de: ayrendor en 04 de Octubre de 2011, 15:29
¿Pero cuando se acaba el plazo para mandar? :facepalm:
Hay un mp para ti.
No lo hay.
Ahora si
Me lo has mandado a mi.

Bill

5. Dando más vida al personaje (I).

En este capítulo vamos a tratar de hacer un personaje con algo más de vida. En lugar de hacer un programa de scroll horizontal vamos a hacer lo que sería un plataformas, pero en habitación negra, es decir, solamente estará el personaje y un fondo negro. En el siguiente, que complementará a este, meteremos un poquito de los conceptos de tiro parabólico, inercia y velocidad. Vamos, que va a ser completita.

De nuevo, creamos una solución con un proyecto de aplicación Windows Forms, y la llamamos DarkRoom. Al formulario que nos crea por defecto le cambiamos el texto a "Dark Room" y el tamaño a 800;600.

Vamos a utilizar el siguiente fondo:
http://img140.imageshack.us/img140/6480/backgroundps.png

Ahora de nuevo cargar el gráfico e inicializar los rects básicos, con lo que la clase Form1 quedaría así:

Citarpublic partial class Form1 : Form
    {
        private Bitmap btmBackground = new Bitmap("background.png");
        private Rectangle viewRect;
        private Rectangle bgRect;

        public Form1()
        {
            InitializeComponent();
            viewRect = new Rectangle(0, 0, Width, Height);
            bgRect = new Rectangle(0, 0, btmBackground.Width, btmBackground.Height);
        }

        private void Form1_Paint(object sender, PaintEventArgs e)
        {
            PaintBackground(e.Graphics);   
        }

        private void PaintBackground(Graphics graphics)
        {
            graphics.DrawImage(btmBackground, viewRect, bgRect, GraphicsUnit.Pixel);
        }
    }

Ahora es cuando llega la labor del diseñador gráfico. En este caso escoger un personaje, dibujarlo y hacer el png, como dijimos, con transparencia, y de forma que podamos saber con alguna fórmula qué frame coger en cada momento. Para hacer un juego, el personaje tiene montones de acciones que puede realizar o estados, y para cada acción o estado tiene varios frames, y termina por convertirse en un pequeño infierno  :gñe:

Vamos a poner un ejemplo, os presento a Lucas, y Lucas puede hacer todo esto:

http://img10.imageshack.us/img10/9651/lucaskl.png

Ahí podemos ver que Lucas puede hacer muchas cosas, que el png no es transparente, que no cumple unas distancias fijas sino variables, y que además contiene errores en el sentido de que hay frames que pertenecen a otro personaje. Os paso también al otro personaje:

http://img255.imageshack.us/img255/2130/nessi.png

A partir del png que tenemos de lucas vamos a hacer un png nuevo que contenga solamente las acciones que queremos, cada fila será una acción diferente, y que cumpla las distancias y la transparencia para que sea más sencillo de tratar. Vamos a escoger tres de las acciones: old stance, run y jump. Como veis ninguno tiene el mismo número de frames, y además en jump el último frame sobra porque es el mismo que el primero. El Jump es el que tiene más frames, tiene 9, así que tendremos una rejilla de 9x3, con altura 48px y ancho 40px. Así que la imagen resultante será de 360x144. Así que con photoshop creamos la imagen, fondo transparente, le creamos una rejilla de referencia para diferenciar los rectángulos en los que se sitúa cada frame, y llevamos los frames para cada casilla. Al final guardamos el png, sin la rejilla de referencia. Así es como lo veríamos en el photoshop, con un fondo añadido en negro para saber cómo quedará en el resultado, y la rejilla:



Y aquí está el resultado guardado a png sin la rejilla y con fondo transparente:



Ahora vamos a hacer algo diferente a lo de la última vez: el personaje va a tener su propia clase. Es decir, la funcionalidad del personaje no estará en el formulario, sino que será independiente. Damos click con el botón derecho del ratón en el proyecto y escogemos Agregar -> Clase y la llamamos Character.cs. Eso nos creará automáticamente código con el namespace del formulario y una clase llamada Character. Cambiamos la visibilidad de la clase a pública añadiendo la palabra clave public antes de la definición de la clase:

Citarpublic class Character
    {
    }

En esta clase definimos el campo Bitmap en el que vamos a cargar su correspondiente png, y lo llamamos btmCharacter. Pero no lo vamos a cargar a las bravas, sino que haremos que el personaje pueda cargar diferentes pngs, así la misma clase nos servirá para diferentes pngs de personaje siempre y cuando sus pngs sean compatibles. Cuando añadimos el campo nos dice que no encuentra la clase Bitmap, porque nos falta poner el using de System.Drawing, sin embargo si te colocas en la palabra Bitmap sale un cursor con una notita que si la pinchas te sugiere agregarla automáticamente.

Creamos un método LoadBitmap que acepte como parámetro un nombre de fichero y lo que haga sea crear un nuevo bitmap con ese fichero desechando el anterior. También creamos un constructor de la clase que acepte como parámetro el nombre de fichero inicial e invoque a LoadBitmap:

Citarpublic class Character
    {
        private Bitmap btmCharacter = null;

        public Character(string pngName)
        {
            LoadBitmap(pngName);
        }

        public void LoadBitmap(string pngName)
        {
            btmCharacter = new Bitmap(pngName);
        }
    }

Añadimos otra clase y la llamamos KeyController. Esta clase nos servirá para llevar el control de las teclas pulsadas y su estado, dado que vamos a controllar si hay más de una tecla pulsada en un determinado instante. En este KeyController, por ahora, llevaremos control de únicamente 4 teclas, correspondientes a las direccionales. Definimos un tipo enumerado público para dichas 4 teclas fuera de la clase y un campo privado de tipo array booleano y cardinalidad 4, inicializando todos los valores a false, que será el que indique las teclas pulsadas. De este último lo encapsulamos a una propiedad pública de solo lectura. Además añadimos dos métodos para reaccionar al KeyDown y al KeyUp que serán invocados desde la ventana principal:

Citarpublic enum DirectionKeys
    {
        up,
        down,
        left,
        right
    }

    public class KeyController
    {
        private bool[] keysPressed = new bool[4] { false, false, false, false };

        public bool[] KeysPressed
        {
            get { return keysPressed; }
        }

        public bool KeyDown(Keys key)
        {
            switch (key)
            {
                case Keys.Up:
                    keysPressed[(int)DirectionKeys.up] = true;
                    return true;
                case Keys.Down:
                    keysPressed[(int)DirectionKeys.down] = true;
                    return true;
                case Keys.Left:
                    keysPressed[(int)DirectionKeys.left] = true;
                    return true;
                case Keys.Right:
                    keysPressed[(int)DirectionKeys.right] = true;
                    return true;
            }
            return false;
        }

        public bool KeyUp(Keys key)
        {
            switch (key)
            {
                case Keys.Up:
                    keysPressed[(int)DirectionKeys.up] = false;
                    return true;
                case Keys.Down:
                    keysPressed[(int)DirectionKeys.down] = false;
                    return true;
                case Keys.Left:
                    keysPressed[(int)DirectionKeys.left] = false;
                    return true;
                case Keys.Right:
                    keysPressed[(int)DirectionKeys.right] = false;
                    return true;
            }
            return false;
        }
    }

Hay que entender que si bien no es la forma más rápida de hacer las cosas, sí es la más ordenada y reutilizable. Así estamos separando la zona de interfaz con el usuario, la reacción al Input y el personaje. Los enemigos y objetos, por ejemplo, también llevarían su propia clase. Además en el form añadimos un campo que sea una instancia de este keycontroller y añadimos los eventos de respuesta a KeyDown y KeyUp:

Citarprivate KeyController keyController = new KeyController();

Citarprivate void Form1_KeyDown(object sender, KeyEventArgs e)
        {
            e.Handled = keyController.KeyDown(e.KeyCode);
        }

        private void Form1_KeyUp(object sender, KeyEventArgs e)
        {
            e.Handled = keyController.KeyUp(e.KeyCode);
        }

Volvemos al personaje. Hay que añadirle campos para llevar el control de varias cosas:
- El rectángulo definitorio de un frame, que será (0,0,40,48).
- Un enumerado para los diferentes estados en los que puede estar el personaje
- Un array de enteros que nos dirá los frames que contiene cada estado
- El estado actual del personaje
- El número de frame actual del personaje dentro de su estado
- Un rectángulo que lleve el control de las dimensiones del mundo en el que se puede mover el personaje
- La posición x del personaje
- La posición y del personaje (altura para cuando salta)

Citarprivate enum CharState
        {
            stand,
            run,
            jump
        }
        private Rectangle charRect = new Rectangle(0, 0, 40, 48);
        private int[] stateFrames = new int[3] { 4, 8, 9 };
        private CharState state = CharState.stand;
        private int actualFrameNumber = 0;
        private Rectangle worldRect;
        private int x = 0;
        private int y = 0;

Hay que modificar su constructor para que acepte como parámetro el rectángulo de las dimensiones del mundo:

Citarpublic Character(string pngName, Rectangle worldRect)
        {
            this.worldRect = worldRect;
            LoadBitmap(pngName);
        }

La cláusula "this" sirve para indicar que se trata de la clase. Es útil para casos como este: el parámetro que se pasa al constructor se llama exactamente igual que un campo de la clase.

Ahora hay que hacer que en la instanciación del personaje se le pase un rectángulo con las dimensiones del mundo:

CitarcharLucas = new Character("lucas.png", new Rectangle(0,0,800,426));

Además hay que añadirle un método privado para pasar al siguiente frame, un método para coger el Bitmap del frame actual y que además avanza al siguiente frame, y por último un método que dado unos gráficos, pinte al personaje:

Citarprivate void NextFrame()
        {
            actualFrameNumber++;
            if (actualFrameNumber >= stateFrames[(int)state])
            {
                actualFrameNumber = 0;
            }
        }

        private Bitmap ActualBitmap()
        {
            Bitmap result = new Bitmap(charRect.Width, charRect.Height);
            Rectangle srcRect = new Rectangle(actualFrameNumber * charRect.Width, (int)state * charRect.Height, charRect.Width, charRect.Height);
            Graphics g = Graphics.FromImage(result);
            g.DrawImage(btmCharacter, charRect, srcRect, GraphicsUnit.Pixel);
            NextFrame();
            return result;
        }

        public void Paint(Graphics g)
        {
            Bitmap actualBitmap = ActualBitmap();
            Rectangle destRect = new Rectangle(x, worldRect.Height-actualBitmap.Height-y, charRect.Width, charRect.Height);
            g.DrawImage(actualBitmap, destRect, charRect, GraphicsUnit.Pixel);
        }

En el paint del form1, dónde pintábamos el background, hay que hacer que además pinte al personaje:

Citarprivate void Form1_Paint(object sender, PaintEventArgs e)
        {
            PaintBackground(e.Graphics);
            charLucas.Paint(e.Graphics);
        }

Ahora mismo, si ejecutamos, ya veremos a nuestro lucas situado al principio del todo:



Pero está muerto  :'(

Ahora es cuando entra el concepto de tiempo. En el capítulo anterior repintábamos a petición del usuario cuando pulsaba una tecla. Ahora no lo vamos a hacer así. Vamos a meter una instancia de una clase llamada Timer que nos permite definir un intervalo de tiempo y que al habilitarlo nos llame a métodos que estén suscritos a su evento Tick. En el Form1 definimos el timer:

Citarprivate Timer frameTimer;

Añadimos un método DoOnFrameTimer que será el que se dispare. Ojo, los parámetros tienen que ser los que son porque son los definidos en el delegado de Timer.Tick. En cada tick lo que hacemos es pintar de nuevo la escena.

Citarprivate void DoOnFrameTimer(object sender, EventArgs e)
        {
            Invalidate();
        }

Ahora es cuando tenemos que crear el timer y darle un intervalo, y llega el concepto de FPS. Según el tiempo que definamos tendrá más FPS, aunque todavía no llevaremos un control de repintado según demanda de procesador que sí tendremos cuando veamos XNA. En este caso vamos a definir 20 FPS, como un segundo tiene 1000 ms entonces el timer se tiene que llamar cada 1000/20 = 50ms:

CitarframeTimer = new Timer();
            frameTimer.Interval = 50;
            frameTimer.Tick += DoOnFrameTimer;
            frameTimer.Enabled = false;

Ahora ya tendríamos a Lucas haciendo cosas, estando vivo. Como además queremos comprobar el resto de estados, añadimos el siguiente método de test a la clase Character:

Citarpublic void NextState()
        {
            if ((int)state < 2)
            {
                state++;
            }
            else
            {
                state = 0;
            }
        }

Y en el evento KeyUp del Form1 añadimos que se llame al NextState de charLucas:

Citarprivate void Form1_KeyUp(object sender, KeyEventArgs e)
        {
            e.Handled = keyController.KeyUp(e.KeyCode);
            charLucas.NextState();
        }

Ahora cuando pulsamos una tecla cualqueira lucas cambia a su siguiente estado, así podremos ver la animación de stand, run y jump.




Adjunto el código fuente, en la carpeta bin/Debug encontraréis además un ejecutable generado, los gráficos necesarios y el psd del personaje por si queréis tocar.

http://www.megaupload.com/?d=8TF32SM7



Bill

Avisados estais de que si este era "duro", en el siguiente entran lo conceptos de vector velocidad, inercia, tiro parabólico... si no os gusta la matemática o física ni un poquito, abandonad ahora  :amo:

O preguntad mucho, que contesto  :gñe:

Khram Cuervo Errante

#257
Joder, me encantaría hacer una movida de estas para una conferencia que tengo que dar, pero no tengo tiempo de programar nada...

Pelu, no los asustes tanto, que esa física es de la facilita...

Sorry but you are not allowed to view spoiler contents.

Bill

Un pequeño anexo para "juguetear". Dado que tenemos el personaje en una clase, se puede instanciar varias veces, así que hay un pequeño ejercicio divertido que se puede hacer, y es que salgan varios luquitas haciendo lo mismo por la pantalla. Para ello modificamos el constructor del personaje para que acepte una x, que será la posición de ese lucas:

Citarpublic Character(string pngName, Rectangle worldRect,int x)
        {
            this.worldRect = worldRect;
            LoadBitmap(pngName);
            this.x = x;
        }

En el Form1 cambiamos la declaración del Character por un array de 10 elementos:

Citarprivate Character[] charLucas = new Character[10];

En la inicialización del Character hacemos que se pase una x apropiada dentro de un for:

Citarfor (int i = 0; i<10; i++)
            {
                charLucas = new Character("lucas.png", new Rectangle(0, 0, 800, 426),i*80);
            }

El for tiene 3 parámetros: inicialización, condición e incremento. La inicialización es para iniciar la variable del for, que hemos declarado ahí mismo, así que comienza en i=0. La condición es lo que se debe cumplir para continuar ejecutando el for, en este caso que i sea menor que 10 para que recorra de 0 hasta 9. Y por último el incremento que se hace cada vez que se ejecuta una vuelta del for, en este caso i++ que significa i=i+1.

En todos los sitios en los que usábamos charLucas hay que sustituirlo ahora por el for haciendo la acción que se llevaba a cabo:

Citarprivate void Form1_Paint(object sender, PaintEventArgs e)
        {
            PaintBackground(e.Graphics);
            for (int i = 0; i < 10; i++)
            {
                charLucas.Paint(e.Graphics);
            }
        }

Citarprivate void Form1_KeyUp(object sender, KeyEventArgs e)
        {
            e.Handled = keyController.KeyUp(e.KeyCode);
            for (int i = 0; i < 10; i++)
            {
                charLucas.NextState();
            }
        }

Y aquí el resultado: 10 luquitas haciendo lo mismo



¿Alguien es capaz de hacer que aparezcan 3 filas de luquitas?  :gñe:


Bill

6. Dando más vida al personaje (II).

Lo primero que vamos a hacer es modificar el tipo de los campos x e y de la clase Character para que en lugar de enteros sean double (un número real, con sus decimales). ¿Por qué? Porque cuando se trabaja con una física de un juego, aunque sea inventada, normalmente termina habiendo incrementos y localizaciones no enteras. En nuestro caso sí lo van a ser porque la física que vamos a implementar en el juego es terriblemente sencilla, y con números enteros, pero más vale prevenir... Con lo cual no solamente modificamos la declaración sino que a la hora de pintar el bitmap tendremos que hacerles un casteo a entero. De esta forma la declaración es:

Citarprivate double x = 0;
       private double y = 0;

Y el pintado:

Citarpublic void Paint(Graphics g)
       {
           Bitmap actualBitmap = ActualBitmap();
           Rectangle destRect = new Rectangle((int)x, worldRect.Height-actualBitmap.Height-(int)y, charRect.Width, charRect.Height);
           g.DrawImage(actualBitmap, destRect, charRect, GraphicsUnit.Pixel);
       }

Definimos un vector para la velocidad, al ser un mundo plano este vector será de dos dimensiones representando a x e y:

Citarprivate double[] speedVector = new double[2]{0.0f, 0.0f};

Lo de 0.0f es la manera de expresar que es 0.0 decimal (float), podríamos haber puesto un 0 normal.

Necesitaremos dos métodos auxiliares, uno para incrementar la velocidad que aceptará en qué dimensión aumentamos la velocidad (si es para correr, la x que es el 0, si es para saltar la y que es el 1) y el incremento de la velocidad en dicha dimensión. El segundo método será para relajar la velocidad en el eje x. Además, definimos una constante para la velocidad máxima, que será de 12 píxeles por frame:

Citarconst double maxSpeed = 12.0f;

El método de incrementar la velocidad sería:

Citarprivate void IncreaseSpeed(int i, double k)
       {
           speedVector += k;
           if (speedVector > maxSpeed)
           {
               speedVector = maxSpeed;
           }
           else if (speedVector < -maxSpeed)
           {
               speedVector = -maxSpeed;
           }
       }

Y el de relajar la velocidad (hace que la velocidad tienda a 0, para la inercia):

Citarprivate void RelaxSpeed(double k)
       {
               if (speedVector[0] < 0)
               {
                   speedVector[0] += k;
                   if ((speedVector[0]) > 0)
                   {
                       speedVector[0] = 0.0f;
                   }
               }
               else if (speedVector[0] > 0)
               {
                   speedVector[0] -= k;
                   if ((speedVector[0]) > 0)
                   {
                       speedVector[0] = 0.0f;
                   }
               }
       }

Ahora definimos un método Move que será el encargado de calcular las nuevas coordenadas de nuestro personaje con respecto a su velocidad y posición, además de calcular en qué estado está y pasar de frame:

Citarprivate void Move()
       {
           x = x + speedVector[0];
           if (x < 0)
           {
               x = 0;
               speedVector[0] = 0.0f;
           }
           else if (x > worldRect.Width - charRect.Width-10)
           {
               x = worldRect.Width - charRect.Width-10;
               speedVector[0] = 0.0f;
           }
           y = y + speedVector[1];
           if (y < 0)
           {
               y = 0;
               speedVector[1] = 0.0f;
           }
           else if (y > worldRect.Height - charRect.Height)
           {
               y = worldRect.Height - charRect.Height;
               speedVector[1] = 0.0f;
           }
           if (y > 0)
           {
               state = CharState.jump;
           }
           else if (speedVector[0] != 0)
           {
               state = CharState.run;
           }
           else
           {
               state = CharState.stand;
           }
           NextFrame();
       }

Como vemos hay una comprobación de que no se salga de los límites del mundo, considera que el personaje está saltando si y>0, si no está saltando y tiene velocidad en las x entonces está corriendo, y si no está saltando ni corriendo es que está parado.

Y ahora necesitamos un método para procesar el input del teclado, calcular el siguiente movimiento y pintar el personaje, que se llamará ProcessMovement. Este método debe hacer varias cosas:
- Si la y>0 entonces frenar la velocidad en Y en 1 punto.
- Si están pulsando la tecla hacia arriba y no la de abajo, y el personaje está en el suelo, aumentar la velocidad en Y en 12 puntos para representar el impulso inicial.
- Si están pulsando la tecla derecha y no la izquierda incrementa su velocidad en 1 punto y mira hacia la derecha
- Si están pulsando la tecla izquierda y no la derecha decrementa su velocidad en 1 punto y mira hacia la izquierda
- Si no pulsan ni derecha ni izquierda por separado, relaja la velocidad.
- Realiza el movimiento
- Pinta.

Citarpublic void ProcessMovement(KeyController controller, Graphics g)
       {
           if (y > 0)
           {
               IncreaseSpeed(1, -1);
           }
           if ((controller.UpPressed) && (!controller.DownPressed) && (y == 0))
           {
               IncreaseSpeed(1, 12);
           }
           if ((controller.RightPressed) && (!controller.LeftPressed))
           {
               IncreaseSpeed(0, 1);
               direction = CharDirection.right;
           }
           else if ((!controller.RightPressed) && (controller.LeftPressed))
           {
               IncreaseSpeed(0, -1);
               direction = CharDirection.left;
           }
           else
           {
               RelaxSpeed(1);
           }
           Move();
           Paint(g);
       }

Cambiamos el evento Paint de Form1 para invocar a este nuevo método en lugar del Paint (al cual cambiamos la visibilidad a privada):

Citarprivate void Form1_Paint(object sender, PaintEventArgs e)
       {
           PaintBackground(e.Graphics);
           charLucas.ProcessMovement(keyController, e.Graphics);
       }

Y nos queda un último toque mágico: si el personaje mira hacia la izquierda hay que hacer un flip horizontal del bitmap, y eso se hace en ActualBitmap() con el método RotateFlip de la clase Bitmap:

Citarprivate Bitmap ActualBitmap()
       {
           Bitmap result = new Bitmap(charRect.Width, charRect.Height);
           Rectangle srcRect = new Rectangle(actualFrameNumber * charRect.Width, (int)state * charRect.Height, charRect.Width, charRect.Height);
           Graphics g = Graphics.FromImage(result);
           g.DrawImage(btmCharacter, charRect, srcRect, GraphicsUnit.Pixel);
           if (direction != CharDirection.right)
           {
               result.RotateFlip(RotateFlipType.RotateNoneFlipX);
           }
           return result;
       }

El resultado es que nuestro muñeco ya corre, salta, se para, y tiene una física que aunque no es real es la lógica para un juego de plataformas. Os recomiendo juguetear con el código y hacerle perrerías, como por ejemplo probar a implementar un doble salto o un efecto kirby.



Código con gráficos y ejecutable:

http://www.megaupload.com/?d=WO7WXJXA



Últimos mensajes

Vuelven los gatos de YoYo
[Hoy a las 01:29]


Qué hago aquí? de Lector
[Hoy a las 01:28]


Errores del foro de jug0n
[Hoy a las 01:28]


[Follipandi del 106]Hilo para poner al corriente a Clave de jug0n
[Hoy a las 01:16]


Por que 106? De donde viene? de YoYo
[Hoy a las 01:14]