(Updated code from the standpoint of the end of 2016.)
It means so. First, you need to separate the logic of the game from the presentation. Once and for all.
Remember: you must have an object in the layer of logic, which is a field, obstacles and all that, and let the presentation layer deal with its display. Mix logic and presentation == govnokod.
To begin with, a general auxiliary base class that implements INotifyPropertyChanged
(usually it is in your MVVM framework, or you drag it from project to project):
class VM : INotifyPropertyChanged { protected bool Set<T>(ref T storage, T value, [CallerMemberName] string propertyName = null) { if (EqualityComparer<T>.Default.Equals(storage, value)) return false; storage = value; RaisePropertyChanged(propertyName); return true; } protected void RaisePropertyChanged([CallerMemberName] string propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); public event PropertyChangedEventHandler PropertyChanged; }
Okay, let's go make sketches.
class BoardVM : VM // это наша доска, понятно { #region property int NumberOfRows int numberOfRows; public int NumberOfRows { get { return numberOfRows; } set { Set(ref numberOfRows, value); } } #endregion #region property int NumberOfCols // аналогично #endregion
Well, now we need obstacles. They are located between the cells, and should in theory belong to the board. Good. We need a separate type that describes the obstacle, let it be the Obstacle
. The board contains a list of obstacles, which, of course, may vary.
public ObservableCollection<Obstacle> Obstacles { get; private set; } }
For the time being, nothing more is needed, but perhaps we will need another list of figures.
So, the obstacle. The obstacles, in theory, do not "float", so their position may be an ordinary property. Let the obstacle be vertical / horizontal, we will set the initial cell and direction. We may have many types of obstacles, so we must consider the possibility that we will have spawned classes.
class Obstacle { public int X { get; } public int Y { get; } public int Length { get; } public bool IsVertical { get; } }
Okay, it's time to paint. Since the board is a non-trivial object, we will create a UserControl
for it.
<UserControl x:Class="YourCoolGame.View.BoardPresentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <!-- здесь будут края вашей доски, возможно, вы хотите на них что-то нарисовать --> <Grid> <!-- А это сама доска. Заполнять будем по рабоче-крестьянски, в code-behind --> <Grid x:Name="CellsHost"/> <!-- тут ещё какие-нибудь контролы, если надо --> </Grid> </UserControl>
Now the code-behind.
// там где-то выше namespace YourCoolGame.View class BoardPresentation : UserControl { public BoardPresentation() { InitializeComponent(); // окей, нам надо подписаться на изменение объекта из слоя логики // который, как вы уже обязаны знать, придёт к нам через DataContext DataContextChanged += (sender, args) => OnBoardChanged((BoardVM)args.OldValue, (BoardVM)args.NewValue); OnBoardChanged(null, (BoardVM)DataContext); } void OnBoardChanged(BoardVM prev, BoardVM curr) { // окей, у нас новая доска. нам надо подписаться на изменение всего, // что нам интересно. но для начала отписаться от старой доски if (prev != null) prev.PropertyChanged -= OnBoardGeometryChanged; OnBoardGeometryChanged(); if (curr != null) // ну и подписываемся curr.PropertyChanged -= OnBoardGeometryChanged; }
Here, the preparations are over, in theory. Now it remains only to create the desired board.
// аргументы игнорируются void OnBoardGeometryChanged(object sender, EventArgs e) { BoardMV board = (BoardMV)DataContext; if (board != null) SetBoardSize(board.NumberOfCols, board.NumberOfRows); else SetBoardSize(0, 0); } void SetBoardSize(int cols, int rows) { // Будем держать в Grid'е 1 + 2 * cols столбцов: // для клеток и для препятствий. То же со строками. var neededNumberOfCols = 2 * cols + 1; var actualNumberOfCols = CellsHost.ColumnDefinitions.Count; if (neededNumberOfCols > actualNumberOfCols) { // добавляем for (int i = actualNumberOfCols; i < neededNumberOfCols; i++) { bool isBorderCell = (i % 2 == 0); CellsHost.ColumnDefinitions.Add( new ColumnDefinition() { Width = isBorderCell ? BorderThickness : CellSize }); // добавили столбец? теперь его надо заполнить // во все строки добавляем по клетке if (!isBorderCell) for (int j = 1; j < CellsHost.RowDefinitions.Count; j += 2) AddCellAt(i / 2, j / 2); } } else { // убираем for (int i = actualNumberOfCols, i > neededNumberOfCols; i--) { bool isBorderCell = (i % 2 == 0); if (!isBorderCell) for (int j = 1; j < CellsHost.RowDefinitions.Count; j += 2) RemoveCellAt(i / 2, j / 2); CellsHost.ColumnDefinitions.RemoveAt(i); } } // ну и то же самое для строк }
We have the playing field cells. They can do all sorts of interesting things, for example, be painted in different colors, intercept mouse events, and so on. Let's write functions for them.
void AddCellAt(int i, int j) { var cell = new Cell(i, j, GetColorForCell(i, j)) { DataContext = DataContext }; int colInGrid = 2 * i + 1; int rowInGrid = 2 * j + 1; Grid.SetColumn(cell, colInGrid); Grid.SetRow(cell, rowInGrid); CellsHost.Children.Add(cell); } void RemoveCellAt(int i, int j) { var cell = CellsHost.Children.OfType<Cell>() .Where(c => c.Col = i && c.Row == j) .Single(); CellsHost.Children.Remove(cell); } }
Well, there still it would be necessary to add Obsacle
, but you already understand it. For now we will describe Cell
. Cell
, of course, also UserControl
, a simple one.
<UserControl x:Class="YourCoolGame.View.Cell" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Grid Name="ContentElement" Click="OnClick"/> </UserControl>
Well, the code behind:
// там где-то выше снова namespace YourCoolGame.View class Cell : UserControl { int col, row; // конструктор по умолчанию нужен public Cell() : this(-1, -1, Colors.Transparent) { } public Cell(int col, int row, Color color) { InitializeComponent(); this.col = col; this.row = row; ContentElement.Background = new SolidColorBrush(color); } // это на самом деле не очень хороший момент, т. к. получается // сильная связность. развязаться можно при помощи команд, например. void OnClick(object sender, RoutedEventArgs e) { BoardVM board = (BoardVM)DataContext; board.ActivateCell(col, row); } }
That seems to be all. In obstacles, work the same way as with cells: subscribe to Obstacles
changes, get a UserControl
representing the obstacle, and add it to the Grid
where you need it.