跳到主要内容

Getting Started with C# Basic

I. Language Overview 💻

C# (pronounced "C sharp") is a modern, type-safe, object-oriented programming language designed and developed by Microsoft. While maintaining the expressiveness and efficiency of C++, it also provides modern features such as automatic memory management and exception handling.

信息

The # symbol in musical notation represents sharp, indicating that a note should be raised by a half step when marked with #. This naming reflects Microsoft's ambition and positioning for the language — to both inherit the excellent traditions of the C language family while achieving enhancement and breakthrough.

As a versatile and powerful language, C# is widely used in various fields. It is particularly very popular in gameplay programming. With its powerful performance and flexibility, it has become the preferred language for game developers.

提示

This tutorial will focus on practical syntax and skills based on actual game development needs.

C# is most prominently known as the language used for scripting in the Unity game engine. The Godot engine also supports writing game logic in C#.

Unity implements cross-platform C# scripting support through Mono.

Godot 3.x uses Mono as its C# runtime environment. Godot 4.x adopts native .NET support, gradually migrating to the modern .NET ecosystem.

信息

Mono is an open-source implementation of the .NET framework that provides cross-platform development solutions, offering developers a complete .NET-compatible environment.

II. Language Characteristics 👥

  • Object-Oriented: C# supports the principles of object-oriented programming, making it easier to organize and structure code.

  • Type-Safe: C# is a statically-typed language, meaning variables must be declared with a specific data type, enhancing code safety.

  • Memory Management: C# includes automatic memory management through garbage collection, reducing the risk of memory leaks.

  • Modern language features: C# continues to evolve and introduces many modern programming language features, making development more efficient and flexible.

  • Open source ecosystem: C# has an active open source community that provides a large number of open source projects, documents and resources to help solve problems encountered in cross-platform development.

III. Development environment 🖥️

.NET SDK 🔧

.NET SDK: This software development kit includes compilers, libraries, and other resources for developing applications in C#.

👉 .NET SDK Download

.NET Framework 🔧

.NET Framework is a traditional framework that has been available for many years, specifically for the Windows platform.

.NET Core 🔧

.NET Core is a cross-platform, open-source version of .NET, designed to support modern applications.

.NET 🔧

The .NET platform merge the capabilities of both .NET Framework and .NET Core and offering developers a singular platform to create applications of any kind. These developments were necessitated to adapt to modern technological trends and demands for improved performance and scalability.

Development tools 📊

The main IDE for C# is Visual Studio from Microsoft.

However, there are alternatives, such as Visual Studio Code (a lightweight code editor with support for C# extensions) and JetBrains Rider.

Each environment has its own benefits and features, and the choice depends on the specific needs of the developer:

  • Visual Studio: Comprehensive IDE with advanced tools for large-scale projects and multi-language support.

  • JetBrains Rider: Cross-platform .NET IDE with powerful tools for .NET development and a rich set of plugins.

IV. Hello World 👋

Open your development tool and create a console project, or use the command line dotnet new console to create a new console application. Use dotnet run to compile and run a simple C# program.

public class Program
{
public static void Main()
{
Console.WriteLine("Hello World!");
}
}

The entry point in a C# program is typically represented by the Main() method, which is located in the Program class. This method must be static and serves as the starting point for the program's execution.

void is the keyword and means this section of our program – called a method – will not "return" any values.

Both Console.WriteLine() and Console.Write() print information to the console.

Common C# program structure:

public class Program
{
public static void Main(string[] args)
{
// program code
}
}

The args argument contains an array of strings that is passed to the program upon its launch.

V. Variables and Data Types 👀

Variables are used to store data in a program.

1. Variables naming rules 📚

Variable names should be descriptive.

  • Variable names must start with a letter or an underscore. This one can be a bit frustrating when you want to start a variable name with a number, like 6thEnemy or 2DBackground, but it’s just not allowed in C#. Instead, use enemy6 or background2D.

  • After the first letter or underscore, I recommend sticking mostly to letters, numbers, or underscores unless you have a really good reason to use something else. Some unusual characters can be used, but others will cause syntax errors.

  • You cannot use a space in a variable name. A space actually splits your variable name into two variable names and will confuse the compiler. For example, playerScore is one variable, while player Score will be treated like two variables.

  • Use camel case when your variable names have more than one word.

提示

Writing multi-word variable names like thisplayerfirstNameis ok, but the words tend to blend together. To make the specific words in the name clearer, use camel case, which means giving each word after the first word a capital letter like playerFirstName. You can capitalize the first letter, but by C# convention, variable names start with a lowercase first letter.

2. Basic data types 📊

  • Integer Types: int, long, short, byte

  • Floating-Point Types: float, double, decimal

  • Character Type: char

  • Boolean Type: bool

  • String Type: string

One of the reasons we need to specify the type of our variable is so that C# knows how much memory to set aside.

int is stored in 32 bits, or 4 bytes of memory.

If you need to save on memory and only need to store numbers that fall within a small range, using byte or short is the right choice.

float is our go-to for storing decimal numbers. Like int, it is stored in 32 bits or 4 bytes of memory.

double uses twice the memory that a float needs (hence the name double) but is more precise (more decimal places), while decimal is four times the memory size of a float and provides even more precision (even more decimal places)!

char is stored in 16 bits, or 2 bytes of memory.

bool is stored in 1 bit of memory, but is typically allocated 1 byte (8 bits) for efficiency in most systems.

string is the type we use to store text. Strings are actually made up of characters, so the amount of memory they use depends on the length of the string.

public class Program
{
public static void Main()
{
Console.WriteLine(10);
Console.WriteLine(3.14159);
Console.WriteLine(-0.5f);
Console.WriteLine('Y');
Console.WriteLine(true);
Console.WriteLine("games");
}
}
Data TypeMemory SizeDescription
bool1 bit (typically 1 byte)Stores logical true or false values
byte8 bits (1 byte)Stores small range integers
short16 bits (2 bytes)Stores smaller range integers
int32 bits (4 bytes)Stores standard integers
long64 bits (8 bytes)Stores very large integers
char16 bits (2 bytes)Stores a single character
float32 bits (4 bytes)Stores single-precision floating-point numbers
double64 bits (8 bytes)Stores double-precision floating-point numbers, more precise than float
decimal128 bits (16 bytes)Stores high-precision decimal numbers
stringVariable (depends on length)Stores text, composed of characters

3. Declaring Variables 💡

In C#, you declare a variable by specifying its data type and a name.

A variable must be declared before it can be assigned.

To create a variable, we must declare it – and to do that we need to tell C# what type the variable will be (what kind of value it will store) and give it a name (a programmer-defined label that we will use to access the variable).

In general, a declaration statement looks like this: type variableName; (type is the data type, variableName is the variable name)

A variable declaration only creates the variable, it doesn’t store a value.

First Format (Declaring Multiple Variables)

More concise when declaring multiple variables of the same type.

int playerScore, playerHealth, gameHighScore;
float scoreModifier;
string playerFirstName, playerLastName;
bool playerHasHighScore, gameOver;
Second Format (Declaring Each Variable Individually)

More prominent in terms of readability.

int playerScore;
int playerHealth;
int gameHighScore;
float scoreModifier;
string playerFirstName;
string playerLastName;
bool playerHasHighScore;
bool gameOver;

4. Variable assignment 📝

Assignment statement looks like this: variableName = someValue; (variableName is the variable name, someValue is the value

5. Initializing Variables 🔄

Initialization = Declaration + Assignment

public class Program
{
public static void Main()
{
// Declaration without initialization
int playerHealth;
// Initialization later in the program
playerHealth = 100;
Console.WriteLine("Player's Health is " + playerHealth);
// Initialization at declaration
float distanceToTarget = 10.9f;
Console.WriteLine("Distance to target: " + distanceToTarget);
}
}

6. Type Inference 🔗

C# supports type inference using the var keyword, allowing the compiler to automatically determine the data type based on the assigned value.

public class Program
{
public static void Main()
{
// Compiler infers string
var name = "Player";
// Compiler infers float
var score = 100.5f;

Console.WriteLine($"Name: {name}");
Console.WriteLine($"Score: {score}");
}
}

7. Constants 🔒

Constants are variables whose values cannot be changed once assigned. They are declared using the const keyword:

const double PI = 3.14159;

const variables should be used when you need to define a variable that doesn’t change throughout the program’s life cycle.

Key characteristics of constants:

  • Must be initialized when declared

  • The value must be determined at compile time

In C#, the naming convention for constants generally follows the rules of Pascal case (capitalization of the first letter of each word) and is all uppercase, separated by underscores. This is known as the UPPER_SNAKE_CASE.

public class GameConstants
{
public const int MAX_PLAYER_LEVEL = 100;
public const int INITIAL_HEALTH_POINTS = 100;
public const string GAME_VERSION = "1.0.0";
}

8. Nullable Types 🤔

Nullable types in C# allow representing an absent or uninitialized value for value types.

In C#, value types cannot be assigned a value of null. However, by using nullable types, you can explicitly allow a value type to be null.

int? nullableInt = null;

To check for the presence of a value, you can use the HasValue property, and to retrieve the value itself, you use Value.

The ?? operator is a null-coalescing operator that returns the left operand if it’s not null; otherwise, it returns the right one.

public class Program
{
public static void Main()
{
int? number = null;
number = 6;

if (number.HasValue)
{
Console.WriteLine(number.Value);
}

int? a = null;
int? b = 10;
int c = a ?? b ?? 0;
Console.WriteLine(c);
}
}

9. Type Conversion 🔀

Implicit Type Conversion

Implicit type conversion happens automatically when converting from a data type that can hold less information to one that can hold more information (for example, from int to double).

int playerHealth = 100;       // int
float healthPercentage = playerHealth; // int → float
Explicit Conversion

Explicit-type conversion (casting) is required when there’s a risk of data loss during the conversion.

float enemyDamage = 50.5f;
int roundedDamage = (int)enemyDamage; // float → int
Value Type Conversion Methods
  1. Convert Class
string scoreText = "200";
int playerScore = Convert.ToInt32(scoreText);
  1. Parse Method

Focusing on string conversion, the performance is better, but the rules are stricter. Use it with TryParse to achieve safe conversion.

string dateStr = "2008-10-31";
if (DateTime.TryParse(dateStr, out DateTime gameDate))
{
Console.WriteLine(gameDate);
}
else
{
Console.WriteLine("Invalid date format.");
}
Safe Conversion

as is used for safe type conversion of reference types, returning null instead of throwing an exception if conversion is not possible. is is used to check if an object is of a specific type.

public class GameCharacter
{
public string Name { get; set; }
}

public class Player : GameCharacter
{
public int Score { get; set; }
}

public class Enemy : GameCharacter
{
public int Health { get; set; }
}

public class GameManager
{
public void ProcessCharacter(object character)
{
if (character is Player player)
{
Console.WriteLine($"Player {player.Name}, Score: {player.Score}");
}
else if (character is Enemy enemy)
{
Console.WriteLine($"Enemy {enemy.Name}, Health: {enemy.Health}");
}

var gameChar = character as GameCharacter;
if (gameChar != null)
{
Console.WriteLine($"Character Name: {gameChar.Name}");
}
}
}

public class Program
{
public static void Main()
{
var gameManager = new GameManager();
object[] characters = {
new Player { Name = "Hero", Score = 100 },
new Enemy { Name = "Joker", Health = 50 },
};

foreach (var character in characters)
{
gameManager.ProcessCharacter(character);
}
}
}

VI. Strings 🔝

In game development, string processing is very commonly used, for example, text localization, UI text display, configuration parsing, data storage, etc.

A string is a sequence of characters. In C#, a string is a sequence of Unicode characters. It is a data type used to store a sequence of data values (usually bytes).

1. Declaration and Initialization 📝

The string type in C# is immutable, meaning every time the string is modified, a new instance is created.

string greeting = "Hello, C#!";
string emptyName = string.Empty;

2. Common String Operations 🔄

String Concatenation

// Use the + operator to concatenate multiple strings and variables together
string firstName = "Satoshi";
string lastName = "Nakamoto";
string fullName = firstName + " " + lastName;

String Interpolation

string firstName = "Satoshi";
string lastName = "Nakamoto";
string message = $"Hello, {firstName} {lastName}!";

Verbatim String Literal

string normalPath = "C:\\Program Files\\dotnet";
// No need to escape backslashes
string verbatimPath = @"C:\Program Files\dotnet";

Common String Methods

string text = "   C# Programming   ";
string trimmedText = text.Trim();
string leftTrim = text.TrimStart();
string rightTrim = text.TrimEnd();
string upperCaseText = text.ToUpper();
string lowerCaseText = text.ToLower();
int length = text.Length;
char firstChar = text[0];

Find and Replace

string text = "Hello World";

bool contains = text.Contains("World"); // Contains check
int index = text.IndexOf("o"); // Find position
int lastIndex = text.LastIndexOf("o"); // Last occurrence position

string newText = text.Replace("World", "C#"); // Replace string
string noSpaces = text.Replace(" ", ""); // Remove spaces

String Comparison

string str1 = "hello";
string str2 = "HELLO";

// Comparison methods
bool isEqual1 = str1 == str2; // Case-sensitive
bool isEqual2 = str1.Equals(str2, StringComparison.OrdinalIgnoreCase); // Case-insensitive

// Compare sizes
int result = string.Compare(str1, str2); // Returns -1, 0, or 1

Split and Join

// Splitting game skills
string skillConfig = "Fireball IceSword LightningStrike";
string[] skills = skillConfig.Split(' ');

// Joining game items
string[] items = { "Sword", "Shield", "Potion" };
string playerInventory = string.Join(" + ", items);

Format

// Basic formatting
string playerName = "Hero";
int level = 99;
string status = string.Format("Player {0} reached level {1}", playerName, level);

// Interpolated string
string gameInfo = $"Player {playerName} current level {level}";

// Number formatting
double damage = 123.456;
string damageText = $"Damage: {damage:F2}"; // Keep two decimal places

// Currency formatting
double gold = 1234.56;
string goldText = $"Gold: {gold:C}"; // Currency format

// Alignment formatting
string itemLog = $"{'Item Name',10}{'Quantity',5}";

// Base conversion
int exp = 100;
string hexExp = $"Experience: {exp:X}"; // Hexadecimal

3. StringBuilder

StringBuilder is designed for efficiently modifying strings without the need to create numerous new instances.

using System.Text;

class GameLogBuilder
{
public string BuildGameLog()
{
// Create StringBuilder
StringBuilder log = new StringBuilder();

// Append log information
log.Append("Player entered the game ");
log.AppendLine("Time: " + DateTime.Now);

// Add multiple pieces of information
log.AppendFormat("Level: {0} ", 99);
log.AppendLine("Map: Main City");

// Insert information
log.Insert(0, "[Game Log] ");

// Replace information
log.Replace("Main City", "Novice Village");

// Get the final log
string finalLog = log.ToString();
return finalLog;
}
}

4. Regular Expressions 🔗

Regular Expression Basics

using System;
using System.Text.RegularExpressions;

class RegexDemo
{
static void Main()
{
// Create Regex objects
Regex digitRegex = new Regex(@"\d+"); // Matches one or more digits
Regex emailRegex = new Regex(@"\b\w+@\w+\.\w+\b"); // Simple email match

// Directly match digits
bool hasDigits = Regex.IsMatch("Player123", @"\d+");
Console.WriteLine($"Contains digits: {hasDigits}");

// Email validation
string email = "player@game.com";
bool isValidEmail = Regex.IsMatch(email, @"\b\w+@\w+\.\w+\b");
Console.WriteLine($"Is email valid: {isValidEmail}");

// Extract matched content
string text = "Player Level: 99, Score: 1000";
Match match = Regex.Match(text, @"\d+");
if (match.Success)
{
Console.WriteLine($"Extracted number: {match.Value}");
}
}
}

Regular Expression Matching

public class RegexPatterns
{
// Number matching
public static readonly string Numbers = @"\d+";

// Email matching
public static readonly string Email = @"^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$";

// Phone number (China)
public static readonly string ChinesePhone = @"^1[3-9]\d{9}$";

// URL matching
public static readonly string Url = @"https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)";

// Date format (yyyy-MM-dd)
public static readonly string Date = @"^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$";
}

Regular Expression Groups

using System;
using System.Text.RegularExpressions;

class RegexGroupDemo
{
static void Main()
{
// Match player information: Name (group) and Level (group)
string pattern = @"(\w+):(\d+)";
string input = "Hero:99 Mage:80 Archer:75";

// Find all matches
MatchCollection matches = Regex.Matches(input, pattern);

// Iterate through groups
foreach (Match match in matches)
{
// Entire match
Console.WriteLine($"Full match: {match.Value}");

// Group access
string playerName = match.Groups[1].Value;
string playerLevel = match.Groups[2].Value;

Console.WriteLine($"Player: {playerName}, Level: {playerLevel}");
}

// Named groups
string namedPattern = @"(?<Name>\w+):(?<Level>\d+)";
Match namedMatch = Regex.Match(input, namedPattern);

if (namedMatch.Success)
{
Console.WriteLine($"Named group - Player: {namedMatch.Groups["Name"].Value}");
Console.WriteLine($"Named group - Level: {namedMatch.Groups["Level"].Value}");
}
}
}

Working with strings and regular expressions is an integral part of C# development. Whether manipulating strings, performing pattern matching, or utilizing regular expressions for complex text processing, mastering these concepts enhances your ability to handle and process textual data effectively in C#.

VII. Operators 🔢

1. Arithmetic Operators 🧮

Arithmetic operators: +, -, *, /, and % for performing arithmetic operations.

They include:

  • Addition (+): Adds two operands.
  • Subtraction (-): Subtracts the right operand from the left operand.
  • Multiplication (*): Multiplies two operands.
  • Division (/): Divides the left operand by the right operand.
  • Modulus (%): Returns the remainder of the division.

2. Comparison Operators 🔃

Comparison operators: ==, !=, <, >, <=, and >= for comparing values.

They include:

  • Equal to (==): Checks if two operands are equal.
  • Not equal to (!=): Checks if two operands are not equal.
  • Greater than (>): Checks if the left operand is greater than the right operand.
  • Less than (<): Checks if the left operand is less than the right operand.
  • Greater than or equal to (>=): Checks if the left operand is greater than or equal to the right operand.
  • Less than or equal to (<=): Checks if the left operand is less than or equal to the right operand.
提示

Comparison (==, !=) and relational (<, >, <=, >=) operators are used to compare two values.

It’s important to remember that when comparing reference types, the == operator checks for reference equality, not content.

3. Logical Operators 🔄

Logical operators: &&, ||, and ! for creating logical expressions.

They include:

  • Logical AND (&&): Returns true if both operands are true.
  • Logical OR (||): Returns true if at least one operand is true.
  • Logical NOT (!): Returns true if the operand is false, and vice versa.

4. Assignment Operators 🔧

Assignment operators are used to assign values to variables.

Assignment (=): Assigns the value on the right to the variable on the left.

5. Bitwise operators 🔃

Bitwise operators: &, |, ^, ~, <<, and >> for manipulating bits.

Bitwise operations allow for manipulations at the level of individual bits of a numerical value.

6. Priority 🔢

It’s important to know operator precedence as it affects the order of operations.

提示

Priority is sorted from high to low.

CategoryOperatorsAssociativity
Primaryx.y, f(x), a[x], x++, x--Left to right
Unary+, -, !, ~, ++x, --x, (T)xRight to left
Multiplicative*, /, %Left to right
Additive+, -Left to right
Shift<<, >>Left to right
Relational<, >, <=, >=, is, asLeft to right
Equality==, !=Left to right
Logical AND&Left to right
Logical XOR^Left to right
Logical OR|Left to right
Conditional AND&&Left to right
Conditional OR||Left to right
Conditional?:Right to left
Assignment=, +=, -=, *=, /=Right to left

7. Operator Overloading 🔢

In C#, operator overloading allows you to redefine the way built-in operators work for user-defined types such as classes and structs.

To overload an operator, you define a static method in your class or struct with the operator keyword followed by the operator symbol you want to overload.

The method must return a result and take at least one parameter of the type you’re overloading the operator for.

Here’s a simple example of overloading the + operator for a custom Vector class:

public class Program
{
public static void Main()
{
Vector vector1 = new Vector(1, 2);
Vector vector2 = new Vector(2, 3);
Vector result = vector1 + vector2; // This will call the overloaded + operator
Console.WriteLine(result);
}
public class Vector(int x, int y)
{
private int X { get; } = x;
private int Y { get; } = y;

// Overload + operator
public static Vector operator +(Vector v1, Vector v2)
{
return new Vector(v1.X + v2.X, v1.Y + v2.Y);
}

public override string ToString()
{
return $"Vector({X}, {Y})";
}
}
}

VIII. Process Control 🔄

Control structures and loops are fundamental elements of any program, allowing developers to efficiently manage the flow of code execution.

1. Conditional Statements 🤔

  • if-else: Used to perform actions based on the truthfulness of a condition
  • switch-case: Used to choose one block of code to execute from multiple possibilities

if-else statements are used for conditional execution. They allow your program to take different paths based on whether a certain condition is true or false.

The switch operator allows you to check a variable against multiple values.

It is more compact and often more convenient for checking the values of a single variable.

Nested Control Structures: You can nest control structures within each other to create more complex logic.

public class Program
{
public static void Main()
{
CheckPlayerStatus(99, true);
}

static void CheckPlayerStatus(int level, bool isVip)
{
// Basic if-else
if (level < 10)
{
Console.WriteLine("Newbie Player");
}
else if (level < 50)
{
Console.WriteLine("Regular Player");
}
else
{
Console.WriteLine("Advanced Player");
}

// Nested if-else
if (level >= 50)
{
if (isVip)
{
Console.WriteLine("Prestigious VIP Player");
}
else
{
Console.WriteLine("Regular Advanced Player");
}
}

// Complex condition check
if (level > 90 && isVip)
{
Console.WriteLine("Top VIP Player");
}

var playerType = level switch
{
< 10 => "Newbie",
>= 10 and < 50 => isVip ? "Regular VIP" : "Regular",
>= 50 and < 90 => isVip ? "Expert VIP" : "Expert",
>= 90 => isVip ? "Top VIP" : "Master"
};
Console.WriteLine($"Player Type: {playerType}");
}
}

2. Loop Structure 🔁

Loops allow you to execute a block of code repeatedly.

  • for: Used when the number of iterations is known
  • foreach: Used for iterating over over all elements of a collection
  • while: Used when the number of iterations is unknown
  • do-while: Guarantees the execution of the code block at least once

Choosing the best loop depends on the specific situation.

  • break exits the loop prematurely
  • continue does not terminate the loop, but continues with the next iteration

A nested loop is a loop placed inside another loop. It is often used for processing a two-dimensional array or matrix.

class SimpleRPGGame
{
// Player class
class Player(string? name)
{
private string? Name { get; } = name;
public int Health { get; set; } = 100;
public int Attack => 10;

public void DisplayStatus()
{
Console.WriteLine($"Player {Name} - Health: {Health}, Attack: {Attack}");
}
}

// Monster class
class Monster
{
public string Name { get; }
public int Health { get; set; }
public int Attack { get; }

public Monster()
{
string[] monsterNames = ["Goblin", "Skeleton Warrior", "Dragon"];
Random random = new Random();
Name = monsterNames[random.Next(monsterNames.Length)];
Health = random.Next(50, 100);
Attack = random.Next(5, 15);
}

public void DisplayStatus()
{
Console.WriteLine($"Monster {Name} - Health: {Health}, Attack: {Attack}");
}
}

// Main game logic
static void Main()
{
Console.WriteLine("Welcome to the Simple RPG Game!");

// Create player
Console.Write("Please enter your character name: ");
string? playerName = Console.ReadLine();
if (playerName != null)
{
Player player = new Player(playerName);

// Battle system
while (true)
{
// Generate random monster
Monster monster = new Monster();

Console.WriteLine("\nEncountered a new monster!");
monster.DisplayStatus();
player.DisplayStatus();

// Battle choices
Console.WriteLine("\nChoose your action:");
Console.WriteLine("1. Attack");
Console.WriteLine("2. Run away");
string? choice = Console.ReadLine();

if (choice == "1")
{
// Battle logic
while (player.Health > 0 && monster.Health > 0)
{
// Player attacks
monster.Health -= player.Attack;
Console.WriteLine($"You attacked {monster.Name}");

// Monster attacks
if (monster.Health > 0)
{
player.Health -= monster.Attack;
Console.WriteLine($"{monster.Name} attacked you");
}

// Display status
player.DisplayStatus();
monster.DisplayStatus();

// Check battle result
if (monster.Health <= 0)
{
Console.WriteLine("You defeated the monster!");
break;
}

if (player.Health <= 0)
{
Console.WriteLine("Game over, you were defeated!");
return;
}
}
}
else
{
Console.WriteLine("You chose to run away...");
continue;
}

// Ask if continue
Console.WriteLine("\nDo you want to continue the adventure? (y/n)");
if (Console.ReadLine()?.ToLower() != "y")
{
break;
}
}
}

Console.WriteLine("Game over, thank you for playing!");
}
}