
In Part 1, we created a basic game that included using sensors to move a spaceship on the screen, and we added a couple of effects to give the impression of movement.
In this next version, I set out to capture voice commands to make the spaceship fire. To achieve this, we will set the following objectives:
- Convert voice to text
- Make the spaceship fire
- Display some enemies
- Detect when any of the spaceships gets hit
To convert voice to text, I didn’t want to reinvent the wheel, and I had already seen that Gerald Versluis had a great article on this, so we will base our implementation on his example. If you want to see the implementation, you can find it here:
https://github.com/jfversluis/MauiSpeechToTextSample/tree/main
Once the voice service is implemented, we will need to create a new class called Shot that will represent a shot, and we will create a method called FireShip that will be responsible for generating a new shot.
The Shot class will look like this:
private class Shot
{
public float X, Y;
public const float Speed = 10f;
public Shot(float x, float y)
{
X = x;
Y = y;
}
public void Move(bool down = false)
{
Y += down ? Speed : -Speed;
}
}The method should look like this:
private async Task FireShip()
{
float centerX = canvasView.CanvasSize.Width / 2;
float centerY = canvasView.CanvasSize.Height / 2;
activeShots.Add(new Shot(centerX, centerY));
Console.WriteLine($"Shoot added on: ({centerX}, {centerY})");
MainThread.BeginInvokeOnMainThread(() => canvasView.InvalidateSurface());
}The painting of the shots will be done again in the PaintSurface event, so we will add the following portion of code:
// Draw shots
using (var shotPaint = new SKPaint { Color = SKColors.Red, StrokeWidth = 10 })
{
for (int i = activeShots.Count - 1; i >= 0; i--)
{
var shot = activeShots[i];
canvas.DrawLine(shot.X, shot.Y, shot.X, shot.Y - 30, shotPaint);
shot.Move();
if (shot.Y < 0)
{
activeShots.RemoveAt(i);
Console.WriteLine("Shoot deleted");
}
}
}The code basically takes from a collection of active shots and draws a line that moves successively across the screen until it disappears. Once it goes out of view, it is removed from the collection. We will also add a small indicator that will show at the top of the screen how many shots are active, mainly for debugging purposes:
// Draw an indicator
using (var textPaint = new SKPaint
{
Color = SKColors.White,
TextSize = 30,
IsAntialias = true
})
{
canvas.DrawText($"Active shoots: {activeShots.Count}", 10, 30, textPaint);
}If you run the project now, nothing will happen, haha. So, we will subscribe to the voice detected event to make our super shot. This will be done in the OnAppearing event of the project:
override protected async void OnAppearing()
{
base.OnAppearing();
await _voiceRecognitionService.RequestPermissions();
_= _voiceRecognitionService.Listen(CultureInfo.GetCultureInfo("en-us"), new Progress<string>(partialText =>
{
listenedText = partialText;
if (!string.IsNullOrEmpty(listenedText))
{
_voiceRecognitionService_OnSpeechRecognized(this, partialText);
OnPropertyChanged(nameof(listenedText));
}
}), tokenSource.Token);
}
private void _voiceRecognitionService_OnSpeechRecognized(object? sender, string e)
{
var words = e.ToLower().Split(' ');
foreach (var word in words)
{
if (word.Contains("pew") || word.Contains("pure") || word.Contains("pum") || word.Contains("boom"))
{
pewTimes.Enqueue(DateTime.Now);
}
float x = (float)random.NextDouble() * canvasView.CanvasSize.Width;
float y = canvasView.CanvasSize.Height - 50;
recognizedWords.Add(new RecognizedWord(word, x, y));
}
}In this method, we will receive a string that can contain several words depending on what is identified, so we will try to read some common words to trigger the shot. In this case, I tried to detect “pew,” “pum,” and “boom” as some common words to execute a shot. If any of these words is detected, a new shot is queued in the collection.
If you run the project now, the spaceship will be able to shoot! But I know, what’s the fun in shooting if I don’t have anything to destroy! So now we’ll create a method to draw the enemy spaceships, and we’ll base it on the same design as the player spaceship but change the colors a bit to differentiate between good and bad guys. But first, let’s create a new entity to represent the enemies:
private class EnemyShip
{
public float X, Y;
public List<Shot> Shots = new List<Shot>();
private Random random = new Random();
public EnemyShip(float x, float y)
{
X = x;
Y = y;
}
public void Move()
{
// Simple random movement
X += (float)(random.NextDouble() * 4 - 2);
Y += (float)(random.NextDouble() * 4 - 2);
}
public void Fire()
{
Shots.Add(new Shot(X, Y));
}
public void UpdateShots()
{
for (int i = Shots.Count - 1; i >= 0; i--)
{
Shots[i].Move(true); // true for moving down
if (Shots[i].Y > 1000) // Adjust based on your game's scale
{
Shots.RemoveAt(i);
}
}
}
}Now, let’s draw the enemies:
private void DrawEnemyShip(SKCanvas canvas, EnemyShip enemy, SKImageInfo info)
{
float x = enemy.X - shipPosition.X;
float y = enemy.Y - shipPosition.Y;
// Rotate the camera and apply 3D effect (similar to stars)
x += cameraRotationY * 20;
y += cameraRotationX * 20;
float z = 1000 + (x * x + y * y) / 20000;
x = x * 1000 / z + info.Width / 2;
y = y * 1000 / z + info.Height / 2;
// Check if the enemy is visible on screen
if (x >= 0 && x <= info.Width && y >= 0 && y <= info.Height)
{
canvas.Save();
canvas.Translate(x, y);
canvas.Scale(1000 / z);
using (var paint = new SKPaint { IsAntialias = true })
{
// Enemy ship body
paint.Color = new SKColor(192, 57, 43); // Dark Red
var bodyPath = new SKPath();
bodyPath.MoveTo(-20, 15);
bodyPath.LineTo(20, 15);
bodyPath.LineTo(0, -30);
bodyPath.Close();
canvas.DrawPath(bodyPath, paint);
// Enemy wings
paint.Color = new SKColor(231, 76, 60); // Red
var leftWing = new SKPath();
leftWing.MoveTo(-20, 15);
leftWing.LineTo(-40, 30);
leftWing.LineTo(-15, 0);
leftWing.Close();
canvas.DrawPath(leftWing, paint);
var rightWing = new SKPath();
rightWing.MoveTo(20, 15);
rightWing.LineTo(40, 30);
rightWing.LineTo(15, 0);
rightWing.Close();
canvas.DrawPath(rightWing, paint);
// Enemy cabin
paint.Color = new SKColor(52, 152, 219); // Blue
canvas.DrawCircle(0, -5, 10, paint);
}
canvas.Restore();
}
}This method will also be called from the PaintSurface event.
// Draw enemy ships
foreach (var enemy in enemies)
{
DrawEnemyShip(canvas, enemy, info);
}If you run the project now, what you will see is that the line moves up the screen but passes through the enemies. What we need to detect now is when one of the shots hits any enemy spaceship, so we will create a method that will handle detecting these types of collisions. We’ll call it CheckCollisions:
private void CheckCollisions()
{
float screenCenterX = canvasView.CanvasSize.Width / 2;
float screenCenterY = canvasView.CanvasSize.Height / 2;
// Check player shots against enemies
for (int i = activeShots.Count - 1; i >= 0; i--)
{
var shot = activeShots[i];
for (int j = enemies.Count - 1; j >= 0; j--)
{
var enemy = enemies[j];
// Calculate screen positions
float enemyScreenX = screenCenterX + (enemy.X - shipPosition.X);
float enemyScreenY = screenCenterY + (enemy.Y - shipPosition.Y);
float shotScreenX = shot.X;//screenCenterX + (shot.X - shipPosition.X);
float shotScreenY = shot.Y; // screenCenterY + (shot.Y - shipPosition.Y);
float distance = (float)Math.Sqrt(
Math.Pow(shotScreenX - enemyScreenX, 2) +
Math.Pow(shotScreenY - enemyScreenY, 2));
Console.WriteLine($"Shot ({shotScreenX},{shotScreenY}) Enemy ({enemyScreenX},{enemyScreenY}) Distance: {distance}");
if (distance < 50) // Adjust this value as needed
{
explosions.Add(new Explosion(enemy.X, enemy.Y));
activeShots.RemoveAt(i);
enemies.RemoveAt(j);
Console.WriteLine("Enemy hit!");
break;
}
}
}
// Check enemy shots against player (simplified collision)
foreach (var enemy in enemies)
{
for (int i = enemy.Shots.Count - 1; i >= 0; i--)
{
var shot = enemy.Shots[i];
if (Math.Abs(shot.X - shipPosition.X) < 30 &&
Math.Abs(shot.Y - shipPosition.Y) < 30)
{
// Player hit! Implement game over logic here
enemy.Shots.RemoveAt(i);
Console.WriteLine("Player hit!"); // Debug output
explosions.Add(new Explosion(shipPosition.X, shipPosition.Y));
}
}
}
}But since we not only want to detect the collision but also show an effect similar to an explosion, we will create a couple of new classes called Explosion and ExplosionParticle that will be responsible for this effect:
private class Explosion
{
public float X { get; private set; }
public float Y { get; private set; }
private const int ParticleCount = 50;
private List<ExplosionParticle> particles;
private const int Duration = 60; // frames
private int currentFrame = 0;
public bool IsFinished => currentFrame >= Duration;
public Explosion(float x, float y)
{
X = x;
Y = y;
particles = new List<ExplosionParticle>();
for (int i = 0; i < ParticleCount; i++)
{
particles.Add(new ExplosionParticle(x, y));
}
}
public void Update()
{
currentFrame++;
foreach (var particle in particles)
{
particle.Update();
}
}
public void Draw(SKCanvas canvas, SKPoint shipPosition, SKImageInfo info,float cameraRotationX, float cameraRotationY)
{
float x = X - shipPosition.X;
float y = Y - shipPosition.Y;
// Apply the rotation of the camera
x += cameraRotationY * 20;
y += cameraRotationX * 20;
float z = 1000 + (x * x + y * y) / 20000;
x = x * 1000 / z + info.Width / 2;
y = y * 1000 / z + info.Height / 2;
using (var paint = new SKPaint { IsAntialias = true })
{
foreach (var particle in particles)
{
float alpha = 1 - (float)currentFrame / Duration;
paint.Color = particle.Color.WithAlpha((byte)(255 * alpha));
canvas.DrawCircle(
x + particle.OffsetX * (1000 / z),
y + particle.OffsetY * (1000 / z),
particle.Size * (1000 / z),
paint
);
}
}
}
}
private class ExplosionParticle
{
public float OffsetX { get; private set; }
public float OffsetY { get; private set; }
public float VelocityX { get; private set; }
public float VelocityY { get; private set; }
public float Size { get; private set; }
public SKColor Color { get; private set; }
private static Random random = new Random();
public ExplosionParticle(float x, float y)
{
OffsetX = 0;
OffsetY = 0;
float angle = (float)(random.NextDouble() * Math.PI * 2);
// Increase the speed
float speed = (float)(random.NextDouble() * 4 + 2);
VelocityX = (float)Math.Cos(angle) * speed;
VelocityY = (float)Math.Sin(angle) * speed;
// Increase the size
Size = (float)(random.NextDouble() * 5 + 2);
Color = new SKColor(
(byte)random.Next(200, 256),
(byte)random.Next(100, 200),
(byte)random.Next(50)
);
}
public void Update()
{
OffsetX += VelocityX;
OffsetY += VelocityY;
VelocityY += 0.1f;
Size *= 0.98f;
}
}Collision detection will be implemented through a timer that we will run when the screen is displayed, and from there, we will process the shot queue and detect the collisions to make the necessary drawings:
Dispatcher.StartTimer(TimeSpan.FromMilliseconds(16), () =>
{
MainThread.BeginInvokeOnMainThread(() =>
{
UpdateGame();
ProcessPewQueue();
canvasView.InvalidateSurface();
});
return true; // Return true to keep the timer running
});The final result should look like this:

Contribute to rretamal/MauiGyras development by creating an account on GitHub.github.com
Check here Part 2!: https://medium.com/@rretamal.dev/using-sensors-in-net-maui-4c0996f852ed
Happy Coding!