Linux-.NET-C#-SDL2環境構築


Linux で .NET C# + SDL2 を使った GUI アプリを作る方法

目次

~ Ubuntu で FreeCell 風カードゲームを表示 ~

はじめに

Linux 環境で C# を使って GUI アプリを開発したい方に向けて、SDL2 ライブラリを使ったクロスプラットフォームな開発環境構築と、FreeCell風のカード表示サンプルを紹介します。

1. 開発環境の構築

1-1. 必要なパッケージのインストール

まず、.NET 8 SDK と SDL2 の開発用ライブラリをインストールします。

# Microsoft リポジトリの登録
wget https://packages.microsoft.com/config/ubuntu/$(lsb_release -rs)/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb

# .NET SDK のインストール
sudo apt-get update
sudo apt-get install dotnet-sdk-8.0

# SDL2 + 画像表示ライブラリのインストール
sudo apt install libsdl2-2.0-0 libsdl2-dev
sudo apt install libsdl2-image-2.0-0 libsdl2-image-dev

1-2. SDL2-CS(C#バインディング)の導入

以下のコマンドで、C#プロジェクトに SDL2-CS を追加します。

dotnet new console -o SdlApp
cd SdlApp
dotnet add package SDL2-CS

2. サンプル構成

以下の構成でカード表示アプリを作成します。

  • カード画像:cards/C-01.pngS-13.png
  • 背景色:暗緑色
  • 8列 × 最大6枚のカードを表示

3. プログラム構成

Program.cs

class Program {
    static void Main(string[] args) {
        new CardWindow().Run();
    }
}

CardWindow.cs

using System;
using System.Collections.Generic;
using SDL2;

public class CardWindow {
    private IntPtr window;
    private IntPtr renderer;
    private bool running = true;
    private Dictionary<string, IntPtr> cardTextures = new();

    public void Run() {
        SDL.SDL_Init(SDL.SDL_INIT_VIDEO);
        SDL_image.IMG_Init(SDL_image.IMG_InitFlags.IMG_INIT_PNG);

        window = SDL.SDL_CreateWindow("FreeCell Style",
            SDL.SDL_WINDOWPOS_CENTERED, SDL.SDL_WINDOWPOS_CENTERED,
            1024, 768, SDL.SDL_WindowFlags.SDL_WINDOW_SHOWN);
        renderer = SDL.SDL_CreateRenderer(window, -1, 0);

        LoadCardImages("cards");

        while (running) {
            while (SDL.SDL_PollEvent(out SDL.SDL_Event e) != 0) {
                if (e.type == SDL.SDL_EventType.SDL_QUIT) running = false;
            }

            SDL.SDL_SetRenderDrawColor(renderer, 0, 64, 0, 255);
            SDL.SDL_RenderClear(renderer);
            DrawCards();
            SDL.SDL_RenderPresent(renderer);
            SDL.SDL_Delay(16);
        }

        Cleanup();
    }

    private void LoadCardImages(string folder) {
        string[] suits = { "C", "D", "H", "S" };
        for (int i = 1; i <= 13; i++) {
            foreach (string suit in suits) {
                string num = i.ToString("D2");
                string key = $"{suit}-{num}";
                string path = $"{folder}/{key}.png";
                IntPtr surface = SDL_image.IMG_Load(path);
                if (surface == IntPtr.Zero)
                    Console.WriteLine($"Failed to load {path}: {SDL.SDL_GetError()}");
                IntPtr texture = SDL.SDL_CreateTextureFromSurface(renderer, surface);
                SDL.SDL_FreeSurface(surface);
                cardTextures[key] = texture;
            }
        }
    }

    private void DrawCards() {
        int cardW = 71, cardH = 96;
        int startX = 50, startY = 100;
        int spacingX = 90, spacingY = 30;
        string[] testOrder = { "C", "D", "H", "S", "C", "D", "H", "S" };

        for (int col = 0; col < testOrder.Length; col++) {
            string suit = testOrder[col];
            for (int row = 0; row < 6; row++) {
                string key = $"{suit}-{(row + 1).ToString("D2")}";
                if (!cardTextures.TryGetValue(key, out var tex)) continue;

                SDL.SDL_Rect dst = new SDL.SDL_Rect {
                    x = startX + col * spacingX,
                    y = startY + row * spacingY,
                    w = cardW,
                    h = cardH
                };
                SDL.SDL_RenderCopy(renderer, tex, IntPtr.Zero, ref dst);
            }
        }
    }

    private void Cleanup() {
        foreach (var tex in cardTextures.Values)
            SDL.SDL_DestroyTexture(tex);
        SDL.SDL_DestroyRenderer(renderer);
        SDL.SDL_DestroyWindow(window);
        SDL_image.IMG_Quit();
        SDL.SDL_Quit();
    }
}

csproj の設定(例)

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks> <!-- SDL2-CSのために必要 -->
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="SDL2-CS" Version="2.0.0" />
  </ItemGroup>
</Project>

おわりに

このように、Linux 上でも C# と SDL2 を使って、GUI アプリやゲームの開発が可能です。カードの画像を差し替えれば、自作のソリティアやフリーセルも実現可能です!


実行プログラム

エントリーポイント

最初に呼び出されます

Program.cs

class Program
{
    static void Main(string[] args)
    {
        new CardWindow().Run();
    }
}

Program.cs

カードクラス

カードの性質をモデル化します。

using SDL2;
using System;

public class Card
{
    public string Suit { get; }
    public int Number { get; }
    public IntPtr Texture { get; private set; }
    public SDL.SDL_Rect DestRect { get; set; }

    public string Key => $"{Suit}-{Number.ToString("D2")}";

    public Card(string suit, int number, IntPtr texture, int x, int y, int width, int height)
    {
        Suit = suit;
        Number = number;
        Texture = texture;
        DestRect = new SDL.SDL_Rect { x = x, y = y, w = width, h = height };
    }

    public void Render(IntPtr renderer)
    {
        var rect = DestRect;
        SDL.SDL_RenderCopy(renderer, Texture, IntPtr.Zero, ref rect);
    }

    public bool HitTest(int mouseX, int mouseY)
    {
        return mouseX >= DestRect.x && mouseX <= DestRect.x + DestRect.w &&
               mouseY >= DestRect.y && mouseY <= DestRect.y + DestRect.h;
    }

    public void Dispose()
    {
        if (Texture != IntPtr.Zero)
        {
            SDL.SDL_DestroyTexture(Texture);
            Texture = IntPtr.Zero;
        }
    }
}

メインプログラム

実際に動くプログラムです

CardWindow.cs

using System;
using System.Collections.Generic;
using SDL2;

public class CardWindow
{
    private IntPtr window;
    private IntPtr renderer;
    private bool running = true;
    private List<Card> cards = new();
    private List<Stack<Card>> columns = new();
    private Card? selectedCard = null;

    public void Run()
    {
        Initialize();
        LoadCards("cards");

        while (running)
        {
            HandleInput();
            Update();
            Render();
            SDL.SDL_Delay(16);
        }

        Cleanup();
    }

    private void Initialize()
    {
        SDL.SDL_Init(SDL.SDL_INIT_VIDEO);
        SDL_image.IMG_Init(SDL_image.IMG_InitFlags.IMG_INIT_PNG);

        window = SDL.SDL_CreateWindow("FreeCell Click Move",
            SDL.SDL_WINDOWPOS_CENTERED, SDL.SDL_WINDOWPOS_CENTERED,
            1024, 768, SDL.SDL_WindowFlags.SDL_WINDOW_SHOWN);

        renderer = SDL.SDL_CreateRenderer(window, -1, 0);
    }

    private void LoadCards(string folder)
    {
        int cardW = 71, cardH = 96;
        string[] suits = { "C", "D", "H", "S" };

        foreach (var suit in suits)
        {
            for (int num = 1; num <= 13; num++)
            {
                string key = $"{suit}-{num.ToString("D2")}";
                string path = $"{folder}/{key}.png";

                IntPtr surface = SDL_image.IMG_Load(path);
                if (surface == IntPtr.Zero) continue;

                IntPtr texture = SDL.SDL_CreateTextureFromSurface(renderer, surface);
                SDL.SDL_FreeSurface(surface);

                if (texture == IntPtr.Zero) continue;

                cards.Add(new Card(suit, num, texture, 0, 0, cardW, cardH));
            }
        }

        Random rand = new();
        for (int i = 0; i < 8; i++) columns.Add(new Stack<Card>());

        foreach (var card in cards)
        {
            int colIndex = rand.Next(8);
            columns[colIndex].Push(card);
        }
    }

    private void HandleInput()
    {
        while (SDL.SDL_PollEvent(out SDL.SDL_Event e) != 0)
        {
            if (e.type == SDL.SDL_EventType.SDL_QUIT)
                running = false;

            if (e.type == SDL.SDL_EventType.SDL_MOUSEBUTTONDOWN)
            {
                int mouseX = e.button.x;
                int mouseY = e.button.y;

                for (int col = 0; col < columns.Count; col++)
                {
                    if (columns[col].Count == 0) continue;

                    var topCard = columns[col].Peek();
                    if (topCard.HitTest(mouseX, mouseY))
                    {
                        selectedCard = columns[col].Pop();
                        return;
                    }
                }

                if (selectedCard != null)
                {
                    for (int col = 0; col < columns.Count; col++)
                    {
                        int colX = 50 + col * 90;
                        if (mouseX >= colX && mouseX <= colX + 71)
                        {
                            columns[col].Push(selectedCard);
                            selectedCard = null;
                            return;
                        }
                    }
                }
            }
        }
    }

    private void Update() { }

    private void Render()
    {
        SDL.SDL_SetRenderDrawColor(renderer, 0, 64, 0, 255);
        SDL.SDL_RenderClear(renderer);
        DrawCards();
        SDL.SDL_RenderPresent(renderer);
    }

    private void DrawCards()
    {
        int cardW = 71, cardH = 96;
        int startX = 50, startY = 100;
        int spacingX = 90, spacingY = 30;

        for (int col = 0; col < columns.Count; col++)
        {
            var stack = columns[col];
            int x = startX + col * spacingX;
            Card[] stackArray = stack.ToArray();
            Array.Reverse(stackArray);

            for (int row = 0; row < stackArray.Length; row++)
            {
                var card = stackArray[row];
                card.DestRect = new SDL.SDL_Rect
                {
                    x = x,
                    y = startY + row * spacingY,
                    w = cardW,
                    h = cardH
                };
                card.Render(renderer);
            }
        }
    }

    private void Cleanup()
    {
        foreach (var card in cards)
            card.Dispose();

        SDL.SDL_DestroyRenderer(renderer);
        SDL.SDL_DestroyWindow(window);
        SDL_image.IMG_Quit();
        SDL.SDL_Quit();
    }
}

プロジェクトファイル

コンパイル方法等を記述します。

SdlFreecell.csproj

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="SDL2-CS" Version="2.0.0" />
  </ItemGroup>
</Project>
よかったらシェアしてね!
目次