Enlaces |Inicio|Otros paises Buscar en Microsoft.

com:
Ir

Home|MSDN Library|Descarga|Centros de desarrollo|Eventos|Mapa|Países|Contacto Inicio de MSDN Artículos Técnicos Productos y Tecnologías Arquitectura Centros de desarrollo Información técnica MSDN Académico Universidad .NET Eventos 3 de marzo de 2006 Comunidad Online Partnering y Certificación TechNet Publicado: 16 de Mayo de 2006 Soporte Técnico Derek Pierson Seguridad 3Leaf Development Casos de éxito Contacta con nosotros Para este artículo Desarrolla con MSDN Código para este artículo: C# Código Compartido Código para este artículo: VB Emprendedores MSDN Comentar este artículo Versión imprimible (en inglés)

Inicio del desarrollo de juegos: Parte VII: detección de terreno y colisión

Resumen: Bienvenido al séptimo artículo sobre cómo iniciarse en el desarrollo de juegos. En el último artículo, abordamos la forma en que las luces y los materiales pueden aportar realismo a una escena. También analizamos cómo crear un terreno a partir de un mapa de elevación y finalmente agregamos fuentes a la aplicación. En este artículo, perfeccionaremos la forma en que luce el terreno y comenzaremos a incorporarlo a nuestro juego. Antes de empezar, haremos la limpieza de código obligatoria, que incorpora todos los comentarios que he recibido.
En esta página

Limpieza de código Control de dispositivo Texturas Generación de terreno a través de procedimientos Nueva cámara Elevación del terreno Resumen Acerca del autor

Limpieza de código
La limpieza para este artículo consiste en algo de refactorización menor para mantener limpio el código y algunos cambios a las luces y las normales para la iluminación. •Controle el evento DeviceResizing. •Administre los eventos Device (consulte la siguiente sección). •El ambiente se cambió y la luz se difumina para que quede más oscuro. Si utiliza Color.White para la luz ambiental, ya existe un 100% de ella, de modo que cualquier otra luz no será visible. Ahora estoy usando Color.FromArgb ( 64, 64, 64 ) para el ambiente y Color.FromArgb ( 128, 128, 128 ) para la luz difusa. •Se agregó _device.RenderState.NormalizeNormals = true;, de modo que las normales se volvieron a calcular al momento de ajustar en escala el terreno. Puede eliminar esta llamada si no va a ajustar en escala el terreno, dado que esto tiene cierto costo de

rendimiento. •En el último artículo, afirmé de manera incorrecta que el elemento presentParams.AutoDepthStencilFormat = DepthFormat.D16; configuraba un DepthStencil, pero, en realidad, configura un DepthBuffer. Para obtener una aclaración, revise estos vínculos: •http://en.wikipedia.org/wiki/Depth_buffer •http://en.wikipedia.org/wiki/Stencil_buffer Principio de la página

Control de dispositivo
Hasta este momento, hemos supuesto que nada puede salir mal una vez inicializado el dispositivo de gráficos, hasta que terminemos de usarlo. En el mundo real las cosas no salen del todo bien y en el mundo de DirectX podemos sufrir la pérdida del dispositivo en respuesta a varias acciones del usuario, tales como usar Alt+Tab para desplazarse de una aplicación a otra, minimizar una ventana o usar ciertas funciones de administración de energía. Una vez perdido el dispositivo, todas las funciones de representación producirán un error silenciosamente y las operaciones de representación no devolverán un código de error aunque en realidad presenten un error. Cualquier llamada a las funciones de representación, por ejemplo, DrawIndexedPrimitives, se descarta hasta que el dispositivo se restablece. Sin embargo, cualquier llamada a Device.Present provocará una excepción. Existen dos excepciones que se asocian a dispositivos perdidos: •DeviceLostException: el dispositivo se perdió y no se puede restablecer. •DeviceNotResetException: el dispositivo se perdió, pero se puede restablecer. Para detectar el estado de pérdida del dispositivo, DirectX proporciona el evento DeviceLost para el objeto Device. Una vez perdido el dispositivo, éste se debe restablecer para que funcione en forma adecuada. También puede comprobar si el dispositivo se encuentra perdido mediante la propiedad CheckCopperativeLevel del dispositivo. Una vez perdido el dispositivo, la aplicación consultará el dispositivo y determinará si se puede restablecer a un estado operacional. Si es posible restablecerlo, entonces se destruyen todos los recursos de la memoria de video ubicados en Pool.Default y las cadenas de intercambio y se llama al método Reset. La memoria del sistema copia los recursos creados en Pool.Managed y, de este modo, no es necesario destruirlos ni volver a crearlos. Si no es posible restablecer el dispositivo, entonces la aplicación espera hasta que éste se pueda restaurar. El método Reset es el único método que tiene algún efecto sobre la aplicación una vez perdido el dispositivo y es la única forma de cambiar un dispositivo del estado de pérdida al estado operacional. El método Reset emitirá un error, a menos que la aplicación haya liberado todos los recursos asignados, de modo que los recursos creados en el evento DeviceReset deberían destruirse en el evento DeviceLost. Basta de teoría. Necesitamos actualizar BattleTank2005 para detectar los eventos de pérdida y restablecimiento del dispositivo y reaccionar a ellos. En la parte inferior del método ConfigureDevice de la clase GameEngine, agregue el siguiente código:
_device.DeviceReset+=new EventHandler(OnDeviceReset); _device.DeviceLost+=new EventHandler(OnDeviceLost); _device.Disposing+=new EventHandler(OnDeviceDisposing); OnDeviceReset ( this, null ); Ahora agregue los tres métodos siguientes a la clase GameEngine: private void OnDeviceReset(object sender, EventArgs e) {

if ( _font != null ) _font.OnResetDevice ( ); // Turn on some low level ambient light _device.RenderState.Ambient = Color.LightGray; } private void OnDeviceLost(object sender, EventArgs e) { if ( _font != null & _font.Disposed == false ) _font.OnLostDevice ( ); } private void OnDeviceDisposing(object sender, EventArgs e) { if ( _mouse != null ) _mouse.Dispose ( ); if ( _keyboard !=null ) _keyboard.Dispose ( ); if ( _terrain != null ) terrain.Dispose ( ); }

Revise los comentarios en el código para una explicación más detallada. Principio de la página

Texturas
En el último artículo utilizamos un archivo de textura única y la estiramos a través del terreno para hacer que éste se viera más real. Las texturas nos ofrecen la capacidad de asignar imágenes a las superficies de nuestras formas. Cuando se aplica una textura 2D a una forma 3D, es necesario hacer coincidir de alguna manera las coordenadas de la textura con las coordenadas de la forma. Esto se puede lograr asignando las coordenadas de la primera a aquéllas de la segunda. Los valores Tu y Tv representan a las coordenadas X/Y de la textura (un par u,v único se conoce también como "texel"). La diferencia principal es que las coordenadas de la textura son bidimensionales y se ubican en el intervalo de 0,0f – 1,0f.

En el código simplemente asignamos cada coordenada de la textura a la coordenada de la malla coincidente, dividiéndola por el número de cuadrantes. Al hacer esto se obtiene un único texel que extiende un único cuadrante del terreno.
vertex.Tu = (float)x / _numberOfQuadsX; vertex.Tv = (float)z / _numberOfQuadsZ

Junto con este simple enfoque, también se podrían utilizar múltiples texturas al mismo tiempo. Esto recibe el nombre de multitexturización y es un tema que vale la pena explorar para cualquier desarrollador que trate de crear un terreno más realista. Por ahora sólo utilizaremos una textura única, pero la multitexturización explica el motivo por el cual pasamos un cero a la llamada SetTexture en el método Render de la clase

Terrain. El cero, como ya habrá imaginado, identifica que ésa es la primera textura que se utilizará.
_device.SetTexture ( 0, _terrainTexture );

Existen varias otras modificaciones que puede hacer a la forma en que la textura aparece, pero ninguna de ellas cambia realmente la forma en que usamos la textura del terreno en el código. DirectX utiliza una técnica llamada "Filtrado" para emparejar cualquier distorsión provocada al ampliar o reducir el triángulo de textura para ajustarlo al triángulo de pantalla. Debe probar a cambiar el valor SamplerStageStates pasado al método Device.SetSamplerState. Como siempre, debe establecer un equilibrio entre velocidad y detalle. Otra solución al problema de la distorsión es utilizar mipmap. Este enfoque utiliza un conjunto de texturas encadenadas, más pequeñas y de menor resolución. Si utiliza el método FromFile de TextureLoader, DirectX generará automáticamente una cadena de mipmap. Si se siente inclinado desde el punto de vista gráfico o bien desea obtener el mejor resultado posible de la textura, también puede utilizar el editor de texturas que se incluye en DirectX SDK (DxTex.exe) para generar una cadena de mipmap. (Este editor también sirve para muchas otras funciones de manipulación de textura). El resultado que alcanzamos con el archivo Down.jpg en BattleTank2005 fue aceptable, pero bastante aburrido. Si se utiliza una herramienta de terceros, se puede crear una textura de terreno personalizada para el mapa de elevación, con resultados mucho mejores de los que alcanzamos, pero ¿qué pasa si queremos generar terreno aleatorio para cada nivel? La respuesta es la generación de terreno a través de procedimientos. Principio de la página

Generación de terreno a través de procedimientos
La textura que utilizamos se tomó de la parte inferior del skybox y la coloración y los toques de luz de la textura no tienen relación con el paisaje creado según el mapa de elevación. Otra opción es utilizar esta misma técnica, pero con un archivo de textura creado específicamente para el mapa de elevación (o dicho de manera más precisa, el mapa de elevación y la textura se crean al mismo tiempo) mediante un programa como Terragen. Este enfoque le permite jugar con el archivo de textura hasta conseguir exactamente lo que quiere, pero es un trabajo bastante intenso, en especial si desea ofrecer al jugador un gran número de mapas donde jugar. Otro enfoque consiste en generar el archivo de textura con un mapa de elevación y un par de archivos de textura con un proceso llamado "Generación de textura procedural". Con este enfoque puede utilizar archivos de textura que representen el terreno a distintas alturas. Por ejemplo, podría especificar una textura de arena para las elevaciones inferiores, textura de hierba para los siguientes niveles de elevación, textura de roca para el siguiente nivel y, por último, textura de nieve para las elevaciones de mayor altura. El primer paso es dividir el terreno en regiones según la elevación. Recuerde lo indicado en el artículo anterior sobre la posibilidad de representar 256 elevaciones distintas con un mapa de elevación en escala de grises. Si divide ese valor por cuatro, cada región tendrá 64 unidades de elevación. TexturaRango de elevación Arena 0-64 Hierba 64-128 Roca 128-192 Nieve 192-256 Cada una de estas regiones se asocia a un archivo de textura, lo que significa que la textura del terreno resultante utilizará el color de la textura en esa ubicación.

Sin embargo, existe un ajuste más que debemos agregar. En lugar de usar simplemente un mismo color para todas las elevaciones dentro de un rango determinado, queremos mezclar las texturas de modo que haya un cambio más gradual entre las texturas de una región a otra. Esto se logra calculando el porcentaje de cada textura en cada elevación y luego combinando los colores. Para crear el mapa de textura, agregué otra Solución al código fuente llamada TerrainGenerator. Esta aplicación de consola ocupa cuatro archivos de textura y un mapa de elevación en formato RAW y crea la textura de terreno. Si estaba utilizando alguno de los métodos que mencioné en el artículo anterior para generar automáticamente el mapa de elevación, podría combinar estas dos características y autogenerar el terreno para cada nivel de una sola vez. Esta decisión de implementación se la dejo a usted. Todos los códigos del método principal sólo administran los parámetros de entrada y realizan cierta validación, por lo que no abordaré este tema en detalle. El punto básico de la aplicación es el método CreateTerrainTexture. Después de configurar ciertas matrices y variables para retener los distintos datos, entramos en un doble bucle que realiza el trabajo real.
for ( int z = 0; z < _numberOfVerticesZ; z++ ) { for ( int x = 0; x < _numberOfVerticesX; x++ ) { // Implementation code below } }

El primer paso es obtener el valor de elevación de las coordenadas en cuestión.
elevation =(float) heightmap[ ( z * _numberOfVerticesZ ) + x ];

Con ese valor podemos calcular el porcentaje de textura (por ejemplo, qué cantidad de cada textura será visible en esa elevación). Dado que tenemos cuatro texturas en este ejemplo, hacemos esto cuatro veces.
textureFactor[0] textureFactor[1] textureFactor[2] textureFactor[3] = = = = ComputeTexturePercent ComputeTexturePercent ComputeTexturePercent ComputeTexturePercent ( ( ( ( 256, elevation ); 192, elevation ); 128, elevation ); 64, elevation );

El método ComputeTexturePercent real devuelve un valor entre 0,0 y 1,0 que representa el porcentaje de la textura visible a una altura dada.
private static float ComputeTexturePercent ( float h1, float elevation ) { float regionSize = 256/4; float texturePercent = ( regionSize - Math.Abs ( elevation-h1 ) ) / regionSize; if ( texturePercent < 0.0f ) texturePercent = 0.0f; else if ( texturePercent > 1.0f ) texturePercent = 1.0f; return texturePercent; }

El siguiente paso es obtener los valores RGB para la coordenada de cada una de las texturas.
for ( int i = 0; i < 4; i++ ) { Color color = textures[i].GetPixel ( x, z ); oldR[i] = color.R; oldG[i] = color.G; oldB[i] = color.B;

}

Luego, este valor "antiguo" se utiliza junto con el factor de la textura para calcular el nuevo valor RGB.
newR = ( ( textureFactor[0] * oldR[0] ) + ( textureFactor[1] * oldR[1] ) + ( textureFactor[2] * oldR[2] ) + ( textureFactor[3] * oldR[3] ) ); newG = ( ( textureFactor[0] * oldG[0] * oldG[1] ) + ( textureFactor[2] * oldG[2] ) + oldG[3] ) ); ) + ( textureFactor[1] ( textureFactor[3] *

newB = ( ( textureFactor[0] * oldB[0] ) + ( textureFactor[1] * oldB[1] ) + ( textureFactor[2] * oldB[2] ) + ( textureFactor[3] * oldB[3] ) );

El paso final es crear un nuevo color y definir el píxel en la textura del terreno para ese color.
Color newColor = Color.FromArgb ( (int)newR, (int)newG, (int)newB ); finalTexture.SetPixel ( x, z, newColor );

Cuando está todo listo, el resultado es el siguiente.

El enfoque que utilizamos para crear la textura del terreno es el más simple. Tal como afirmé en el artículo anterior, hay personas que se dedican exclusivamente a trabajar en mejores formas para crear terrenos que parezcan reales. Algunas mejoras que se podrían hacer a nuestro enfoque son: •Incluir una pendiente. Por ejemplo: la nieve no se pegaría a una superficie casi vertical, sino al terreno subyacente que aparecería. Podría representar esto mostrando una textura de nieve cuando la pendiente varíe de 0% a 80%, pero mostrando roca cuando la pendiente supere el 80%. •El ángulo de la luz del sol y las áreas con sombra que ésta origina. Podría haber nieve en las elevaciones menores que estén a la sombra, pero no en aquéllas superiores que reciben la luz del sol. •Clima. Crecerá vegetación distinta en los climas húmedos y el terreno drenará de distinta forma dependiendo de la pendiente y de la capacidad de escurrir del agua. •El uso de distintas texturas para el mismo rango de elevación (por ejemplo, distintas texturas de hierba) aumenta el realismo Independientemente de cuán preciso sea al crear el terreno, la resolución de la textura del terreno tiene el mayor impacto sobre el realismo que exhibirá la escena. Como siempre, debe aprender a equilibrar el costo de cargar un archivo de textura de gran tamaño que proporciona un alto nivel de detalle con el costo de su rendimiento. Antes de integrar el terreno generado, debemos (o quiero) actualizar la forma en que movemos la cámara.

Principio de la página

Nueva cámara
La clase camera que hemos utilizado hasta ahora cumple con su tarea, pero deseo reemplazarla por la implementación más tradicional de una cámara de Acción en primera persona. Revise este artículo para consultar la teoría detrás de los cambios: http://www.toymaker.info/Games/html/camera.html. Cuando nos desplazamos sobre el terreno, mantenemos los valores de cabeceo en torno al eje x definidos por el mouse (ratón). Para hacer que la cámara actúe en primera persona de manera más realista, debemos tomar el control de la Guiñada, la Rotación y el Cabeceo de la cámara. Esto nos permite ajustar la cámara al terreno subyacente. Por ejemplo: si vamos conduciendo por el costado de una colina empinada, el cabeceo y la rotación de la cámara deben corresponder a la pendiente en la cual nos encontramos. La guiñada de la cámara es la misma que en el encabezado y los cambios sólo deben basarse en la intervención del usuario, no en las características del terreno. Guiñada, Cabeceo y Rotación se combinan para describir la actitud de un objeto. La Guiñada expresa la rotación alrededor del eje Y; el Cabeceo, del eje X; y la Rotación, del eje Z.

Para implementar este cambio, vamos a agregar un par de métodos a la clase camera y a hacer otras modificaciones menores. En primer lugar, agregamos un método para controlar la Guiñada, el Cabeceo y la Rotación.
public void AdjustYaw ( float radians ) { Matrix rotation = Matrix.RotationAxis (_up, radians ); _right = Vector3.TransformNormal ( _right, rotation ); _look = Vector3.TransformNormal ( _look, rotation ); } public void AdjustPitch ( float radians ) { _pitch -= radians; if ( _pitch > _maxPitch ) { radians += _pitch - _maxPitch; } else if ( _pitch < -_maxPitch ) { radians += _pitch + _maxPitch; } Matrix rotation = Matrix.RotationAxis ( _right, radians ); _up = Vector3.TransformNormal ( _up, rotation ); _look = Vector3.TransformNormal ( _look, rotation ); } public void AdjustRoll ( float radians )

{ Matrix rotation = Matrix.RotationAxis( _look, radians ); _right = Vector3.TransformNormal( _right, rotation ); _up = Vector3.TransformNormal( _up, rotation ); }

Luego, cambiamos el método Update para calcular la matriz de vista.
public void Update ( ) { if ( Vector3.Length ( _velocity ) > _maxVelocity ) _velocity = Vector3.Normalize ( _velocity ) * _maxVelocity; // Update _postion += _velocity; // Stop _velocity = new Vector3 ( ); _lookAt = _postion + _look; Vector3 up = new Vector3 ( 0.0f, 1.0f, 0.0f ); _viewMatrix = Matrix.LookAtLH ( _postion, _lookAt, up ); _right.X = _viewMatrix.M11; _right.Y = _viewMatrix.M21; _right.Z = _viewMatrix.M31; _up.X = _viewMatrix.M12; _up.Y = _viewMatrix.M22; _up.Z = _viewMatrix.M32; _look.X = _viewMatrix.M13; _look.Y = _viewMatrix.M23; _look.Z = _viewMatrix.M33; float lookLengthOnXZ = (float)Math.Sqrt ( _look.Z * _look.Z + _look.X * _look.X ); _pitch = (float)Math.Atan2 ( _look.Y, lookLengthOnXZ ); _yaw = (float)Math.Atan2 ( _look.X, _look.Z ); ComputeViewFrustum ( ); }

También agregamos un valor máximo de cabeceo y un valor de control para la velocidad de la cámara. La velocidad de la cámara se ajusta con la variable deltaTime para producir un movimiento más parejo.
m_maxPitch = Geometry.DegreeToRadian( 89.0f ) m_maxVelocity = 1.0f;

A continuación, necesitamos crear una instancia un poco diferente con la cámara. En el método Initialize de la clase GameEngine, se agrega el siguiente código
_camera = new Camera ( ); _camera.MaxVelocity = 100.0f; _camera.Position = new Vector3 ( 0.0f, 0.0f, 0.0f ); _camera.LookAt = new Vector3 ( 0.0f, 0.0f, 0.0f ); _camera.Update ( );

Con estos cambios a la clase camera, la integración del terreno en el juego será mucho más fácil. Esto también demuestra que hay muchas formas de realizar la misma tarea. Debe probar las distintas versiones y elegir la que más le guste. Principio de la página

Elevación del terreno
Es posible que en el artículo anterior haya observado que utilizamos la matriz de ajuste de escala para cambiar el tamaño de la malla del terreno antes de la presentación. Estas variables de escala se deben aplicar a todas las resoluciones de coordenadas, así puedo trasladarlas a variables separadas. Recuerde definir agregar _device.RenderState.NormalizeNormals = true para volver a calcular las normales.

En la parte inferior de la clase Terrain se agrega el siguiente código.
private float _scaleX = 1.0f; private float _scaleZ = 1.0f; private float _scaleY = 0.3f;

En el método Render de la clase Terrain, se cambia la línea de código que calcula la matriz de ajuste de escala a:
_device.Transform.World = Matrix.Scaling ( _scaleX, _scaleY, _scaleZ );

Ahora podemos determinar la altura (Y) del terreno en cualquier conjunto de coordenadas dadas, considerando la escala aplicada. Agregue el siguiente método a la clase Terrain.
public float TerrainHeight ( int x, int z ) { x = x / (int)_scaleX; z = z / (int)_scaleZ; if ( _isHeightMapRAW ) return (float)( _elevationsRAW[ ( z * _numberOfVerticesZ ) + x] ) * _scaleY; else return (float)( _elevations[(int)x,(int)z] ) / _scaleY; }

Lo que estamos haciendo es recuperar el valor Y de la matriz de elevación, basándonos en las coordenadas X y Z especificadas. Asegúrese de cambiar los valores de escala de multiplicación a división cuando el valor de escala sea mayor que 1 Ahora debemos ajustar la coordenada Y de la cámara al valor Y correspondiente del terreno. En el método Render de la clase GameEngine, se agrega la siguiente línea de código inmediatamente después de la llamada al método Update de la cámara.
camera.SetHeight ( _terrain.TerrainHeight ( (int)_camera.Position.X, (int)_camera.Position.Z ) );

En este punto, es probable que desee deshabilitar las claves Q y Z, de modo de los jugadores no puedan cambiar los valores Y por su cuenta. El último paso es agregar un nuevo método a la clase camera que nos permite definir directamente la altura de la cámara. La variable heightOfTurret garantiza que estemos levemente por sobre el terreno para evitar el recorte a medida que nos desplazamos. Siga adelante y cambie el valor a 0 para que vea a lo que me refiero.
public void SetHeight ( float y ) { float heightOfTurret = 1.5f; _postion.Y = y + heightOfTurret; }

El efecto neto de estos cambios es que ahora podemos "conducir" a través del terreno. Cuando haga esto debe asegurarse de deshabilitar la entrada para moverse a lo largo del eje Y (Q y Z). Existen muchos otros cambios que deberá hacer para que la cámara y el terreno funcionen en conjunto y así el juego sea una experiencia perfecta. Ya no tengo más espacio, pero vea cómo restringiría el movimiento del jugador/cámara en el terreno (para asegurarse de no caer de la malla del terreno). También podría ajustar el cabeceo y la rotación de la cámara basándose en la pendiente del terreno. Pista: el encabezado es importante en ambas instancias. Principio de la página

Resumen
Existen varios conceptos avanzados de terreno que omití deliberadamente por ahora, pero que retomaré más adelante. Si busca "Terrain DirectX" obtendrá quizás más información de la que puede asimilar, pero recuerde que algunos desarrolladores se

especializan exclusivamente en esto. Hasta entonces: ¡que disfrute la codificación! Principio de la página

Acerca del autor
Derek Pierson es un Evangelista técnico de 3 Leaf Development y se especializa en el desarrollo de Herramientas de evangelización técnica y Asesoría técnica en y sobre dichas tecnologías. Derek tiene más de 10 años de experiencia en diseño y desarrollo de aplicaciones para empresas, en distintos lenguajes de programación. Es un entusiasta profesor del arte de la programación y cree que la elegancia y la simplicidad son características que definen un buen software.

Principio de la página Administre su perfil |Contactar ©2007 Microsoft Corporation. Todos los derechos reservados. Condiciones de uso |Marcas registradas | Declaración de Privacidad