Beginning Game Development: Part VII –Terrain and Collision Detection
Introduction
Welcome to the seventh article on beginning game development. In the last article, we covered how lights and materials can add realism to a scene. We also discussed how to create a terrain from a heightmap, and finally added fonts to the application. In this article, we are going to refine the way the terrain looks and start incorporating it into our game.
Before we start, let's do the obligatory code cleanup, incorporating all the feedback I received.
Code cleanup
The cleanup for this article consists of some minor refactoring to keep the code clean and some changes to the lights and normals for the lighting.
- Handle DeviceResizing event.
- Manage Device events (see next section).
- Changed the ambient and diffuse light to be darker. If you use
Color.White
for the ambient light, there is already 100% so any other light is not visible. Instead, I am now usingColor.FromArgb ( 64, 64, 64 )
for the ambient andColor.FromArgb ( 128, 128, 128 )
for the diffuse light. - Added _
device.RenderState.NormalizeNormals = true;
so that the normals are recalculated when scaling the terrain. You can remove this call if you are not scaling the terrain since it has some performance cost. - In the last article, I incorrectly stated that
presentParams.AutoDepthStencilFormat = DepthFormat.D16;
was setting up a DepthStencil, this code is actually settings up a DepthBuffer. For a clarification check out these links:
Device Handling
Up to this point we have pretended that once we have initialized the graphics device nothing can go wrong until we are done using it. In the real world things do go wrong, and in the DirectX world losing the device can happen in response to a number of user actions, such as Alt-tabbing from application to application, minimizing a window, or certain power management functions.
Once the device has been lost, all render functions will silently fail and the render operations will not return an error code even though they are really failing. Any calls to render functions, such as
DrawIndexedPrimitives
are discarded until the device is reset. Any call to Device.Present
however will cause an exception.
There are two exceptions that are associated with lost devices:
DeviceLostException: The device is lost and cannot be reset.
DeviceNotResetException: The device is lost and can be reset.
To detect the device lost state, DirectX provides the DeviceLost event for the Deviceobject. Once the Device has been lost, it must be reset to function properly. You can also check if a device is in the lost state using the CheckCopperativeLevel property of the device.
Once the Device is lost the application will query the Device and determine if it can be restored to the operational state. If it can be restored, then all video memory resources placed in Pool.Default and swap chains are destroyed and the Reset method is called. Resources created in Pool.Managed are backed by system memory and do not have to be destroyed and recreated. If the device cannot be reset then the application waits until it can be restored.
The Reset method is the only method that has any effect on the application once a device has been lost and is the only way to change a device from the lost state to the operational state. The Reset method will fail unless the application has released all resources allocated, so resources created in the DeviceReset event should be destroyed in the DeviceLost event.
Enough theory. We need to update BattleTank2005 to detect the device reset and lost events and react to them.
At the bottom of the ConfigureDevice method of the GameEngine class add the following code:
Visual C#
_device.DeviceReset+=new EventHandler(OnDeviceReset);
_device.DeviceLost+=new EventHandler(OnDeviceLost);
_device.Disposing+=new EventHandler(OnDeviceDisposing);
OnDeviceReset ( this, null );
Visual Basic
AddHandler m_device.DeviceLost, AddressOf OnDeviceLost
AddHandler m_device.DeviceReset, AddressOf OnDeviceReset
AddHandler m_device.Disposing, AddressOf OnDeviceDisposing
OnDeviceReset(Me, Nothing)
Now add the following three methods to the GameEngine class:
Visual C#
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 ( );
}
Visual Basic
Private Sub OnDeviceReset(ByVal sender As Object, ByVal e As EventArgs)
If (Not (m_font) Is Nothing) Then
m_font.OnResetDevice()
End If
m_device.Transform.Projection = m_camera.Projection
' Turn on some low level ambient light
m_device.RenderState.Ambient = Color.FromArgb(64, 64, 64)
m_device.RenderState.NormalizeNormals = True
End Sub
Private Sub OnDeviceLost(ByVal sender As Object, ByVal e As EventArgs)
If Not (m_font Is Nothing) AndAlso m_font.Disposed = False Then
m_font.OnLostDevice()
End If
End Sub
Private Sub OnDeviceDisposing(ByVal sender As Object,
ByVal e As EventArgs)
If (Not (m_mouse) Is Nothing) Then
m_mouse.Dispose()
End If
If (Not (m_keyboard) Is Nothing) Then
m_keyboard.Dispose()
End If
If (Not (m_terrain) Is Nothing) Then
m_terrain.Dispose()
End If
End Sub
Check out the comments in the code for a more detailed explanation.
Textures
In the last article we used a single texture file and stretched it across the terrain to make the terrain more realistic. Textures provide us with the ability to map images onto the surfaces of our shapes
When you apply a 2D texture to a 3D shape you need to somehow match the texture coordinates to the coordinates of the shape. You accomplish this by mapping the coordinates of the texture to the coordinates of the shape. The Tu and Tv values are nothing other than the X/Y coordinates of the texture (a single u,v pair is also called a "texel"). The main difference is that texture coordinates are two dimensional and fall into the range of 0.0f – 1.0f.
In the code we simply assign each texture coordinate to the matching mesh coordinate by dividing it by the number of quads. Doing this results in a single texel spanning a single quad of the terrain.
Visual C#
vertex.Tu = (float)x / _numberOfQuadsX;
vertex.Tv = (float)z / _numberOfQuadsZ
Visual Basic
vertex.Tu = CType(x, Single) / _numberOfQuadsX
vertex.Tv = CType(z, Single) / _numberOfQuadsZ
In addition to this simple approach you could also use multiple textures at a time. This is called multitexturing and is a topic well worth exploring for any developer trying to create the most realistic terrain. We are only using single texture right now, but multitexturing explains why we passed a zero to the SetTexture call in the Render method of theTerrain class. The zero, as you may have guessed identified the texture as the first texture to be used.
Visual C#
_device.SetTexture ( 0, _terrainTexture );
Visual Basic
_device.SetTexture(0, _terrainTexture)
There are a number of other tweaks you can do to the way the texture is displayed but none really changes the way we are using the terrain texture in code.
DirectX uses a technique called "Filtering" to smooth out any distortions caused when a texture triangle is magnified or minified to fit the screen triangle. You should experiment with changing the SamplerStageStates value passed to the Device.SetSamplerStatemethod. As always you are making a tradeoff between speed and detail.
Another solution to the distortion problem is to use mipmaps. This approach uses a series of smaller lower resolution textures chained together. If you use the FromFilemethod of the TextureLoader, DirectX automatically generates a mipmap chain. If you are graphically inclined or want to squeeze the best possible result from the texture you can also use the texture editor included with the DirectX SDK (DxTex.exe) to generate a mipmap chain. (This editor is also useful in a number of other texture manipulation functions.)
The result we achieved using the Down.jpg file in BattleTank2005 was passable but it was pretty boring. If you use a third party tool you can create a terrain texture customized for your heightmap with much better results than we achieved, but what if you want to generate random terrain for each level? The answer is procedural terrain generation.
Procedural Terrain Generation
The texture we used was the bottom of the skybox, and the coloration and highlights in the texture had no relationship to the landscape created by the heightmap. Another option is to use this same technique, but use a texture file specifically created for the heightmap (or more accurately the heightmap and texture are created at the same time) using a program such as Terragen. This approach lets you fiddle with the texture file until it is exactly the way you want it, but it is fairly labor intensive, especially if you want to offer the player a large number of maps to play on.
Yet another approach is to generate the texture file with a heightmap and a couple of texture files using a process called "Procedural Texture Generation." With this approach you can use texture files to represent the terrain at various heights. For example, you could specify a sand texture for low elevations, grass texture for the next elevation levels, rock texture for the next and finally a snow texture for the highest elevations.
The first step is to separate the terrain into regions by elevations. Remember for the last article that we can represent 256 distinct elevations using a grayscale heightmap. If you divide that value by four then each region covers 64 elevation units.
Texture | Elevation Range |
Sand | 0-64 |
Grass | 64-128 |
Rock | 128-192 |
Snow | 192-256 |
Each of these regions is associated with a texture file which means that the resulting terrain texture will use the color of the texture at that location.
There is one more refinement we need to add, however. Rather than simply using the same color for all elevations within a range we want to blend the textures so that there is a more gradual change of textures from region to region. We accomplish this by calculating the percentage of each texture at each elevation and then blending the colors.
To create the texture map I added another Solution to the source code calledTerrainGenerator. This console application takes four texture files and one heightmap in RAW format and creates the terrain texture. If you were using some of the methods I mentioned in my previous article to automatically generate the heightmap, you could combine these two features and auto-generate the terrain for each level on the fly. I will leave that implementation up to you.
All of the code in the main method simply manages the input parameters and does some validation so I will not cover it in detail. The real meat of the application is theCreateTerrainTexture method. After setting up some arrays and variables to hold the various pieces of data we enter a double loop which performs the actual work.
Visual C#
for ( int z = 0; z < _numberOfVerticesZ; z++ )
{
for ( int x = 0; x < _numberOfVerticesX; x++ )
{
// Implementation code below
}
}
Visual Basic
Dim z As Integer = 0
While z < _numberOfVerticesZ
Dim x As Integer = 0
While x < _numberOfVerticesX
'Implementation code below
x = x + 1
End While
z = z + 1
End While
The first step is to get the elevation value at the coordinates in question.
Visual C#
elevation =(float) heightmap[ ( z * _numberOfVerticesZ ) + x ];
Visual Basic
elevation = CType(heightmap(((z * _numberOfVerticesZ) + x)), Single)
Using this value we then compute the texture percent (for example, how much of each texture is visible at that elevation). Since we have four textures in this sample we do this four times.
Visual C#
textureFactor[0] = ComputeTexturePercent ( 256, elevation );
textureFactor[1] = ComputeTexturePercent ( 192, elevation );
textureFactor[2] = ComputeTexturePercent ( 128, elevation );
textureFactor[3] = ComputeTexturePercent ( 64, elevation );
Visual Basic
textureFactor(0) = ComputeTexturePercent(256, elevation)
textureFactor(1) = ComputeTexturePercent(192, elevation)
textureFactor(2) = ComputeTexturePercent(128, elevation)
textureFactor(3) = ComputeTexturePercent(64, elevation)
The actual ComputeTexturePercent method returns a value between 0.0 and 1.0 representing the percent of the texture visible at a given height.
Visual C#
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;
}
Visual Basic
Private Function ComputeTexturePercent(ByVal h1 As Single,
ByVal elevation As Single) As Single
Dim regionSize As Single = (256 / 4)
Dim texturePercent As Single = ((regionSize - Math.Abs((
elevation - h1))) / regionSize)
If (texturePercent < 0.0!) Then
texturePercent = 0.0!
ElseIf (texturePercent > 1.0!) Then
texturePercent = 1.0!
End If
Return texturePercent
End Function
The next step is to get the RGB values for the coordinate from each of the textures.
Visual C#
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;
}
Visual Basic
Dim i As Integer = 0
Do While (i < 4)
Dim color As Color = textures(i).GetPixel(x, z)
oldR(i) = color.R
oldG(i) = color.G
oldB(i) = color.B
i = (i + 1)
Loop
This "old" value is then used together with the texture factor to compute the new RGB value.
Visual C#
Dim i As Integer = 0
Do While (i < 4)
Dim color As Color = textures(i).GetPixel(x, z)
oldR(i) = color.R
oldG(i) = color.G
oldB(i) = color.B
i = (i + 1)
Loop
Visual Basic
newR = ((textureFactor(0) * oldR(0)) +
((textureFactor(1) * oldR(1)) +
((textureFactor(2) * oldR(2)) +
(textureFactor(3) * oldR(3)))))
newG = ((textureFactor(0) * oldG(0)) +
((textureFactor(1) * oldG(1)) +
((textureFactor(2) * oldG(2)) +
(textureFactor(3) * oldG(3)))))
newB = ((textureFactor(0) * oldB(0)) +
((textureFactor(1) * oldB(1)) +
((textureFactor(2) * oldB(2)) +
(textureFactor(3) * oldB(3)))))
The final step is to create the new color and set the pixel in the terrain texture to that color.
Visual C#
Color newColor = Color.FromArgb ( (int)newR, (int)newG, (int)newB );
finalTexture.SetPixel ( x, z, newColor );
Visual Basic
Dim newColor As Color = Color.FromArgb(CType(newR, Integer), CType(newG, Integer), CType(newB, Integer)) finalTexture.SetPixel(x, z, newColor)
When all is said and done, this is the result.
The approach we took to create the terrain texture is the simplest approach possible. As I stated in the last article there are people that do nothing else but work on better ways to create realistic-looking terrain. Some enhancements that could be made to our approach are:
- Take into account slope. For example: Snow would not stick to an almost vertical surface, instead the underlying terrain would show. You could represent this as showing the snow texture when the slope is between 0% and 80% but show rock when the slope is greater then 80%.
- Angle of the sun and the shade areas resulting from this. Snow might exist at lower elevations that are in the shade, but not at higher elevations not in the shade.
- Climate. Different vegetation will grow in wetter climates and the terrain will drain differently depending on slope and ability of the water to run off.
- Using a number of different textures for the same elevation range (i.e. different grass textures) increases the realism.
Regardless of how precise you are when creating the terrain, the resolution of the terrain texture has the single biggest impact on how realistic the scene looks. As usual you must learn how to balance the cost of loading a large texture file that provides a high level of detail with the performance cost.
Before we integrate the terrain generated we need to (or I want to) update the way we move the camera around.
New Camera
The camera class we have used so far does its job, but I want to replace it with a more traditional implementation of a First Person Shooter camera.
Check out this article to see the theory behind the changes:http://www.toymaker.info/Games/html/camera.html.
As we are moving around on the terrain we maintain the pitch values provided by the mouse. To make the camera a more realistic first person camera we need to take control the Yaw, Roll and Pitch of the camera. This way we can adjust the camera to the underlying terrain. For example: if we are driving up the side of a steep hill, the pitch and roll of the camera should be equal to the slope we are on. The Yaw of the camera is the same as the heading and changes only based on user input, not on any terrain features.
Yaw, Pitch and Roll combine to describe the attitude of an object. Yaw expresses the rotation around the Y axis, Pitch the X axis and Roll the Z axis.
To implement this change we are going to add a couple of methods to the camera class and make some other minor modifications.
First we add a method each to control the Yaw, Pitch and Roll.
Visual C#
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 );
}
Visual Basic
Public Sub AdjustYaw(ByVal radians As Single)
If (radians = 0.0!) Then
Return
End If
Dim rotation As Matrix = Matrix.RotationAxis(_up, radians)
_right = Vector3.TransformNormal(_right, rotation)
_look = Vector3.TransformNormal(_look, rotation)
End Sub
Public Sub AdjustPitch(ByVal radians As Single)
If (radians = 0.0!) Then
Return
End If
_pitch = (_pitch - radians)
If (_pitch > _maxPitch) Then
radians = (radians _
+ (_pitch - _maxPitch))
ElseIf (_pitch _
< (_maxPitch * -1)) Then
radians = (radians _
+ (_pitch + _maxPitch))
End If
Dim rotation As Matrix = Matrix.RotationAxis(_right, radians)
_up = Vector3.TransformNormal(_up, rotation)
_look = Vector3.TransformNormal(_look, rotation)
End Sub
Public Sub AdjustRoll(ByVal radians As Single)
If (radians = 0.0!) Then
Return
End If
Dim rotation As Matrix = Matrix.RotationAxis(_look, radians)
_right = Vector3.TransformNormal(_right, rotation)
_up = Vector3.TransformNormal(_up, rotation)
End Sub
Then we change the Update method to compute the View matrix.
Visual C#
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 ( );
}
Visual Basic
Public Sub Update()
If (Vector3.Length(_velocity) > _maxVelocity) Then
_velocity = (Vector3.Normalize(_velocity) * _maxVelocity)
End If
' Update
_postion = (_postion + _velocity)
' Stop
_velocity = New Vector3
_lookAt = (_postion + _look)
Dim up As Vector3 = New Vector3(0.0!, 1.0!, 0.0!)
_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
Dim lookLengthOnXZ As Single = CType(Math.Sqrt(((_look.Z * _look.Z) _
+ (_look.X * _look.X))), Single)
_pitch = CType(Math.Atan2(_look.Y, lookLengthOnXZ), Single)
_yaw = CType(Math.Atan2(_look.X, _look.Z), Single)
ComputeViewFrustum()
End Sub
We also added a max pitch value and a control value for the camera speed. The camera speed is adjusted using the deltaTime variable to yield a smoother motion.
Visual C#
m_maxPitch = Geometry.DegreeToRadian( 89.0f )
m_maxVelocity = 1.0f;
Visual Basic
Dim _maxPitch As Single = Geometry.DegreeToRadian(89.0F)
Dim _maxVelocity As Single = 1.0F
Next we need to instantiate the camera a little differently. In the Initialize method of the GameEngine class add the following code.
Visual C#
_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 ( );
Visual Basic
m_camera = New Camera
m_camera.MaxVelocity = 100.0F
m_camera.Position = New Vector3(0.0F, 0.0F, 0.0F)
m_camera.LookAt = New Vector3(0.0F, 0.0F, 0.0F)
m_camera.Update()
With these changes to the camera class, integrating the terrain into the game will be a lot easier. It also shows you that there are many ways of accomplishing the same task. You should experiment with the various versions and choose the one you like best.
Terrain Elevation
In the last article you may have noticed that we used the Scaling matrix to change the size of the terrain mesh before rendering. These scale variables must be applied to any coordinate resolutions, so I moved them into separate variables. Remember to set add
_device.RenderState.NormalizeNormals = true
to recompute the normals.
At the bottom of the Terrain class add the following code.
Visual C#
private float _scaleX = 1.0f;
private float _scaleZ = 1.0f;
private float _scaleY = 0.3f;
Visual Basic
Dim _scaleX As Single = 4.0!
Dim _scaleZ As Single = 4.0!
Dim _scaleY As Single = 0.5!
In the Render method of the Terrain class change the line of code that computes up the scaling matrix to:
Visual C#
_device.Transform.World = Matrix.Scaling ( _scaleX, _scaleY, _scaleZ );
Visual Basic
_device.Transform.World = Matrix.Scaling(_scaleX, _scaleY, _scaleZ)
Now we can determine the height (Y) of the terrain at any given coordinate set, taking into account the scale applied. Add the following method to the Terrain class.
Visual C#
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;
}
Visual Basic
Public Function TerrainHeight(ByVal x As Integer, ByVal z As Integer) As Single
x = (x / CType(_scaleX, Integer))
z = (z / CType(_scaleZ, Integer))
If m_isHeightMapRAW Then
Return (CType(_elevationsRAW(((z * _numberOfVerticesZ) + x)),
Single) * _scaleY)
Else
Return (CType(_elevations(CType(x, Integer), CType(z, Integer)),
Single) / _scaleY)
End If
End Function
All we are doing is retrieving the Y value from the elevation array based on the X and Z coordinates passed in. Make sure to change the scaling values from multiplication to division when the scaling value is greater then 1.
Now we need to adjust the Y coordinate of the camera to the appropriate Y value of the terrain. In the Render method of the GameEngine class add the following line of code immediately after the call to the Update method of the camera.
Visual C#
camera.SetHeight ( _terrain.TerrainHeight ( (int)_camera.Position.X, (int)_camera.Position.Z ) );
Visual Basic
m_camera.SetHeight(m_terrain.TerrainHeight(m_camera.Position.X,
m_camera.Position.Z))
At this point you would probably want to disable the Q and Z keys so the players cannot change the Y values on their own. The last step is to add a new method to the camera class that allows us to directly set the height of the camera. The heightOfTurret variable ensures that we are slightly above the terrain to avoid clipping as we drive around. Go ahead and change the value to 0 to see what I mean.
Visual C#
public void SetHeight ( float y )
{
float heightOfTurret = 1.5f;
_postion.Y = y + heightOfTurret;
}
Visual Basic
Public Sub SetHeight(ByVal y As Single)
Dim heightOfTurret As Single = 1.5!
_postion.Y = (y + heightOfTurret)
End Sub
The net effect of these changes is that we can now "drive" across the terrain. You need to make sure to disable the input for moving along the Y axis (Q and Z) when you are doing this. There are a number of other changes that need to be made to make the camera and the terrain work together for a smooth game experience. I am out of space but you can see how you would constrain the movement of the player/camera to the extents of the terrain (to make sure we don't fall of the terrain mesh). You could also adjust the pitch and roll of the camera based on the slope of the terrain. Hint: the heading is important in both instances.
Summary
There are a number of more advanced terrain concepts which I purposely skipped for right now but will return to later on. If you search for "Terrain DirectX" you will get more information then you can possibly digest, but then remember that some developers specialize in nothing else.
Until then: Happy coding.
0 comments:
Post a Comment