Noticias:

Encuentran a Wally en Tombuctú: "Me fui a por tabaco", declaró el aparecido. Se rumorea que Lobatón le hará la primera entrevista.

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.

Genki

pues tiene toda la pinta que no... gambit hace meses que no se pasa por aquí

Sorry but you are not allowed to view spoiler contents.

Crosher

¿Y nadie que le conozca puede llamarle?
o alguien hacerse cargo?
Es que he empezado con muchas ganas y me acabo de dar cuenta que está parado...
Cita de: Ningüino Flarlarlar en 17 de Enero de 2019, 16:39
Crosher es la nueva sensación del foro.

Sorry but you are not allowed to view spoiler contents.

Cita de: Deke en 14 de Junio de 2011, 00:08
Es como si te empeñases en jugar al Twister siendo daltónico.

www.latragaperras.blogspot.es

Orestes

Pelu abandonó el foro. La última vez que hablé con él no tenía intención de volver. Despídete del curso.

Sorry but you are not allowed to view spoiler contents.

Crosher

#283
Pues me quedo con el tetris...  :(
Cita de: Ningüino Flarlarlar en 17 de Enero de 2019, 16:39
Crosher es la nueva sensación del foro.

Sorry but you are not allowed to view spoiler contents.

Cita de: Deke en 14 de Junio de 2011, 00:08
Es como si te empeñases en jugar al Twister siendo daltónico.

www.latragaperras.blogspot.es

Bill

Como megaupload ha petado un poco, he perdido todo el código. Pero he vuelto a hacer el Tetris hasta dónde se había quedado la última lección, como también se habían perdido las imágenes de los cubos las he hecho en tres patadas con el paint, aunque no se ven igual, suficiente es.

El código hasta la última lección, completo, en dropbox:

http://dl.dropbox.com/u/11320362/Cientris.zip

Continuaré con la forma de añadir el contador de puntos y los sonidos. Y pasaremos a otro tipo de juego.

raul_isl

Y yo pensando que esto habia muerto!
Siempre he tenido la ilusion de intentarlo, pero como te piraste pues... :(
Y tampoco tengo el tiempo suficiente. A ver si algun dia consigo un trabajo con horario de personas y puedo ponerme.

Index

Ahí te veo. Yo también ando ocupadillo gran parte del tiempo, pero una hora diaría mínimo puedo dedicarle sin problemas, a base de constancia tiramos. Voy a volver con el tetris desde el principio mientras vas haciendo lo nuevo.

Cita de: Yarot en 15 de Febrero de 2009, 19:33
Pues espero que el chauvinismo sea algo rico y masticable porque un día de estos igual te lo comes a hostias.

Bill

12. Creando un Tetris: Música y sonidos

[Introducción]
Lo primero es identificar lo que queremos hacer. Queremos poner una música de fondo mientras se está jugando, y además los siguientes sonidos:
- Cuando cae la pieza.
- Cuando se realiza línea.
- Cuando se pierde la partida.

Hacerlo con .wav es muy sencillo, pero vamos a hacerlo con sonidos .mp3 y .wma para ver cómo añadir clases que están en componentes COM. En este caso queremos añadir el componente COM del Windows Media Player, y utilizarlo. Para eso hay que ir a referencias y agregar una nueva referencia, y pinchar en la pestaña COM. Veremos que hay cientos de ellos. Vamos directamente a Windows Media Player y seleccionamos el que es wmp.dll:



Ahora añadimos a resources los recursos musicales. En este caso en lugar de recursos embebidos, los marcaremos como Content, y marcaremos la opción "Copy if newer".



Y por último creamos la clase TetrisSounds para ayudarnos a utilizarlos:


using System.IO;
using System.Windows.Forms;
using WMPLib;

namespace Tetris
{
    public class TetrisSounds
    {
        #region - Fields -

        private WindowsMediaPlayer backgroundPlayer = new WindowsMediaPlayer();

        private WindowsMediaPlayer eventPlayer = new WindowsMediaPlayer();

        #endregion

        #region - Constructor -

        public TetrisSounds()
        {
            this.backgroundPlayer.URL = Path.GetDirectoryName(Application.ExecutablePath) + "\\Resources\\theme_song.wma";
            this.backgroundPlayer.settings.setMode("loop", true);
            this.backgroundPlayer.controls.stop();
        }

        #endregion

        #region - Methods -

        public void StartBackground()
        {
            this.backgroundPlayer.controls.play();
        }

        public void StopBackground()
        {
            this.backgroundPlayer.controls.stop();
        }

        public void PlaySound(string fileName)
        {
            this.eventPlayer.URL = fileName;
            this.eventPlayer.controls.play();
        }

        public void PlayDrop()
        {
            PlaySound(Path.GetDirectoryName(Application.ExecutablePath) + "\\Resources\\splat3a.wma");
        }

        public void PlayLinesRemove()
        {
            PlaySound(Path.GetDirectoryName(Application.ExecutablePath) + "\\Resources\\explosion.mp3");
        }

        public void PlayLost()
        {
            PlaySound(Path.GetDirectoryName(Application.ExecutablePath) + "\\Resources\\ohh.mp3");
        }

        #endregion
    }
}


Ahora solamente falta hacer algunas modificaciones en los lugares en los que el sonido debe comenzar.

Lo primero es en TetrisEngine añadir el field para almacenar una instancia de TetrisSounds:


        private TetrisSounds sounds = new TetrisSounds();


En el métido Start() the TetrisEngine hacer que la música comience a sonar:


        public void Start()
        {
            sounds.StartBackground();
...


Y en el Stop() que deje de sonar:


        public void Stop()
        {
            tetrisTimer.Stop();
            sounds.StopBackground();
        }


Y también modificamos su DoAction() para que si se para el juego porque se ha perdido, suene el sonido de que se ha perdido:


        private void DoAction(TetrisAction action)
        {
            if (space.DoAction(action) == PieceMoveResult.DropZone)
            {
                Stop();
                sounds.PlayLost();
            }
            if (OnUpdate != null)
            {
                OnUpdate(this, null);
            }
        }


¿Qué pasa con los otros dos sonidos? ¿El de drop y el de línea? Pues que veremos más adelante cómo introducirlos, porque para identificar cuál de los dos hay que hacer sonar, hay que saber si se han eliminado líneas, y eso se va a hacer aprovechando el contador de puntos en el siguiente capítulo.

El fichero para descargar:

http://dl.dropbox.com/u/11320362/Cientris2.zip

Este el tetris tal y como funciona en esta lección:

Tetris, lección 12

Y este es el producto casi final (le falta algún debug) para el que faltan dos lecciones:

Tetris, lección final




Bill

13. Creando un Tetris: Mostrar la siguiente pieza

Introducción
Para mostrar la siguiente pieza lo primero que tenemos que hacer es "ordenar" las cosas que pintamos. Es decir, ahora mismo estamos pintando en el Canvas de la ventana principal, y poniendo las coordenadas en las que queremos pintar. Pero existen componentes contenedores que sirven para agrupar otros componentes.

Pintando en un panel
Primero redimensionamos nuestra ventana principal a 510;335.
Ahora colocamos un Panel en la ventana (está en el toolbox, en la categoría containers). Lo renombramos a pnlMain.
Redimensionamos nuestro Panel a 111;277 y lo colocamos por la izquierda de la ventana:



Ahora nos vamos al código del MainForm.cs y nos vamos al método DoOnUpdate, que es el que dispara el evento Update de nuestro TetrisEngine. Vemos que actualmente llama al Invalidte() del formulario, que fuerza a su repintado. Lo que queremos es que llame al Invalidate() de nuestro panel, así que lo cambiamos:


        private void DoOnUpdate(object sender, EventArgs e)
        {
            pnlMain.Invalidate();
        }


Pero claro, en el evento Paint del panel no tenemos nada, tenemos respuesta al Paint del formulario. Tenemos que quitar el evento Paint del formulario (borrar el código y sus referencias), y añadir un evento Paint del panel (pinchar en el panel, seleccionar eventos en lugar de propiedades, y doble click en Paint):


        private void pnlMain_Paint(object sender, PaintEventArgs e)
        {
            engine.Draw(e.Graphics);
        }


Y por último, cuando inicializamos nuestro engine, le pasamos coordenadas para su pintado, y hay que ajustarlas al nuevo medio:

engine.Initialize(0, 210, 10, 22, 2);

Ejecutamos y vemos un efecto extraño al jugar: se ve el refresco. Esto es por algo que ya habíamos comentado antes, el flicker. Y para evitar el flicker, lo que hay que hacer es utilizar el double buffering. El problema viene de que si bien los formularios tienen double buffer como propiedad, y es pública, el resto de componentes contenedores no, es una propiedad protegida así que no la podemos cambiar, no tenemos acceso a ella.

La solución es crear nuestra propia clase BufferedPanel que descienda de Panel y que en su constructor modifique este double buffering, dado que al descender de Panel tiene acceso a sus propiedades protegidas. Así que añadimos un BufferedPanel.cs con el siguiente código:

using System.Windows.Forms;

namespace Tetris
{
    public class BufferedPanel : Panel
    {
        public BufferedPanel()
            : base()
        {
            this.DoubleBuffered = true;
            this.UpdateStyles();
        }
    }
}


Ahora mostramos el código de MainForm.Designer.cs, que es dónde se crean las clases visuales del formulario principal, y buscamos dónde se crea nuestro pnlMain y sustituimos la clase Panel por BufferedPanel

        private void InitializeComponent()
        {
            this.pnlMain = new BufferedPanel();
            this.SuspendLayout();


Ejecutamos y ya funciona. Ya estamos listos para continuar y mostrar la pieza siguiente.

Mostrar la pieza siguiente
Lo primero es decidir en qué zona la vamos a mostrar. Así que ponemos un panel pequeñito de 78x78 en nuestro formulario principal. De nuevo, tenemos que ir al código del designer y cambiar la clase Panel por BufferedPanel para que no tenga flicker. Además le ponemos un label para indicar que se trata de la pieza siguiente:



¿Dónde almacenábamos la pieza? En la instancia de TetrisSpace. Pues bien, ahí ahora además de almacenar la pieza actual hay que almacenar la siguiente, y la añadimos a sus fields:


        /// <summary>
        /// Instance of the next piece.
        /// </summary>
        private readonly TetrisPiece nextPiece;


Además modificamos el constructor de TetrisSpace para que cree esta pieza siguiente:


            nextPiece = new TetrisPiece(seed);


Ahora vamos a modificar TetrisPiece para tener dos métodos NewPiece más:
- Uno para construir una pieza siguiente, sin necesidad de más información.
- Otro para construir una pieza recibiendo además la pieza siguiente, para asignar su Id.

        /// <summary>
        /// Calculates a new piece, from a existing next piece .
        /// </summary>
        /// <param name="spaceWidth">Width of the space.</param>
        /// <param name="spaceHeight">Height of the space.</param>
        /// <param name="spaceDropHeight">Height of the space drop.</param>
        /// <param name="nextPiece">The next piece.</param>
        public void NewPiece(int spaceWidth, int spaceHeight, int spaceDropHeight, TetrisPiece nextPiece)
        {
            Id = nextPiece.Id;
            Rotation = 0;
            X = spaceWidth / 2 - CellWidth / 2;
            Y = spaceHeight - spaceDropHeight - 1 + CellHeight - PieceY[Id];
        }

        /// <summary>
        /// Calculates a new Next Piece.
        /// </summary>
        public void NewPiece()
        {
            NewPiece(CellWidth, CellHeight, 0);
            Y = 3;
        }


Volvemos al constructor de TetrisSpace y añadimos que inmediatamente después de instanciar nextPiece, además calcule su pieza siguiente con NewPiece():


        public TetrisSpace(int left, int top, int width, int height, int dropHeight, int seed)
        {
            piece = new TetrisPiece(seed);
            nextPiece = new TetrisPiece(seed);
            nextPiece.NewPiece();
       
            this.left = left;
            this.top = top;
            this.width = width;
            this.height = height;
            this.dropHeight = dropHeight;
            space = new int[width, height];
        }


Y ahora modificamos el método NewPiece de TetrisSpace para tener en cuenta que tenemos ya calculada la pieza siguiente:


        private void NewPiece()
        {
            piece.NewPiece(width, height, dropHeight, nextPiece);
            nextPiece.NewPiece();
        }


Si ejecutamos ahora, estará funcionando perfectamente, pero todavía no está pintando la pieza siguiente en su sitio. Necesitamos un método DrawNext en TetrisSpace que nos sirva para dibujar la pieza siguiente. Para ello además necesitaremos acceso a las constantes CellHeight y CellWidth de TetrisPiece, que debemos cambiarlas a visibilidad pública, y añadir el código para dibujar la pieza:


        /// <summary>
        /// Draws the next piece in a canvas.
        /// </summary>
        /// <param name="drawZone">The canvas draw zone.</param>
        public void DrawNext(Graphics drawZone)
        {
            for (int y = 0; y < TetrisPiece.CellHeight; y++)
            {
                for (int x = 0; x < TetrisPiece.CellWidth; x++)
                {
                    int actualValue = nextPiece.PieceValueAt(x, y);
                    if (actualValue <= 0)
                    {
                        actualValue = 0;
                    }
                    SpriteZone zone;
                    if (actualValue == 0)
                    {
                        zone = SpriteZone.Game;
                    }
                    else
                    {
                        zone = SpriteZone.Piece;
                    }

                    Rectangle cellRect = new Rectangle(x*sprites.XIncrement, 40-y*sprites.YIncrement-x*sprites.YIncrementForX,
                        sprites.SpriteRect.Width, sprites.SpriteRect.Height);
                    sprites.Draw(drawZone, actualValue, cellRect.X, cellRect.Y, zone);
                }
            }
        }


También necesitamos subir este DrawNext hacia TetrisEngine para permitir que se pueda pintar desde dicha clase a más alto nivel:


        /// <summary>
        /// Draws the next piece.
        /// </summary>
        /// <param name="graphicSpace">The graphic space.</param>
        public void DrawNext(Graphics graphicSpace)
        {
            graphicSpace.FillRectangle(backgroundBrush, graphicSpace.ClipBounds);
            space.DrawNext(graphicSpace);
        }


Ahora solamente queda que el MainForm sepa repintar este panel. Para ello añadimos en el DoOnUpdate el Invalidate() del panel de la pieza:


        private void DoOnUpdate(object sender, EventArgs e)
        {
            pnlMain.Invalidate();
            pnlNext.Invalidate();
        }


Y añadimos el evento Paint del pnlNext de forma que pinte la pieza siguiente:

        private void pnlNext_Paint(object sender, PaintEventArgs e)
        {
            engine.DrawNext(e.Graphics);
        }


El resultado:



Aprovechamos además para cambiar el BackColor del MainForm al color Wheat, para que no se vea gris.

Y el código para descargar:

http://dl.dropbox.com/u/11320362/Cientris3.zip

Bill

14. Creando un Tetris: Marcador de puntos y toques finales

Llega el momento de finiquitar nuestro Tetris. Solamente le falta el marcador de puntos, marcador de líneas conseguidas, los sonidos que habíamos dejado pendientes hasta este momento, y algún toquecito. Comencemos con los marcadores.

Marcador de puntos y líneas

Lo primero es crear los cuatro labels para los marcadores, 2 indicativos (el título) y 2 para presentar los números. Así que creamos los 4 labels, a uno lo llamaremos lblActualPoints y a otro lblActualLines, los otros dos como queráis. Además ponedlos de color diferente:



En TetrisEngine creamos los Fields para almacenar los puntos y líneas de la partida actual:


        /// <summary>
        /// Actual points of the player.
        /// </summary>
        private int actualPoints = 0;

        /// <summary>
        /// Total lines removed in the actual game.
        /// </summary>
        private int totalLinesRemoved = 0;


Y creamos también sus propiedades:


        /// <summary>
        /// Gets or sets the actual points.
        /// </summary>
        /// <value>
        /// The actual points.
        /// </value>
        public int ActualPoints
        {
            get { return actualPoints; }
            set { actualPoints = value; }
        }

        /// <summary>
        /// Gets or sets the total lines removed.
        /// </summary>
        /// <value>
        /// The total lines removed.
        /// </value>
        public int TotalLinesRemoved
        {
            get { return totalLinesRemoved; }
            set { totalLinesRemoved = value; }
        }


En el método Start() de TetrisEngine deben inicializarse a 0, porque la partida empieza con los marcadores limpios:


        public void Start()
        {
            sounds.StartBackground();
            actualPoints = 0;
            totalLinesRemoved = 0;
...


Pero claro, el engine no es consciente de cuántas líneas se borran, porque lo hace la instancia de TetrisSpace. Así que vamos a modificar TetrisSpace para que devuelva el número de líneas eliminadas por la última acción.

Primero, añadirle el campo para las líneas borradas:


        /// <summary>
        /// Lines removed in the last move.
        /// </summary>
        private int linesRemoved = 0;


Y la propiedad para dicho campo:


        /// <summary>
        /// Gets or sets the lines removed in the last move.
        /// </summary>
        /// <value>
        /// The lines removed.
        /// </value>
        public int LinesRemoved
        {
            get { return linesRemoved; }
            set { linesRemoved = value; }
        }


El método DoAction de TetrisSpace es la entrada principal de las acciones, con lo cual ahí inicializaremos linesRemoved a 0:

        public PieceMoveResult DoAction(TetrisAction action)
        {
            linesRemoved = 0;
            if (action == TetrisAction.Drop)
...


Y el lugar exacto en el que TetrisSpace puede saber cuántas líneas borra, es en RemoveCompleteLines():


        private void RemoveCompleteLines()
        {
            int i = 0;
            while (i < (height - dropHeight))
            {
                if (IsCompleteLine(i))
                {
                    RemoveLine(i);
                    linesRemoved++;
                }
                else
                {


Ahora en el Engine hay que hacer que como respuesta a la realización de una acción, incremente su total de puntos y de líneas. Así que nos vamos al DoAction de TetrisSpace:


        private void DoAction(TetrisAction action)
        {
            if (space.DoAction(action) == PieceMoveResult.DropZone)
            {
                Stop();
                sounds.PlayLost();
            }
            else
            {
                totalLinesRemoved += space.LinesRemoved;
                actualPoints += space.LinesRemoved * space.LinesRemoved;
            }


Ahora el Engine ya tiene conocimiento de sus puntos y de la cantidad de líneas borradas. Nos queda que se muestren en el interfaz. ¿Dónde actualizarlo? Primero al crear la aplicación para que se pongan a 0 o lo que quiera el Engine que sea la puntuación inicial. Y luego tras la reacción a las teclas pulsadas en el evento KeyDown:


        public MainForm()
        {
            InitializeComponent();
            DoubleBuffered = true;
            Stream inputStream = GetType().Assembly.GetManifestResourceStream("Tetris.Resources.cubos.png");
            if (inputStream != null)
            {
                Bitmap inputBitmap = new Bitmap(inputStream);
                inputStream.Close();
                engine = new TetrisEngine(Color.Wheat, inputBitmap, 8, true);
                inputBitmap.Dispose();
                engine.OnUpdate += DoOnUpdate;
                engine.Initialize(0, 210, 10, 22, 2);
            }
            lblActualLines.Text = engine.TotalLinesRemoved.ToString();
            lblActualPoints.Text = engine.ActualPoints.ToString();
        }


Y en el MainForm_KeyDown al final lo mismo:


                 case Keys.Left:
                    engine.MoveLeft();
                    break;
                case Keys.Right:
                    engine.MoveRight();
                    break;
            }
            lblActualLines.Text = engine.TotalLinesRemoved.ToString();
            lblActualPoints.Text = engine.ActualPoints.ToString();
        }




Sonidos para el drop y línea conseguida
Hemos visto que quien se entera a la vez de la acción realizada y si la acción conlleva eliminar líneas, es el TetrisSpace. Así que tenemos dos opciones, o pasarle el sounds a TetrisSpace o que genere eventos hacia arriba. En nuestro caso vamos a hacer lo primero, modifiquemos DoAction para que pase el TetrisSounds de TetrisEngine a TetrisSpace:


        public PieceMoveResult DoAction(TetrisAction action, TetrisSounds sounds)


Dentro de ese DoAction buscamos la acción TetrisAction.Down, y justo después del ApplyPiece() reaccionamos tocando el sonido:


                if (linesRemoved > 0)
                {
                    sounds.PlayLinesRemove();
                }
                else
                {
                    sounds.PlayDrop();
                }


Ahora el propio compilador nos avisa de que en tres lugares la llamada al DoAction está mal porque falta un argumento. En esos tres lugares hay que añadir el parámetro sounds a la llamada, y listo.

Toques finales

Primero el título. Lo de Form1 no queda bien. Así que lo cambiamos por Tetris.

Segundo añadir una imagen con instrucciones, porque queda estiloso:

Añadimos al mainForm un objeto image y le cargamos esta (botón derecho, choose image):


Y por último hacer que se pueda cambiar de vista 2D a 3D pulsando la letra c. Creamos los métodos Set2D() y Set3D() que cargan los recursos correspondientes a cada uno, así:


        private void Set3D()
        {
            isometric = true;
            Stream inputStream = GetType().Assembly.GetManifestResourceStream("Tetris.Resources.cubos.png");
            if (inputStream != null)
            {
                Bitmap inputBitmap = new Bitmap(inputStream);
                inputStream.Close();
                engine.InitSprites(inputBitmap, 8, true);
                inputBitmap.Dispose();
            }
        }

        private void Set2D()
        {
            isometric = false;
            Stream inputStream = GetType().Assembly.GetManifestResourceStream("Tetris.Resources.cubos3.png");
            if (inputStream != null)
            {
                Bitmap inputBitmap = new Bitmap(inputStream);
                inputStream.Close();
                engine.InitSprites(inputBitmap, 8, false);
                inputBitmap.Dispose();
            }
        }


El campo isometric debe ser un booleano dentro del propio mainForm:


        private bool isometric = false;


Y ahora modificamos TetrisEngine para tener el método InitSprites:


        public void InitSprites(Bitmap srcCubeBitmap, int numCubes, bool isIsometric)
        {
            TetrisSprite.Instance.Initialize(srcCubeBitmap, numCubes, isIsometric);
        }


Y por último modificamos el MainForm_KeyDown para que reaccione al pulsar la "c", cambiando de 2D a 3D y viceversa:


                case Keys.C:
                    if (isometric)
                    {
                        Set2D();
                    }
                    else
                    {
                        Set3D();
                    }
                    break;


Y como la aplicación comienza en isométrica, hacer que en el constructor del MainForm inicialice la variable isometric a true.

¡Y hecho! ¡Terminado!

Código final de descarga:
http://dl.dropbox.com/u/11320362/Cientris4.zip

Últimos mensajes

Adivina la película de raul_isl
[Hoy a las 02:06]


¿Qué manga estás leyendo? de M.Rajoy
[26 de Abril de 2024, 11:54]


Gran Guía de los Usuarios de 106 de M.Rajoy
[25 de Abril de 2024, 07:20]


Felicidades de M.Rajoy
[15 de Abril de 2024, 13:54]


Marvel Cinematic Universe de M.Rajoy
[15 de Abril de 2024, 08:52]