Saltar al contenido
Ricardo RetamalSoftware y cosas que vale la pena construir
/
Conversemos
← Ver todos los escritos
Building4 min read

Haciendo un Juego con .NET MAUI: Parte 1 — Sensores

Esta fué mi actividad extra curricular del fin de semana. Tenía curiosidad de jugar un poco con sensores en una aplicación en .Net MAUI…

Publicado originalmente en Medium ↗

English version: https://medium.com/@rretamal.dev/using-sensors-in-net-maui-4c0996f852ed

Esta fue mi actividad extracurricular del fin de semana. Tenía curiosidad de jugar un poco con sensores en una aplicación en .Net MAUI, así que para este ejercicio me propuse:

  1. Acceder a la información del acelerometro y giroscopio
  2. Hacer algo con esa información para validar las acciones en un dispositivo

Asi surgió otra vez el ¿por qué no? Y pensé que podría usar esa información para hacer un pequeño juego de naves.

Lo primero que hice fue crear el proyecto en Visual Studio usando la plantilla de .Net MAUI y ponerle un nombre. Para este caso, usé MauiGyras:

Una vez creado el proyecto veras la estructura por defecto:

Para el proyecto tuve que instalar un par de librerías usango Nuget:

  • Microsoft.Maui.Graphics
  • SkiaSharp.Views.Maui.Controls

Ya que tengas las librerías instaladas solo recuerda modificar el Maui.Program.cs, debería quedarte algo así:

    public static class MauiProgram
    {
        public static MauiApp CreateMauiApp()
        {
            var builder = MauiApp.CreateBuilder();
            builder
                .UseMauiApp<App>()
                 .UseSkiaSharp()
                .ConfigureFonts(fonts =>
                {
                    fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                    fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
                });


#if DEBUG
    		builder.Logging.AddDebug();
#endif


            return builder.Build();
        }
    }

GitHub Gist ↗

Partí por el objeto que quería mover, así que diseñé una pequeña nave usando SkiaSharp, para ello en el MainPage.xaml removí el contenido por defecto y coloqué un canvas:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:skia="clr-namespace:SkiaSharp.Views.Maui.Controls;assembly=SkiaSharp.Views.Maui.Controls"
             x:Class="MauiGyras.MainPage">


    <Grid>
        <skia:SKCanvasView x:Name="canvasView" />
    </Grid>
</ContentPage>

GitHub Gist ↗

Todo el pintado y lo que queremos hacer ocurrirá en el evento PaintSurface del canvas, por lo que nos suscribiremos al evento en el constructor de esta manera:

canvasView.PaintSurface += CanvasView_PaintSurface;

Así que la siguiente pregunta fué, ¿cómo diantres dibujo una nave? para ello, escribí este método:

private void DrawSpaceship(SKCanvas canvas, float centerX, float centerY)
{
    using (var paint = new SKPaint { IsAntialias = true })
    {
        // Main body of the spaceship
        paint.Color = new SKColor(41, 128, 185); // Steel blue
        var bodyPath = new SKPath();
        bodyPath.MoveTo(centerX - 30, centerY + 20);
        bodyPath.LineTo(centerX + 30, centerY + 20);
        bodyPath.LineTo(centerX, centerY - 40);
        bodyPath.Close();
        canvas.DrawPath(bodyPath, paint);


        // Wings
        paint.Color = new SKColor(52, 152, 219); // Light blue
        var leftWing = new SKPath();
        leftWing.MoveTo(centerX - 30, centerY + 20);
        leftWing.LineTo(centerX - 60, centerY + 40);
        leftWing.LineTo(centerX - 25, centerY);
        leftWing.Close();
        canvas.DrawPath(leftWing, paint);


        var rightWing = new SKPath();
        rightWing.MoveTo(centerX + 30, centerY + 20);
        rightWing.LineTo(centerX + 60, centerY + 40);
        rightWing.LineTo(centerX + 25, centerY);
        rightWing.Close();
        canvas.DrawPath(rightWing, paint);


        // Cabin of the spaceship
        paint.Color = new SKColor(241, 196, 15); // Yellow
        canvas.DrawCircle(centerX, centerY - 10, 15, paint);


        // Propulsion
        paint.Color = new SKColor(231, 76, 60); // Red
        canvas.DrawCircle(centerX - 15, centerY + 25, 8, paint);
        canvas.DrawCircle(centerX + 15, centerY + 25, 8, paint);
    }


    canvas.Restore();
}

GitHub Gist ↗

¿Qué hace el método a grandes rasgos? Genera la imagen de una nave usando formas simples: la cabina es un triángulo de color azul, las dos alas son de color azul, la cabina es un círculo amarillo, y los propulsores son dos círculos rojos. También recibe algunos parámetros como el canvas para poder pintarlo y los puntos del centro de la pantalla.

Ahora hay que llamar al método, eso lo haremos en el evento PaintSurface, por lo que solo deberías llamar al método DrawSpaceship:

private void CanvasView_PaintSurface(object? sender, SkiaSharp.Views.Maui.SKPaintSurfaceEventArgs e)
{
    SKCanvas canvas = e.Surface.Canvas;
    SKImageInfo info = e.Info;


    DrawSpaceship(canvas, info.Width / 2, info.Height / 2);   
}

GitHub Gist ↗

Si ejecutas el proyecto deberías ver algo así:

¡Qué genial lo que pueden lograr un par de formas geométricas! En mi caso, al menos, me ahorraron buscar una imagen o generarla, jeje.

Ahora para usar los sensores necesitamos permisos!

private void InitializeSensors()
{
    if (Accelerometer.Default.IsSupported)
    {
        Accelerometer.Default.ReadingChanged += OnAccelerometerReadingChanged;
        Accelerometer.Default.Start(SensorSpeed.UI);
    }


    if (Gyroscope.Default.IsSupported)
    {
        Gyroscope.Default.ReadingChanged += OnGyroscopeReadingChanged;
        Gyroscope.Default.Start(SensorSpeed.UI);
    }
}

GitHub Gist ↗

Como verás, aquí revisamos si tenemos el sensor que necesitamos, es decir, el giroscopio y el acelerómetro, y de paso nos suscribiremos a los eventos respectivos para capturar los cambios en los valores.

La captura de datos quedaría de la siguiente manera:

private void OnAccelerometerReadingChanged(object sender, AccelerometerChangedEventArgs e)
{
    baseSpeed = Math.Max(5f, Math.Min(50f, 20f + e.Reading.Acceleration.Z * 30f));


    shipPosition.X += e.Reading.Acceleration.X * baseSpeed; 
    shipPosition.Y -= e.Reading.Acceleration.Y * baseSpeed; 
    MainThread.BeginInvokeOnMainThread(() => canvasView.InvalidateSurface());
}


private void OnGyroscopeReadingChanged(object sender, GyroscopeChangedEventArgs e)
{
    cameraRotationY += e.Reading.AngularVelocity.Y * 0.3f; 
    cameraRotationX += e.Reading.AngularVelocity.X * 0.3f; 
    MainThread.BeginInvokeOnMainThread(() => canvasView.InvalidateSurface());
}

GitHub Gist ↗

Aquí capturaremos un par de valores y los guardaremos. Con el acelerómetro manejaremos la posición de la nave y con el giroscopio la rotación de la cámara.

Esto se ve de la siguiente manera si lo vemos en el dispositivo:

Podemos ver cómo se mueve la nave y cómo va variando la velocidad, pero la verdad se ve sin vida, y un label es solo eso, un label, así que agregaremos a continuación estrellas! esto nos dará una mejor percepción de las acciones que se están haciendo. Para ello, agregaremos un método que nos genere puntos aleatorios y con eso, dar la sensación de muchos puntos situados por todas partes:

private void InitializeStars()
{
    for (int i = 0; i < StarCount; i++)
    {
        stars[i] = new SKPoint(
            (float)(random.NextDouble() * 2000 - 1000),
            (float)(random.NextDouble() * 2000 - 1000)
        );
    }
}

GitHub Gist ↗

Seguidamente hay que pintarlas, por lo que en PaintSurface agregaremos esta porción de código:

// Draw the stars
using (SKPaint starPaint = new SKPaint { Color = SKColors.White })
{
    for (int i = 0; i < StarCount; i++)
    {
        float x = stars[i].X - shipPosition.X;
        float y = stars[i].Y - shipPosition.Y;


        // Rotate the camera
        x += cameraRotationY * 20;
        y += cameraRotationX * 20;


        // Parallax effect
        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 star is out of the screen
        if (x < 0 || x > info.Width || y < 0 || y > info.Height)
        {
            // Reset the star position
            stars[i] = new SKPoint(
                shipPosition.X + (float)(random.NextDouble() * 2000 - 1000),
                shipPosition.Y + (float)(random.NextDouble() * 2000 - 1000)
            );
        }
        else
        {
            canvas.DrawCircle(x, y, 2 / (z / 500), starPaint);
        }
    }
}

GitHub Gist ↗

Aquí iremos iterando sobre las estrellas y aplicaremos una rotación para dar sensación de movimiento dependiendo de la rotación de la cámara. Además, agregaremos un efecto de alejamiento para dar profundidad y, finalmente, las pintaremos usando un círculo.

Así quedaría con el efecto de las estrellas:

Aquí usamos la información de los sensores para darle animación a un objeto por medio de SkiaSharp. En la segunda parte expandiremos un poco el demo!

https://github.com/rretamal/MauiGyras/tree/main

Happy Coding!