Press "Enter" to skip to content

Lesson 07.3: Sending Messages from the ViewModel to the View

When the player fights monsters, we will need to display many messages in the UI – if the player hits the monster, how many hit points of damage the player does, how many points of damage the monster does, etc.

In this lesson, we’re going to create a way to send messages from the GameSession (ViewModel), to the UI (View). We’ll do this by having the ViewModel “raise an event”, and have the View “subscribe” to that event.

NOTE: This technique of have an object raise an event is known as publish/subscribe, or the Observer design pattern.

Step 1: Edit \WPFUI\MainWindow.xaml

On the screen, in the left part of the Gameplay section (upper-right quadrant), we have space for the game messages. We’ll add a RichTextBox control, surrounded by a border. This is at lines 76-91, in the source code below.

A RichTextBox control is like a small word processor. We can add text to it, and modify how the text is displayed (size, color, if it is bolded, etc.). We’ll give it an x:Name attribute. This lets us access it through code, in the MainWindow.xaml.cs file.

The RichTextBox control has default formatting, and we want to override the space it adds between each paragraph. Normally, it has a large margin. Because we are only going to display our game messages one line at a time, we want to have a margin of 0 – without any extra space between the messages.

MainWindow.xaml
<Window x:Class="WPFUI.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:viewModels="clr-namespace:Engine.ViewModels;assembly=Engine"
        d:DataContext="{d:DesignInstance viewModels:GameSession}"
        mc:Ignorable="d"
        FontSize="11pt"
        Title="Scott's Awesome Game" Height="768" Width="1024">
    <Window.Resources>
        <BooleanToVisibilityConverter x:Key="BooleanToVisibility" />
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="225"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="250"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <!-- Menu -->
        <Label Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Content="Menu" Background="AliceBlue"/>
        <!-- Player stats -->
        <Grid Grid.Row="1" Grid.Column="0" Background="Aquamarine">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
            
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>
            
            <Label Grid.Row="0" Grid.Column="0" Content="Name:"/>
            <Label Grid.Row="0" Grid.Column="1" Content="{Binding CurrentPlayer.Name}"/>
            <Label Grid.Row="1" Grid.Column="0" Content="Class:"/>
            <Label Grid.Row="1" Grid.Column="1" Content="{Binding CurrentPlayer.CharacterClass}"/>
            <Label Grid.Row="2" Grid.Column="0" Content="Hit points:"/>
            <Label Grid.Row="2" Grid.Column="1" Content="{Binding CurrentPlayer.HitPoints}"/>
            <Label Grid.Row="3" Grid.Column="0" Content="Gold:"/>
            <Label Grid.Row="3" Grid.Column="1" Content="{Binding CurrentPlayer.Gold}"/>
            <Label Grid.Row="4" Grid.Column="0" Content="XP:"/>
            <Label Grid.Row="4" Grid.Column="1" Content="{Binding CurrentPlayer.ExperiencePoints}"/>
            <Label Grid.Row="5" Grid.Column="0" Content="Level:"/>
            <Label Grid.Row="5" Grid.Column="1" Content="{Binding CurrentPlayer.Level}"/>
        </Grid>
        <!-- Gameplay -->
        <Grid Grid.Row="1" Grid.Column="1"
              Background="Beige">
            <Grid.RowDefinitions>
                <RowDefinition Height="*"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>
            
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="3*"/>
                <ColumnDefinition Width="2*"/>
            </Grid.ColumnDefinitions>
            
            <!-- Game Messages -->
            <Border Grid.Row="0" Grid.Column="0"
                    Grid.RowSpan="2"
                    BorderBrush="Gainsboro"
                    BorderThickness="1">
                <RichTextBox x:Name="GameMessages"
                             Background="Beige"
                             VerticalScrollBarVisibility="Auto">
                    <RichTextBox.Resources>
                        <Style TargetType="{x:Type Paragraph}">
                            <Setter Property="Margin" Value="0"/>
                        </Style>
                    </RichTextBox.Resources>
                </RichTextBox>
                
            </Border>
            
            <!-- Location information -->
            <Border Grid.Row="0" Grid.Column="1"
                    BorderBrush="Gainsboro"
                    BorderThickness="1">
                
                <Grid Margin="3">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="*"/>
                        <RowDefinition Height="Auto"/>
                    </Grid.RowDefinitions>
                    
                    <TextBlock Grid.Row="0"
                               HorizontalAlignment="Center"
                               Text="{Binding CurrentLocation.Name}"/>
                    
                    <Image Grid.Row="1"
                           HorizontalAlignment="Center"
                           VerticalAlignment="Center"
                           Height="125"
                           Width="125"
                           Source="{Binding CurrentLocation.ImageName}"/>
                    
                    <TextBlock Grid.Row="2"
                               HorizontalAlignment="Center"
                               Text="{Binding CurrentLocation.Description}"
                               TextWrapping="Wrap"/>
                </Grid>
                
            </Border>
            
            <!-- Monster information -->
            <Border Grid.Row="1" Grid.Column="1"
                    BorderBrush="Gainsboro"
                    BorderThickness="1">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="*" />
                        <RowDefinition Height="Auto" />
                    </Grid.RowDefinitions>
                    <TextBlock Grid.Row="0"
                               HorizontalAlignment="Center"
                               Height="Auto"
                               Text="{Binding CurrentMonster.Name}" />
                    <Image Grid.Row="1"
                           HorizontalAlignment="Center"
                           VerticalAlignment="Center"
                           Height="125"
                           Width="125"
                           Source="{Binding CurrentMonster.ImageName}" />
                    <StackPanel Grid.Row="2"
                                Visibility="{Binding HasMonster, Converter={StaticResource BooleanToVisibility}}"
                                HorizontalAlignment="Center"
                                Orientation="Horizontal">
                        <TextBlock>Current Hit Points:</TextBlock>
                        <TextBlock Text="{Binding CurrentMonster.HitPoints}" />
                    </StackPanel>
                </Grid>
            </Border>
        </Grid>
        <!-- Inventory and Quests -->
        <Grid Grid.Row="2" Grid.Column="0"
              Background="BurlyWood">
        
            <TabControl>
                <TabItem Header="Inventory">
                    <DataGrid ItemsSource="{Binding CurrentPlayer.Inventory}"
                              AutoGenerateColumns="False"
                              HeadersVisibility="Column">
                        <DataGrid.Columns>
                            <DataGridTextColumn Header="Description"
                                                Binding="{Binding Name}"
                                                Width="*"/>
                            <DataGridTextColumn Header="Price"
                                                Binding="{Binding Price}"
                                                Width="Auto"/>
                        </DataGrid.Columns>
                    </DataGrid>
                </TabItem>
                <TabItem Header="Quests">
                    <DataGrid ItemsSource="{Binding CurrentPlayer.Quests}"
                              AutoGenerateColumns="False"
                              HeadersVisibility="Column">
                        <DataGrid.Columns>
                            <DataGridTextColumn Header="Name"
                                                Binding="{Binding PlayerQuest.Name}"
                                                Width="*"/>
                            <DataGridTextColumn Header="Done?"
                                                Binding="{Binding IsCompleted}"
                                                Width="Auto"/>
                        </DataGrid.Columns>
                    </DataGrid>
                </TabItem>
            </TabControl>
        </Grid>
        <!-- Action controls -->
        <Grid Grid.Row="2" Grid.Column="1"
              Background="Lavender">
            <Grid.RowDefinitions>
                <RowDefinition Height="*" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="255" />
            </Grid.ColumnDefinitions>
            
            <!-- Movement Controls -->
            <Grid Grid.Row="0" Grid.Column="1">
                <Grid.RowDefinitions>
                    <RowDefinition Height="*" />
                    <RowDefinition Height="*" />
                    <RowDefinition Height="*" />
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*" />
                    <ColumnDefinition Width="*" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <Button Grid.Row="0" Grid.Column="1" 
                        Height="25" Width="65" Margin="10" 
                        Click="OnClick_MoveNorth"
                        Visibility="{Binding HasLocationToNorth, Converter={StaticResource BooleanToVisibility}}"
                        Content="North"/>
                <Button Grid.Row="1" Grid.Column="0" 
                        Height="25" Width="65" Margin="10" 
                        Click="OnClick_MoveWest"
                        Visibility="{Binding HasLocationToWest, Converter={StaticResource BooleanToVisibility}}"
                        Content="West"/>
                <Button Grid.Row="1" Grid.Column="2" 
                        Height="25" Width="65" Margin="10" 
                        Click="OnClick_MoveEast"
                        Visibility="{Binding HasLocationToEast, Converter={StaticResource BooleanToVisibility}}"
                        Content="East"/>
                <Button Grid.Row="2" Grid.Column="1" 
                        Height="25" Width="65" Margin="10" 
                        Click="OnClick_MoveSouth"
                        Visibility="{Binding HasLocationToSouth, Converter={StaticResource BooleanToVisibility}}"
                        Content="South"/>
            </Grid>
        </Grid>
    </Grid>
</Window>

Step 2: Create a new “EventArgs” folder, in the Engine project, and create a new class in that folder – GameMessageEventArgs.cs

The ViewModel is going to communicate with the View by “raising events”. This will let the View know that something happened. But, the View also needs to know what text to display on the screen.

To send additional information with an event, you use an “event argument”. We’re going to create a custom event argument that will hold the text to display in the View.

To create a custom event argument, you only need to create a new class and make it inherit from System.EventArgs. Our custom event argument class will be GameMessageEventArgs.

Next, we’ll give the class a Message property, which will hold the message text to display.

Finally, we’ll add a constructor that accept the message as a parameter and sets the Message property’s value.

So, when we raise the message event, we’ll instantiate a new GameMessageEventArgs object, with the message text, and pass that object by the event.

GameMessageEventArgs.cs
namespace Engine.EventArgs
{
    public class GameMessageEventArgs : System.EventArgs
    {
        public string Message { get; private set; }
        public GameMessageEventArgs(string message)
        {
            Message = message;
        }
    }
}

Step 3: Edit \Engine\ViewModels\GameSession.cs

Now that we have a class to hold the message, we can modify the GameSession class to send out messages for the View to display.

ThThe first step is to create a public EventHandler<GameMessageEventArgs> named OnMessageRaised (see line 11). This is how the View “subscribes” to the event – like you might subscribe to a weather alerts on a smartphone. The “<GameMessageEventArgs>” tells the subscribers what type of event arguments to look for, so they can use the data passed in the event.

Next, we will create the RaiseMessage function (line 160). When the ViewModel wants to raise the event, it will call this function.

The code checks if OnMessageRaised has any subscribers (it will be “null”, if there are no subscribers). If there are subscribers, it will invoke the event, passing a new GameMessageEventArgs object that has the desired message text.

To test the event, we’ll add a message when the player moves to a new location and encounters a monster.

We can do this by modifying the CurrentMonster setter (lines 49-53). If the CurrentMonster is not null (so, there is a Monster object in CurrentMonster), we will call the RaiseMessage function with an empty string (to put a blank line in the UI). Then, we’ll call RaiseMessage again, with a message that tells the player what type of monster is at the location.

So, the flow will work like this:

  • The player moves to a new location
  • The location has a monster
  • CurrentMonster is set to the location’s monster
  • The CurrentMonster setter call RaiseMessage
  • RaiseMessage sends the GameMessageEventArgs object to any objects subscribed to OnMessageRaised
GameSession.cs
using System;
using System.Linq;
using Engine.EventArgs;
using Engine.Factories;
using Engine.Models;
namespace Engine.ViewModels
{
    public class GameSession : BaseNotificationClass
    {
        public event EventHandler<GameMessageEventArgs> OnMessageRaised;
        #region Properties
        private Location _currentLocation;
        private Monster _currentMonster;
        public World CurrentWorld { get; set; }
        public Player CurrentPlayer { get; set; }
        public Location CurrentLocation
        {
            get { return _currentLocation; }
            set
            {
                _currentLocation = value;
                OnPropertyChanged(nameof(CurrentLocation));
                OnPropertyChanged(nameof(HasLocationToNorth));
                OnPropertyChanged(nameof(HasLocationToEast));
                OnPropertyChanged(nameof(HasLocationToWest));
                OnPropertyChanged(nameof(HasLocationToSouth));
                GivePlayerQuestsAtLocation();
                GetMonsterAtLocation();
            }
        }
        public Monster CurrentMonster
        {
            get { return _currentMonster; }
            set
            {
                _currentMonster = value;
                OnPropertyChanged(nameof(CurrentMonster));
                OnPropertyChanged(nameof(HasMonster));
                if (CurrentMonster != null)
                {
                    RaiseMessage("");
                    RaiseMessage($"You see a {CurrentMonster.Name} here!");
                }
            }
        }
        public Weapon CurrentWeapon { get; set; }
        public bool HasLocationToNorth
        {
            get
            {
                return CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate + 1) != null;
            }
        }
        public bool HasLocationToEast
        {
            get
            {
                return CurrentWorld.LocationAt(CurrentLocation.XCoordinate + 1, CurrentLocation.YCoordinate) != null;
            }
        }
        public bool HasLocationToSouth
        {
            get
            {
                return CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate - 1) != null;
            }
        }
        public bool HasLocationToWest
        {
            get
            {
                return CurrentWorld.LocationAt(CurrentLocation.XCoordinate - 1, CurrentLocation.YCoordinate) != null;
            }
        }
        public bool HasMonster => CurrentMonster != null;
        #endregion
        public GameSession()
        {
            CurrentPlayer = new Player
                            {
                                Name = "Scott",
                                CharacterClass = "Fighter",
                                HitPoints = 10,
                                Gold = 1000000,
                                ExperiencePoints = 0,
                                Level = 1
                            };
            CurrentWorld = WorldFactory.CreateWorld();
            CurrentLocation = CurrentWorld.LocationAt(0, 0);
        }
        public void MoveNorth()
        {
            if(HasLocationToNorth)
            {
                CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate + 1);
            }
        }
        public void MoveEast()
        {
            if(HasLocationToEast)
            {
                CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate + 1, CurrentLocation.YCoordinate);
            }
        }
        public void MoveSouth()
        {
            if(HasLocationToSouth)
            {
                CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate - 1);
            }
        }
        public void MoveWest()
        {
            if(HasLocationToWest)
            {
                CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate - 1, CurrentLocation.YCoordinate);
            }
        }
        private void GivePlayerQuestsAtLocation()
        {
            foreach(Quest quest in CurrentLocation.QuestsAvailableHere)
            {
                if(!CurrentPlayer.Quests.Any(q => q.PlayerQuest.ID == quest.ID))
                {
                    CurrentPlayer.Quests.Add(new QuestStatus(quest));
                }
            }
        }
        private void GetMonsterAtLocation()
        {
            CurrentMonster = CurrentLocation.GetMonster();
        }
        private void RaiseMessage(string message)
        {
            OnMessageRaised?.Invoke(this, new GameMessageEventArgs(message));
        }
    }
}

Step 4: Edit \WPFUI\MainWindow.xaml.cs

Now that the ViewModel can send messages, we need the View to watch for these messages – “subscribe” to the eventhandler.

In the constructor (line 21), we have the View subscribe to the eventhandler. This line says, “when _gameSession raises an OnMessageRaised event, run the OnGameMessageRaised function (the View’s function to do something, when it sees this event raised)”.

On line 46, we have the new OnGameMessageRaised function. This automatically has two parameters: the sender (the object that raised the event), and the event arguments (in this case, its datatype is GameMessageEventArgs, since we defined that in GameSession, with “EventHandler<GameMessageEventArgs>”.

When this function notices an event was raised, it will take the GameMessageEventArgs object, get the value of its Message property, format it (adding a “run” of text to a new paragraph, and adding it to the RichTextBox document), and scroll to the bottom of the RichTextBox (so the user can always see the latest message.

MainWindow.xaml.cs
using System.Windows;
using System.Windows.Documents;
using Engine.EventArgs;
using Engine.ViewModels;
namespace WPFUI
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private GameSession _gameSession;
        public MainWindow()
        {
            InitializeComponent();
            _gameSession = new GameSession();
            _gameSession.OnMessageRaised += OnGameMessageRaised;
            DataContext = _gameSession;
        }
        private void OnClick_MoveNorth(object sender, RoutedEventArgs e)
        {
            _gameSession.MoveNorth();
        }
        private void OnClick_MoveWest(object sender, RoutedEventArgs e)
        {
            _gameSession.MoveWest();
        }
        private void OnClick_MoveEast(object sender, RoutedEventArgs e)
        {
            _gameSession.MoveEast();
        }
        private void OnClick_MoveSouth(object sender, RoutedEventArgs e)
        {
            _gameSession.MoveSouth();
        }
        private void OnGameMessageRaised(object sender, GameMessageEventArgs e)
        {
            GameMessages.Document.Blocks.Add(new Paragraph(new Run(e.Message)));
            GameMessages.ScrollToEnd();
        }
    }
}

Step 5: Test the game

Run the game, and move to the locations that have monsters. When you see the image of a monster, you should now see a message in the RichTextBox that tells the player what type of monster they encountered.

In the next lesson, we will let the player fight the monster.

NEXT LESSON: Lesson 07.4: Monster Combat

PREVIOUS LESSON: Lesson 07.2: Adding Monsters to Locations

14 Comments

  1. Eric Woodworth
    Eric Woodworth 2021-10-08

    Is there a way to send everything minus the player info, inventory, and quests to the main view screen? Like in those old school MUDs?

    • SOSCSRPG
      SOSCSRPG 2021-10-09

      Hi Eric,

      You could make changes in the GameSession class and add more calls to RaiseMessage(). For example, in the Move functions, update the CurrentLocation property PLUS call RaiseMessage() to display the current location’s information.

      Let me know if that isn’t clear, and if you want an example.

  2. James
    James 2021-12-01

    Scott,

    Noticed an extra line when the game enters its first entry into the RichTextBox.
    my fix was to add a private static bool variable initialized to true to the MainWindow class in MainWindow.xaml.cs. In the OnGameMessageRaised method I check if true. if true I set the bool to false and do GameMessages.Document.Blocks.Clear(). This is only done on its first call to this method. I’m sure this is a hack and there is a better way to deal with the document first block? Best way to see this is to comment out RaiseMessage(“”) from CurrentMonster method in GameSession.cs.

    Thanks, James

    • SOSCSRPG
      SOSCSRPG 2021-12-01

      Hi James,

      I’d consider changing GameMessageEventArgs to have a list of strings for its Message property, instead of the current string datatype. Then, in the OnGameMessageRaised function, put all those lines into a single document block, then add a blank line after the block. I’m not sure that would work, since I haven’t done much with RichTextBox, but that sounds like it might work.

  3. Old Jobobo
    Old Jobobo 2022-12-14

    After completing the last three monster sections and getting to the first test here, no compiler errors but I do not see anything in the monster box. I see the new border but nothing else populates. You should have access to my git repo, if you have time to help figure out what I did wrong I would be very grateful.

    • SOSCSRPG
      SOSCSRPG 2022-12-14

      I should be able to check it out tomorrow (Wednesday) night and let you know what I see.

    • SOSCSRPG
      SOSCSRPG 2022-12-14

      Check the “set” for the CurrentLocation. After raising the property changed notifications, it gives the player the quest at the location, but it doesn’t call GetMonsterAtLocation(), so The CurrentMonster property is never set with a monster object.

      • Old Jobobo
        Old Jobobo 2022-12-15

        Thank you so much, As a thank you if you look at the images in my repo there are a number of images I did in Midjourney to replace your “super awesome” art assets 🙂 if you want to use them and replace your assets in the lesson feel free.

        • SOSCSRPG
          SOSCSRPG 2022-12-15

          You’re welcome – even though I now feel sad about my awesome art. 🙂 Maybe I need to have AI make some monster images.

  4. Alex
    Alex 2024-01-31

    Hello! I have 2 big questions. Excuse me if this is not the proper place to ask.

    1. I haven’t completed this tutorial yet, but am I right thinking that this game is going to be user input driven? The game progresses if a player does some action, and there is no actual game loop. For instance if I would like to make, say, simple Arkanoid, the approach should be different, right? If so how differen?

    2. As a side project I wanted to make my own primitive game engine, to understand how basic stuff works there like rendering, or how colliders are made. But not sure how to start. And I cant find any good C# tutorials on youtube. Can you please advise something?

    Thanks in advance!

    • SOSCSRPG
      SOSCSRPG 2024-01-31

      This is definitely the proper place to ask. 🙂

      1. Correct, this is a “turn-based” game that waits for user input, instead of having a game loop that lets actions happens while the user does nothing. The approach would be different. At a high-level, the player’s button clicks would be processed differently than the current code. They would put some sort of event in a queue (or some other location) that the game loop would look at, and process, every time the game loop’s timer “ticked”. There is some more information about game loops here: https://gameprogrammingpatterns.com/game-loop.html

      2. I don’t do much with games – these lessons only used a game as a learning example, and not a “real” game. But, you’ll probably want to look for tutorials on either Unity, Godot, or MonoGame. Brackeys had a good series on Unity, but he stopped creating tutorials a few years ago. Unity recently made some changes and many developers have moved from Unity to Godot. Most of the game engines have tutorials on their sites, like https://docs.godotengine.org/en/stable/index.html. So that might be the place to start.

      • Alex
        Alex 2024-01-31

        Follow up to the first question. So for instance if I were to create in WPF some “real time stuff” like arkanoid or a simulation. Do I have to create a separate thread for the calculations. As I am not mistaken WPF uses single thread for GUI rendering, and if the gameloop is called on the same thread it will make GUI to freeze until gameloop ends.

        And regarding the second question. I meant NOT to make the game on an engine. But rather create the game engine itself. Like my own GODOT. I am very curious how is drawing of each pixel is done or how the input system is implemented. Is it even possible in C#? Or I have to learn C++ as well?

        • SOSCSRPG
          SOSCSRPG 2024-02-01

          Yes, you would have to do your game processing on a separate thread from the UI. This is even a problem with business applications. If you have a calculation that takes a long time, and you don’t move it to another thread, it will lock up the UI. For business applications, this is often done with “async” and “await”. But for a game, you will probably need to go more into manually managing the processing in the game loop. I haven’t written any serious games, but the Game Programming Patterns site looks like a good place to start. That should give you an idea of how to build a game engine for the game events.

          If you also want to have highly-detailed or high-speed graphics, you probably would need to switch to using DirectX or OpenGL. C and C++ work well with those libraries. I haven’t used it, but SharpGL might be good to look at if you want to use C# for the programming. If you want to write your own graphics engine, then I’d definitely switch to C# or C. They’re generally faster and better for directly accessing the video display’s memory. But, I haven’t done that type of programming in a few decades.

  5. Alex
    Alex 2024-02-01

    Thank you very much for your answers!

Leave a Reply

Your email address will not be published. Required fields are marked *