D
I've previously shown all the plays in WPF. https://ru.stackoverflow.com/a/1222168/373567 ♪ https://ru.stackoverflow.com/a/1255542/373567 ♪ https://ru.stackoverflow.com/a/1239671/373567 I'll show you again.I'll use the MVM design template, but I won't talk about it. If you're interested, you can look at the references above. There's only a code and a brief explanation for it.Support classesJust put them in the project.public class NotifyPropertyChanged : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public class RelayCommand : ICommand
{
private readonly Action<object> _execute;
private readonly Predicate<object> _canExecute;
public event EventHandler CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
public RelayCommand(Action<object> execute, Predicate<object> canExecute = null)
=> (_execute, _canExecute) = (execute, canExecute);
public bool CanExecute(object parameter)
=> _canExecute == null || _canExecute(parameter);
public void Execute(object parameter)
=> _execute(parameter);
}
DataI'll use the same boolbut for the interface to be able to automatically update itself when the playground cell is changed, implementation is required INotifyPropertyChangedShe's in one of the classes above. Just using it.public class Cell : NotifyPropertyChanged
{
private bool _state;
public bool State
{
get => _state;
set
{
_state = value;
OnPropertyChanged(); // сообщает интерфейсу что надо обновить эту ячейку
}
}
}
The game field is a kind of mutant looking at one end in the interface, another to you for easy use in the code.public class GameField : IEnumerable
{
private readonly int _rows;
private readonly int _cols;
private readonly Cell[] _data;
public int Rows => _rows;
public int Cols => _cols;
public bool this[int row, int col]
{
get => _data[row * Cols + col].State;
set => _data[row * Cols + col].State = value;
}
public void Clear()
{
for (int i = 0; i < _data.Length; i++)
_data[i].State = false;
}
public GameField(int rows, int cols)
{
_rows = rows;
_cols = cols;
_data = new Cell[rows * cols];
for (int i = 0; i < _data.Length; i++)
_data[i] = new Cell();
}
IEnumerator IEnumerable.GetEnumerator()
=> _data.GetEnumerator();
}
The one. GetEnumerator() It's just that we need an interface to paint, but at the same time, this game field can work as a two-dimensional body in the code. It's simple, no game logic here, it's just a bridge between the game and the interface.I'm not running a game logic, you're gonna have to do it. There's only a demonstration of the technology you must have come here for.Game logicCode comments// состояние игры
public enum GameState
{
Over,
Running,
Paused
}
public class Game
{
// мне показалось, события здесь хорошо подходят
public event Action<int> ScoreChanged; //возникает при изменении очков
public event Action<GameState> StateChanged; // возникает при изменении состояния игры
private int _score;
private GameState _state;
private readonly GameField _field;
private CancellationTokenSource _cts;
public int Score
{
get => _score;
private set
{
_score = value;
ScoreChanged?.Invoke(value);
}
}
public GameState State
{
get => _state;
private set
{
_state = value;
StateChanged?.Invoke(value);
}
}
public Game(GameField field)
{
_field = field;
}
public async void Start()
{
// если игра уже запущена, повторно не запускать
if (State == GameState.Running)
return;
// пример работы с полем
_field[3, 1] = true;
_field[4, 1] = true;
_field[5, 1] = true;
_field[5, 2] = true;
_field[3, 4] = true;
_field[4, 4] = true;
_field[5, 4] = true;
_field[4, 5] = true;
_field[4, 7] = true;
_field[5, 7] = true;
_field[4, 8] = true;
_field[5, 8] = true;
// собственно, сам запуск игры
State = GameState.Running;
using (_cts = new CancellationTokenSource())
{
await RunGameLoop(_cts.Token);
}
_cts = null;
}
private async Task RunGameLoop(CancellationToken token)
{
try
{
// асинхронный цикл, чтобы не блокировать интерфейс во время выполнения
while (true)
{
Score += 10;
await Task.Delay(1000, token); // подождать секунду
}
}
catch (OperationCanceledException) { } // при отмене токена выбрасывается исключение в Task.Delay, ловим здесь, это нормально
}
public void Pause()
{
if (State != GameState.Running)
return;
_cts?.Cancel(); // остановить игру
_field.Clear(); // очистить поле
State = GameState.Paused;
}
}
That's the logic.View ModelThis class is responsible for providing an interface for display. The example of using a team shows that the rest should look simple.public class MainViewModel : NotifyPropertyChanged
{
private const int rows = 20;
private const int cols = 10;
private int _score;
private GameField _field;
private GameState _state;
private Game _game;
private ICommand _startCommand;
public GameField Field
{
get => _field;
set
{
_field = value;
OnPropertyChanged();
}
}
public GameState State
{
get => _state;
set
{
_state = value;
OnPropertyChanged();
}
}
public int Score
{
get => _score;
set
{
_score = value;
OnPropertyChanged();
}
}
public ICommand StartCommand => _startCommand ??= new RelayCommand(parameter =>
{
if (State == GameState.Running)
_game.Pause();
else
_game.Start();
});
public MainViewModel()
{
Field = new GameField(rows, cols);
_game = new Game(Field);
_game.ScoreChanged += (s) => Score = s;
_game.StateChanged += (s) => State = s;
}
}
ViewThe interface itself.<Window x:Class="WpfTetris.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:local="clr-namespace:WpfTetris"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800" d:DataContext="{d:DesignInstance local:MainViewModel}">
<Window.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
</Window.Resources>
<Window.InputBindings>
<KeyBinding Key="Space" Command="{Binding StartCommand}"/>
</Window.InputBindings>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<StackPanel>
<Button Command="{Binding StartCommand}" Margin="5">
<Button.Style>
<Style TargetType="Button">
<Setter Property="Content" Value="Start"/>
<Style.Triggers>
<DataTrigger Binding="{Binding State}" Value="Running">
<Setter Property="Content" Value="Pause"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</StackPanel>
<StackPanel Grid.Column="1">
<ItemsControl ItemsSource="{Binding Field}" SnapsToDevicePixels="True" >
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderBrush="LightGray" BorderThickness="1" Width="20" Height="20">
<Border Background="Gray" Margin="1" Visibility="{Binding State, Converter={StaticResource BooleanToVisibilityConverter}}"/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Rows="{Binding Field.Rows}" Columns="{Binding Field.Cols}"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</StackPanel>
<StackPanel Grid.Column="2">
<StackPanel Orientation="Horizontal" Margin="5">
<TextBlock Text="Score: "/>
<TextBlock Text="{Binding Score}" FontWeight="Bold"/>
</StackPanel>
</StackPanel>
</Grid>
</Window>
I did three columns on the left button, in the middle of the glass, right of the glass.For example, I had a team gap StartCommandthe same as the button. In the same way, you can tie the shooters to other teams.And most importantly, how to connect the vimodel to the window is the hardest part of this annex.public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainViewModel();
}
}
Start with the simple, if the figure is one square that falls at the speed you need and stops at the bottom of the glass or above the square already lying there. After he fell, let the glasses be calculated. When the glass is filled, the game should end. As soon as you've done it in your entirety, take care of the pieces and turn them around.