Introducción a la Programación de Videojuegos y Gráficos GRADO EN INGENIERÍA INFORMÁTICA Curso 2012/2013 T3: VIDEOJUEGOS 2D Y 3D 3.1. Estructura de un videojuego. 3.2. Motores gráficos(perspectivas, estructuras de datos y algoritmos de visualización, navegación). 3.3. Física (conceptos, colisiones, proyectiles, motores de físicas). 3.4. Programación gráfica 2D (APIs gráficas). 3.5. Estructura de videojuegos 3D. 3.6. Programación gráfica 3D. Estructura de los videojuegos. • La estructura del código de un videojuego sigue un esquema básico que en algunos casos difiere del de otros tipos de software. • La estructura del código de un videojuego tiene la siguente estructurá básica. Inicialización Bucle principal Comprobar entrada de usuario Actualización de datos Finalización Sí ¿Fin del juego? No Salida Inicialización. • La inicialización de un videojuego usualmente tiene los siguientes pasos: • Comprobaciones de hardware. • Inicialización de los datos del motor gráfico. • Carga del estado del videojuego. • Carga de los recursos iniciales (texturas, sonidos, etc). • Posibles cinemáticas. (Nota: Se recomienda que este paso pueda ser saltado mediante una entrada del usuario, ver el mismo video de 10 minutos cada vez que se desea jugar es bastante frustrante). El bucle principal, entrada del usuario • Una vez el usuario ha recibido una imagen del estado del juego, éste puede producir (o no) algunos eventos adicionales. • Los usuarios producen eventos de juego a través de los dispositivos de salida. •Teclado. •Ratón. •Mando de juegos. •Micrófono. • Estos eventos de usuario se combinan con la lógica del juego para producir modificicaciones en el estado del juego. La mayoría de estas tareas son gestionadas usualmente por el entorno de trabajo (framework o herramienta) que estemos usando. Si decidimos no usar un entorno de trabajo es necesario que nosotros mismos implementemos todo este código. El bucle principal, modificación de los datos de estado. • El videojuego tiene que mantener un conjunto de estructuras de datos que representan el estado del juego (Nota: estos datos son independientes de los datos que tiene que mantener el motor gráfico). • Estos datos se modifican por muchos eventos producidos por la lógica del juego. Algunos ejemplos de tales eventos son: • Acciones del usuario. • Entrada de datos por la red. • Acciones de la Inteligencia Artificial del juego. • Eventos en el tiempo. • Eventos relacionados con la física (colisiones, explosiones, etc). • La modificación de los datos puede producir el final del juego al alcanzar un estado en el cual el juego debe terminar. El bucle principal. Salida • Una vez que el estado del juego se ha modificado necesitamos mostrar al usuario una representación de dicho estado. • Esta representación se muestra mediante dispositivos de salida: • Dispositivos gráficos (2D y 3D). • Sonidos. • Mensajes por la red. • Mandos de juego (vibración). Motores gráficos • Los motores gráficos generan la representación gráfica del juego mediante el uso de elementos gráficos más pequeños. • El motor gráfico suele ser parte de un marco (framework) que suele proporcionar la estructura del programa y métodos adicionales para la gestión del teclado, reproducción de sonidos, actualización de los datos. • En este curso usaremos un marco de trabajo que ya incluye (entre otras muchas cosas) un motor gráfico. Motor gráfico y framework • En este curso usaremos como marco de trabajo el XNA Game Studio 4.0. • XNA (X Not Acronym) 4.0 is a una herramienta de programación que permite usar Visual Studio para desarrollar juegos para Windows (para PC o Smartphone) y XBOX 360. • XNA 4.0 Game Studio incluye XNA Framework, un conjunto de librerías diseñadas para el desarrollo de videojuegos y basadas en .NET Framework 2.0. http://msdn.microsoft.com/es-es/library/dd430289 • XNA Framework también incluye un motor gráfico, la mayoría de los conceptos de los motores gráficos (estructuras de datos, colisiones, partículas, perspectiva, etc.) se irán explicando a medida que aparezcan. Programación gráfica en 2D • Para comenzar a desarrollar XNA 4.0. Necesitáis: • Visual Studio 2010. • .NET Framework (si no lo tenéis instalado previamente, Visual Studio lo instalará). • XNA Game Studio 4.0. • Un vez tengáis el software necesario, simplemente ejecutad Visual Studio 2010 en el menú Inicio. La primera aplicación: Nuevo proyecto XNA 4.0 1. En la barra de menú seleccionad File (Fichero) → New (Nuevo) → Project (Proyecto). Aparecerá un diálogo. Seleccionad Windows Game (4.0) en las plantillas de Visual C#. Elegid la carpeta y el nombre del juego (de momento los valores por defecto nos valdrán) y Aceptar. Nuestra primera aplicación: Nuevo proyecto XNA 2. Visual Studio generará un montón de plantillas de fichero y abrirá el fichero principal. Deberíais obtener la siguiente pantalla. Ahora, en el menú, elegid Debug (depurar) → Start Debugging (Iniciar depuración). Nuestra primera aplicación: Nuevo proyecto XNA 3. Después de compilar el proyecto obtendreis el siguiente “videojuego”. ! Eso no es un videojuego!. • Bueno, en realidad sí que lo es. Tiene todos los elementos que describimos en la sección anterior, Inicialización, Bucle Principal (entrada de usuario, gráficos, actualización de los datos) y terminación. • Ahora se debe programar la lógica del juego. Las clases principales de un proyecto XNA • • El proyecto tiene dos ficheros fuente: • Program.cs • • Bastante simple, crea un objeto de tipo Game1 y llama a su método run. Game1.cs • Contiene la definición de la clase principal Game1 del tipo Microsoft.Xna.Framework.Game • Proporciona dos variables de clase • graphics (GraphicsDeviceManager): Permite el acceso a los recursos gráficos, por ejemplo a la GPU. • spriteBatch (SpriteBatch): Gestiona los sprites. Las clases principales de un proyecto XNA Un sprite es una imagen 2D o 3D que se puede integrar en una escena. Y cinco métodos derivados. • Initialize: Inicializa los datos del juego. • Load Content: Carga las imágenes, sonidos y cualquier otro recurso. • UnloadContent: Libera los recursos. • Update: Actualiza los datos del juego usando la lógica del jiego, tambien comprueba la entrada del usuario. • Draw: Realiza la salida gráfica. Sprites y entrada. Para dibujar algo e intentar moverlo: • Abrid el proyecto que generasteis en la última clase • Probablemente todavía está en la lista de proyectos recientes. • Si no está, en la barra de menú seleccionad select File (Archivo) → Open (Abrir) → Project or Solution (Proyecto o Solucion). Sprites y entrada. • • Mirad al panel llamado Solution Explorer (explorador de soluciones) situado normalmente a la derecha de la ventana de Visual Studio window). Vereis dos proyectos dentro de la solución, WindowsGame1 and WindowsGame1Content. De momento, es una buena idea crear subcarpetas dentro de WindowsGame1Content para diferentes tipos de contenidos (imagenes, sonidos etc). FileAdd (Agregar) → New Folder (Nueva Carpeta). Añadid una carpeta Images. Añadir nuevo contenido • Ahora, descargad el fichero “XNALogo.png” de la plataforma Moodle y guardadlo en algún lugar del ordenador. Un buen lugar es la carpeta WindowsGame1Content/Images dentro de vuestro proyecto. • Añadid la imagen como contenido seleccionando Add (Agregar) → Existing Item (Elemento Existente) y seleccionad el fichero XNALogo.png en el diálogo que aparece. • Ahora la imagen deberia aparecer como nuevo contenido del proyecto. Cargar el sprite • Antes de dibujar un Sprite es necesario cargarlo en variables que se puedan usar para gestionarlo. • El objeto por defecto usado para gestionar una imagen es Texture2D. • Añadid un objeto Texture2D al fichero Game1.cs. public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; Texture2D sprite; Cargar el sprite • Ahora necesitamos cargar el recurso. Recordad que toda la carga de recursos de realiza en el método sobrecargado LoadContent. • Añadid la siguiente línea en el método LoadContent. sprite = Content.Load<Texture2D>(@"Images/XNALogo"); protected override void LoadContent() { // Create a new SpriteBatch, which can be used to draw textures. spriteBatch = new SpriteBatch(GraphicsDevice); sprite = Content.Load<Texture2D>(@"Images/XNALogo"); Dibujar el sprite. • Todas las tareas de dibujo se realizan dentro del método sobrecargado Draw. • Ahora añadid las siguientes líneas al método Draw y ejecutad la solución. protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(); spriteBatch.Draw(sprite, Vector2.Zero, Color.White); spriteBatch.End(); // TODO: Add your drawing code here base.Draw(gameTime); • Deberíais obtener el siguiente “juego”. Dibujo básico de sprites. • El procedimiento Begin del objeto SpriteBatch indica que vamos a enviar un conjunto de sprites al dispositivo gráfico. Siempre debemos llamar a Begin antes de dibujar cualquier sprite. • El procedimiento Draw muestra el Sprite en el dispositivo gráfico. • Texture2D sprite: El objeto a dibujar. • Vector2 pos: La posición donde se dibuja el sprite. • Color color: El color del sprite. Dibujo básico de sprites. • Cambiemos un poco el procedimiento Draw. Cambiad la llamada por (por ejemplo) la siguiente línea y ejecutad el juego. spriteBatch.Begin(); spriteBatch.Draw(sprite, new Vector2((Window.ClientBounds.Width / 2) (sprite.Width / 2), (Window.ClientBounds.Height / 2) - (sprite.Height / 2)), Color.White); spriteBatch.End(); • El objeto Window representa la ventana del videojuego. Draw • El procedimiento Draw es un procedimiento sobrecargado que puede tener muchos argumentos: public void Draw ( Texture2D texture, Vector2 position, Nullable<Rectangle> sourceRectangle, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth ); • texture Texture2D: El sprite. • position Vector2: La posición donde se dibuja el Sprite. • sourceRectangle Nullable<Rectangle>: Un rectángulo que indica la parte del Sprite que vamos a dibujar (usar null para dibujar todo el sprite). • color Color: El color del Sprite. Usar Color.White para usar los colores del Sprite. • rotation Single: Ángulo de rotación con el que se pinta el Sprite respecto a su origen (origin). • origin Vector2: Punto origen (origin) del Sprite, por defecto (0,0). • scale Vector2: Factor de escala. • effects SpriteEffects: Efectos a aplicar (invertir horizontal o verticalmente). • layerDepth Single: Profundidad de la capa de dibujo, 0 es la capa frontal y 1 la trasera.Es necesario usar SpriteSortMode si se desea ordenar los sprites durante el dibujo. Texto • Los textos se pintan en pantalla de una forma muy similar a los Sprites. • Antes de pintar un texto es necesario definir una fuente. Las definiciones de fuentes de texto en XNA son ficheros XML que se tratan como cualquier otro recurso y que contienen el nombre de la fuente, el tamaño, los efectos (negrita, cursiva, etc), el rango de caracteres que admitimos. • Descargad de Moodle el fichero text.spritefont • Cread en vuestro una nueva categoría de contenido denominada fonts y añadid el fichero como nuevo recurso. • Podéis abrir el recurso para ver los campos que contiene. Texto (II) • Añadid como variable de clase, una variable de tipo SpriteFont SpriteFont fuente; • En el procedimiento Load() añadid la línea fuente = Content.Load<SpriteFont>(@"fonts/text"); • Finalmente para dibujar el texto añadid la siguiente línea en el procedimiento Draw, entre las llamadas a spriteBatch.begin y spriteBatch.end; spriteBatch.DrawString(fuente, "Puntuacion: ", new Vector2(Window.ClientBounds.Width / 2, Window.ClientBounds.Height - 30), Color.Black, 0, Vector2.Zero,1, SpriteEffects.None, 1); Transparencias. • XNA permite establecer diferentes niveles de transparencia por pixel. Para ello es necesario usar un editor y un formato que fichero que permita establecer un canal alpha por pixel (RGBA), por ejemplo .png . Si la imagan no tiene canal alpha, XNA toma el color magenta (255,0,255) como transparente. • Cambiad ahora el logo que usamos en el ejemplo anterior por el fichero logo_trans.png. Para ello: • Descargad el fichero logo_trans.png de Moodle • Añadid el fichero como nuevo recurso tipo imagen. • Cambiad la linea: sprite = Content.Load<Texture2D>(@"Images/XNALogo"); por sprite = Content.Load<Texture2D>(@"Images/logo_trans"); • Compilad y ejecutad. Movimiento (I) • Para mover un objeto en la pantalla es necesario modificar la posición del objeto entre llamadas al procedimiento Draw. Declaremos dos variables de Sprite, dos variables de posición y dos variables de velocidad: SpriteBatch spriteBatch; Texture2D sprite; Texture2D sprite2; Vector2 pos1 = Vector2.Zero; Vector2 pos2 = Vector2.Zero; float speed1 = 2f; float speed2 = 3f; • Cargad ahora los dos sprites (transparente y no transparente), para ello dejad en LoadContent() el siguiente código: protected override void LoadContent() { // Create a new SpriteBatch, which can be used to draw textures. spriteBatch = new SpriteBatch(GraphicsDevice); sprite = Content.Load<Texture2D>(@"Images/XNALogo"); sprite2 = Content.Load<Texture2D>(@"Images/logo_trans"); // TODO: use this.Content to load your game content here } Movimiento (II) • Ahora necesitamos modificar la posición de los sprites. Recordad que en XNA toda la actualización de los datos se debe realizar en el procedimiento Update. Añadid el siguiente código al procedimiento Update. protected override void Update(GameTime gameTime) { // Allows the game to exit if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); pos1.X += speed1; if (pos1.X > Window.ClientBounds.Width - sprite.Width || pos1.X < 0) speed1 *= -1; pos2.Y += speed2; if (pos2.Y > Window.ClientBounds.Height - sprite.Height || pos2.Y < 0) speed2 *= -1; • Vamos a pintar el logo transparente en color negro. Añadid las siguientes líneas al procedimiento Draw. Compilad y ejecutad. spriteBatch.Draw(sprite,pos1,Color.White); spriteBatch.Draw(sprite2,pos2,Color.Black); Tamaño y efectos. • XNA permite modificar el tamaño de un Sprite, así como pintarlo reflejado horizontal o verticalmente, o incluso girarlo. • Para dibujar el logo transparente a un mayor tamaño (150% de su tamaño original) y reflejado horizontalmente, probad a cambiar la línea: spriteBatch.Draw(sprite2,pos2,Color.Black); por spriteBatch.Draw(sprite2,pos2,null,Color.Black,0,Vector2.Zero,1.5f, SpriteEffects.FlipHorizontally,0); si además deseais girar el Sprite. spriteBatch.Draw(sprite2,pos2,null,Color.Black,MathHelper.ToRadians(45.0f), Vector2.Zero,1.5f, SpriteEffects.FlipHorizontally,0); En XNA todos los ángulos se especifican en Radianes. Podéis usar la clase MathHelper, para convertir de grados a radianes. Profundidad. • XNA permite definir una profundidad de dibujo para los objetos 2D (Orden Z), que sobrepase el orden de dibujo que se indica en la función Draw. De tal forma que un Sprite siempre se dibuje adelante o atrás. • Para usar el Orden Z es necesario modificar la llamada a spriteBatch.Begin(); dentro de Draw, añadiendo un argumento de tipo SpriteSortMode (en este caso, tambíen es necesario añadir un argumento de tipo BlendState) • Deferred: Los sprites no se dibujan hasta que se llama al procedimiento SpriteBatch.End. En ese momento todas las llamadas a Draw se envian al dispositivo gráfico en el mismo orden que se realizaron (este es el modo por defecto). Se pueden tener varios objetos SpriteBatch Activos. • Immediate: Los sprites se pintan tan pronto como se realiza la llamada a Draw. Este es el modo más rápido, pero solo permite un objeto SpriteBatch activo • Texture: Lo mismo que Deferred pero los sprites se ordenan por textura antes de ser pintados. • BackToFront: Lo mismo que Deferred, pero los sprites se ordenan de adelante hacia atrás basado en el orden indicado por el parámetro de profundidad de la llamada a Draw (pinta mas atrás los valores más cercanos al 1). • FrontToBack: Lo mismo que Deferred, pero los sprites se ordenan de atrás hacia adelante basado en el orden indicado por el parámetro de profundidad de la llamada a Draw. (pinta más atras los valores mas cercanos al 0) Mezclas (Blending). • XNA permite definir como se van a mezclar los Sprites con el fondo de la escena (Blending). • Para modificar el Blending es necesario modificar la llamada a spriteBatch.Begin(); dentro de Draw, añadiendo un argumento de tipo BlendState. • AlphaBlend: Fuente y destino se mezclan usando los valores alpha del sprite. Este es el comportamiento por defecto y permite usar transparencias. • Additive: Suma fuente y desino ignorando los valores alpha. • NonPremultiplied: Mezcla fuente y destino usando alpha pero suponiendo que los datos de color no contienen información alpha. • Opaque: Mezcla opaca, sobreescribe la fuente con los datos de destino. Ejemplos de Blending. • Para usar el Z orden y el Blending es necesario modificar la llamada a la función spriteBatch.Begin(), añadid los siguientes argumentos: spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend); • Para probar el Z orden, modificad spriteBatch.Begin() y cambiad las llamadas a Draw en ambos Sprites, spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend); spriteBatch.Draw(sprite, pos1,null,Color.White,0,Vector2.Zero,1,SpriteEffects.None,1); spriteBatch.Draw(sprite2, pos2,null,Color.Black,0,Vector2.Zero,1,SpriteEffects.None,0); • Probad a cambiar también algunos de los valores del argumento BlendState. Animación I. • XNA permite usar Sprites con varias imágenes (frame sheets). • Descargad de Moodle los ficheros threerings.png y plus.png y añadidlos como recursos de imagen del proyecto. • Cambiad las texturas antiguas (XNALogo y logo_trans) por las nuevas que acabamos de añadir al proyecto. sprite = Content.Load<Texture2D>(@"Images/threerings"); sprite2 = Content.Load<Texture2D>(@"Images/plus"); • Las nuevas texturas contienen muchas imágenes (frames), tenemos que seleccionar el trozo de imagen vamos a pintar en cada momento. • Antes de pintar la animación es necesario saber: • El ancho y el alto de cada frame dentro del frame sheet. • El número de filas y columas del frame sheet. • Un índice que específica la fila y la columna dentro del frame sheet del frame que vamos a dibujar Animación II. • threerings.png tiene seis columnas y ocho filas, cada frame ocupa 75x75 pixels. • plus tiene seis columnas y cuatro filas, cada frame ocupa 75x75 pixels. • Añadid esta información como variables de la clase. Point frameSize = new Point(75, 75); Point currentFrame1 = new Point(0, 0); Point sheetSize1 = new Point(6, 8); Point currentFrame2 = new Point(0, 0); Point sheetSize2 = new Point(6, 4); • Cambiad las llamadas a draw para ambos sprites. spriteBatch.Draw(sprite, pos1,new Rectangle(currentFrame1.X * frameSize.X, currentFrame1.Y * frameSize.Y,frameSize.X,frameSize.Y),Color.White, 0, Vector2.Zero, 1, SpriteEffects.None, 0); spriteBatch.Draw(sprite2, pos2, new Rectangle(currentFrame2.X * frameSize.X, currentFrame2.Y * frameSize.Y, frameSize.X, frameSize.Y), Color.White, 0, Vector2.Zero, 1, SpriteEffects.None, 0); • En la función Update, cambiad los valores sprite.Width y sprite.Height por los valores frameSize.X y frameSize.Y respectivamente. Animación III. • Ahora añadid el siguiente código en la función Update: ++currentFrame1.X; if (currentFrame1.X >= sheetSize1.X) { currentFrame1.X = 0; ++currentFrame1.Y; if (currentFrame1.Y >= sheetSize1.Y) currentFrame1.Y = 0; } ++currentFrame2.X; if (currentFrame2.X >= sheetSize2.X) { currentFrame2.X = 0; ++currentFrame2.Y; if (currentFrame2.Y >= sheetSize2.Y) currentFrame2.Y = 0; } • Compilad y ejecutad. Animación IV, framerates. • XNA permite modificar el número de frames por segundo (fps) a los que se ejecuta un juego (por defecto 60fps en Windows y 360, y 30fps en Windows phone). • Para modificar el número de frames, añadid la siguiente línea al final del constructor de Game1; public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; TargetElapsedTime = new TimeSpan(0, 0, 0, 0, 50); } • • Esta línea le indica a XNA que se debe llamar al prodedimiento Game.Update una vez cada 50 milisegundos, obteniendo por tanto, una tasa de 20fps. El objeto GameTime que Draw pasa como parámetro, contiene el campo IsRunningSlowly, que si toma el valor true indica que XNA no es capaz de alcanzar la tasa de refresco especificada. En este caso, usualmente XNA realiza un menor número de llamadas a la función Draw. Animación V, framerates. Bien, pero me gustaría modificar el framerate de un Sprite, no el de todo el juego. • Para modificar el framerate de un Sprite concreto, añadid las dos siguientes variables como variables de la clase int timeSinceLastFrame = 0; int millisecondsPerFrame = 50; • Cambiad el siguiente código dentro de la funcion Update() ++currentFrame1.X; if (currentFrame1.X >= sheetSize1.X) { currentFrame1.X = 0; ++currentFrame1.Y; if (currentFrame1.Y >= sheetSize1.Y) currentFrame1.Y = 0; } por timeSinceLastFrame += gameTime.ElapsedGameTime.Milliseconds; if (timeSinceLastFrame > millisecondsPerFrame) { timeSinceLastFrame -= millisecondsPerFrame; ++currentFrame.X; if (currentFrame.X >= sheetSize.X) { currentFrame.X = 0; ++currentFrame.Y; if (currentFrame.Y >= sheetSize.Y) currentFrame.Y = 0; } }