I need to make a little hiking toy in WPF. The game board will have an arbitrary size (the user himself indicates how many cells there are, but not more than 100 ** 100). On the board itself randomly placed obstacles that can not go game figures. Each figure can walk according to certain rules (one on the spike, the other in the vertical, the third as a pretzel, etc.). During the course, the user selects the figures, and indicates the final field where it should go, if the indication does not contradict the rules, then the figure moves to the selected field, and the path is highlighted in red. Tell me, please, how best to implement such a venture in WPF?

For the time being, I plan to initially create a grid of 100 * 100, in each of its cells put a picture corresponding to the background color, put a Canvas in each picture, and place the figures that make catechins in the desired Canvas. Further, when the user enters the field sizes, I plan to delete extra rows and columns, I just don’t know how to do this in C # (purely in the XAML markup, as I understand it, not to do it.)

The second problem is the obstacles (walls), which are placed in a random order, between the game cells (fields), I think, or try to use a stroke of the picture (select it with an appropriate color) to show the presence of the wall, is it a good idea and how can to do? (I don’t know how to get to the frame of the picture programmatically in C # (in the xaml markup, this is probably not done)) ....

I know that wpf may not be the best technology for this task, but I want to deal with wpf.

  • @ Acne: you are 100% right, I’ll change it in code. (I answer here, there is a limit of comments.) - VladD

1 answer 1

(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.

  • @vvtvvtvvt: Well, for example, you can bind these editboxes to Board.NumberOfCols and Board.NumberOfRows. Insert the board somewhere in the main window, just for God's sake do not use the visual editor, but through some suitable container. - VladD
  • @vvtvvtvvt: DataContext will always be null for the first time, because when the BoardPresentation is constructed, the DataContext property has not yet been set. It will be installed later, for this we subscribe to its changes. Then, everything starts from the MainWindow constructor. This is not quite true: the mapping should follow the logic, and not vice versa. I would remove StartupUri from App.xaml, and create a window in the code responsible for the logic. Regarding the order of constructors, I think the BoardPresentation constructor is called from InitializeComponent inside the MainWindow constructor. Something like this. - VladD
  • one
    @VladD, purely my remark: It is desirable to call logical properties, fields (your Vertical property) as IsPropertyName, HasPropertyName, etc. => IsVertical I do not know, but this is purely my opinion - Acne
  • @vvtvvtvvt: also simple: new ColumnDefinition() { Width = new GridLength(5, GridUnitType.Star) . Here is the documentation with examples. - VladD
  • Thank you, I fixed everything myself. Everything is working. But there was a problem. We, as I understand it, created a visual representation of the BoardPresentation and a logical presentation of the Board class and connected them via a DataContext (they wrote down a Board object in it and made that then every time the DataContex changed, the board would redraw.) And now what if I want to add another control (let's call it NewPresentation) and a logical class for it (let's call New)? - shc345