I am re-creating the game of cluedo and I want to map the possible paths that the player can move after dice roll.
I have mapped the grid by drawing pictureboxes and naming them to their mapped location.
Here is my code so far for the possible paths:
int Roll;
private void RollDice()
{
ResetTiles();
JimRandom Random = new JimRandom();
//Roll DIce 1
int dice1 = Random.Next(1, 7);
//Roll DIce 2
int dice2 = Random.Next(1, 7);
Roll = dice1 + dice2;
//Set Dice images
pbDice1.BackgroundImage = Roller[dice1 - 1].Picture;
pbDice2.BackgroundImage = Roller[dice2 - 1].Picture;
btnRoll.Enabled = false;
Test(Roll);
//Show available moves
Control[] lCurrent = PnlBoard.Controls.Find("pnl" + CurrentPlauer, true);
Panel Current = null;
System.Drawing.Point CurrentLoc = new System.Drawing.Point(0, 0);
foreach (Control c in lCurrent)
{
Current = c as Panel;
CurrentLoc = new System.Drawing.Point(c.Location.X, c.Location.Y);
}
//Dynamic map
List<string> possiblities = new List<string>();
int currentRow = CurrentLoc.Y / tileWidth;
int currentCol = CurrentLoc.X / tileHeight;
//Find all possible start blocks
string down = String.Format("Col={0:00}-Row={1:00}", currentCol, currentRow + 1);
string up = String.Format("Col={0:00}-Row={1:00}", currentCol, currentRow - 1);
string left = String.Format("Col={0:00}-Row={1:00}", currentCol - 1, currentRow);
string right = String.Format("Col={0:00}-Row={1:00}", currentCol + 1, currentRow);
List<string> startBlocks = new List<string>();
//See if down is available
Control[] LPossible = PnlBoard.Controls.Find(down, true);
if (LPossible.Length > 0)
{
startBlocks.Add(down);
}
//See if Up is available
LPossible = PnlBoard.Controls.Find(up, true);
if (LPossible.Length > 0)
{
startBlocks.Add(up);
}
//See if left is available
LPossible = PnlBoard.Controls.Find(left, true);
if (LPossible.Length > 0)
{
startBlocks.Add(left);
}
//See if right is available
LPossible = PnlBoard.Controls.Find(right, true);
if (LPossible.Length > 0)
{
startBlocks.Add(right);
}
//possiblilities 1
foreach (string s in startBlocks)
{
Control[] lStarBlock = PnlBoard.Controls.Find(s, true);
PictureBox startBlock = lStarBlock[0] as PictureBox;
int sRow = startBlock.Location.Y / tileWidth;
int sCol = startBlock.Location.X / tileHeight;
//Rows
for (int row = sRow; row < sRow + Roll; row++)
{
//Columns
for (int col = sCol; col < sCol + Roll; col++)
{
possiblities.Add(String.Format("Col={0:00}-Row={1:00}", col, row));
}
}
}
//Show possible moves
foreach (string p in possiblities)
{
LPossible = PnlBoard.Controls.Find(p, true);
if (LPossible.Length > 0)
{
PictureBox active = LPossible[0] as PictureBox;
active.Image = Cluedo.Properties.Resources.TileActive;
System.Threading.Thread.Sleep(1);
Application.DoEvents();
}
//else
//{
// break;
//}
}
}
There's a lot of things I would do different here. This is more of a Code Review post, but there's a solution to your problem at the end, and perhaps the rest can help you to improve the overall state of your code.
Randomness
You're creating a new random generator instance for every method call:
JimRandom Random = new JimRandom();
This often results in the same values being generated if the method is called in rapid succession. Perhaps that's why you're using a cryptographic RNG instead of a PRNG? A PRNG should be sufficient for a game like this, as long as you reuse it.
Using the right types
You're determining the current player location with the following code:
//Show available moves
Control[] lCurrent = PnlBoard.Controls.Find("pnl" + CurrentPlauer, true);
Panel Current = null;
System.Drawing.Point CurrentLoc = new System.Drawing.Point(0, 0);
foreach (Control c in lCurrent)
{
Current = c as Panel;
CurrentLoc = new System.Drawing.Point(c.Location.X, c.Location.Y);
}
It looks like CurrentPlauer is a string. Creating a Player class that stores the name and current location of a player would make things much easier:
Point currentLocation = currentPlayer.Location;
Splitting game logic from UI code
You're checking for passable tiles by doing string lookups against controls:
string down = String.Format("Col={0:00}-Row={1:00}", currentCol, currentRow + 1);
// ...
Control[] LPossible = PnlBoard.Controls.Find(down, true);
if (LPossible.Length > 0)
{
startBlocks.Add(down);
}
Normally a 2D array is used for tile maps like these, possible encapsulated in a Tilemap or Map class. This makes working with tiles more natural, as you can work in tile coordinates directly instead of having to translate between UI and tile coordinates. It also breaks up the code more cleanly into a game-logic and a UI part (the code in your post is impossible to test without UI):
// TileMap class:
public bool IsPassable(int x, int y)
{
if (x < 0 || x >= Width || y < 0 || y >= Height)
return false;
return tiles[x][y] != Tile.Wall; // enum Tile { Wall, Ballroom, DiningRoom, Hall, ... }
}
// When called from your Board code:
if (map.IsPassable(currentLocation.X, currentLocation.Y + 1))
startBlocks.Add(new Point(currentLocation.X, currentLocation.Y + 1));
Reducing repetition
As for checking all direct neighboring tiles, there's no need to repeat the same code 4 times:
// Let's make a utility function:
public static IEnumerable<Point> GetNeighboringPositions(Point position)
{
yield return new Point(position.X - 1, position.Y);
yield return new Point(position.X, position.Y - 1);
yield return new Point(position.X + 1, position.Y);
yield return new Point(position.X, position.Y + 1);
}
// In the Board code:
foreach (Point neighboringPosition in GetNeighboringPositions(currentPosition))
{
if (map.IsPassable(neighboringPosition.X, neighboringPosition.Y))
startBlocks.Add(neighboringPosition);
}
Determining valid moves
Finally, we get to the code that determines which tiles the current player can move to:
//possiblilities 1
foreach (string s in startBlocks)
{
Control[] lStarBlock = PnlBoard.Controls.Find(s, true);
PictureBox startBlock = lStarBlock[0] as PictureBox;
int sRow = startBlock.Location.Y / tileWidth;
int sCol = startBlock.Location.X / tileHeight;
//Rows
for (int row = sRow; row < sRow + Roll; row++)
{
//Columns
for (int col = sCol; col < sCol + Roll; col++)
{
possiblities.Add(String.Format("Col={0:00}-Row={1:00}", col, row));
}
}
}
What this does is checking a rectangular area, using a starting position as its top-left corner. It's doing so for up to 4 neighboring positions, so the rectangles will partially overlap each other. That's just not going to work. If the map didn't have any obstacles, something like this, combined with a Manhattan distance check, could work (if you don't forget to look to the left and upwards too). Or better, some fancy looping that checks a diamond-shaped area.
However, you've got walls to deal with, so you'll need a different approach. The player's current position is at distance 0. Its direct neighbors are at distance 1. Their neighbors are at distance 2 - except those tiles that are at a lower distance (the tiles that have already been covered). Any neighbours of tiles at distance 2 are either at distance 3, or have already been covered. Of course, wall tiles must be skipped.
So you need to keep track of what tiles have already been covered and what neighboring tiles you still need to check, until you run out of movement points. Let's wrap that up into a reusable method:
public List<Point> GetReachableTiles(Point currentPosition, int maxDistance)
{
List<Point> coveredTiles = new List<Point> { currentPosition };
List<Point> boundaryTiles = new List<Point> { currentPosition };
for (int distance = 0; distance < maxDistance; distance++)
{
List<Point> nextBoundaryTiles = new List<Point>();
foreach (Point position in boundaryTiles)
{
foreach (Point pos in GetNeighboringPositions(position))
{
// You may also want to check against other player positions, if players can block each other:
if (!coveredTiles.Contains(pos) && !boundaryTiles.Contains(pos) && map.IsPassable(pos.X, pos.Y))
{
// We found a passable tile:
coveredTiles.Add(pos);
// And we want to check its neighbors in the next 'distance' iteration, too:
nextBoundaryTiles.Add(pos);
}
}
}
// The next 'distance' iteration should check the neighbors of the current boundary tiles:
boundaryTiles = nextBoundaryTiles;
}
return coveredTiles;
}
Related
I wrote a function that checks for a given cell in a grid if it is a legal move following the rules of the board-game called Reversi otherwise known as Othello. The rules are that a circle can only be placed on the grid whenever the newly placed circle and a previous placed circle bounds one of the opponents circles. The majority of the time the function gives the right output (i.e. true when it is a legal move and false when it is a legal move), but some moves that are legal by the previously stated rules are not deemed as a legal move by the function.
I've tried to use the console to check at every step at which cell the function is currently looking and what the value of the cell is to determine what goes wrong. This has only led me to being even more confused.
The following code is the doomed-function:
bool legalMove(int row, int col)
{
// Check if the cell is occupied
if (board[row,col] != 0)
return false;
// Check if there's an opponents circle somewhere around it
for (int i = -1; i<=1; i++)
for (int j = -1; j<=1; j++)
{
if (i == 0 && j == 0)
continue;
int currentRow = row + i;
int currentCol = col + j;
if (currentRow >= 0 && currentRow < board.GetLength(0) && currentCol >= 0 && currentCol < board.GetLength(1) && board[currentRow,currentCol] == -turn)
{
// Now we know that there's an opponents circle somewhere around this space, we now check if it can be captured
while(true)
{
currentRow += i;
currentCol += j;
Console.WriteLine($"currentRow: {currentRow}, currentCol: {currentCol}, value: {board[currentRow,currentCol]}");
if (currentRow < 0 || currentRow >= board.GetLength(0) || currentCol < 0 || currentCol >= board.GetLength(1) || board[currentRow, currentCol] == 0)
return false; // Outside of the board or an empty space
else if (board[currentRow,currentCol] == turn)
return true; // No empty spaces between our cell and another cell of ours
}
}
}
return false; // No cell found around ours
}
What am I missing here?
Thanks in advance!
EDIT:
The entire program is the following (hope it can help):
/* TO-DO
* Make function out of no legal move and tidy up
* Calculate amount of circles of player to determine the winner
* Make victory label better
!!!Tidy up flipCircles method and fix legalMove method
Create a slider and make it change the gridSize
Make GUI change empty space when board gets smaller or bigger
Tidy up the 2x calling to check what the score is
*/
// Library imports
using System;
using System.Drawing;
using System.Security.Policy;
using System.Windows.Forms;
// Game class
class Game : Form
{
// Declare variables
private Board board;
private Button newGame, help;
private Font font;
private Label countRed, countBlue, gameState;
private TrackBar sizeBoard;
public int gridSize = 6;
public Game()
{
// Set the form properties
ClientSize = new Size(520, 670); Text = "Reversi";
// Creating the GUI and adding it to the form
newGame = new Button(); Controls.Add(newGame);
help = new Button(); Controls.Add(help);
font = new Font("Arial", 14);
countBlue = new Label(); Controls.Add(countBlue); countBlue.Font = font;
countRed = new Label(); Controls.Add(countRed); countRed.Font = font;
gameState = new Label(); Controls.Add(gameState); gameState.Font = font;
board = new Board(gridSize); Controls.Add(board);
// Settings of the GUI
newGame.Size = new Size(100, 30); newGame.Location = new Point(150, 10); newGame.Text = "New Game"; newGame.BackColor = Color.LightSlateGray;
help.Size = new Size(100, 30); help.Location = new Point(270, 10); help.Text = "Help"; help.BackColor = Color.LightSlateGray;
countBlue.Size = new Size(110, 30); countBlue.Location = new Point(150, 50); countBlue.Text = $"{board.countBlue} stones"; countBlue.ForeColor = Color.CornflowerBlue;
countRed.Size = new Size(110, 30); countRed.Location = new Point(150, 90); countRed.Text = $"{board.countRed} stones"; countRed.ForeColor = Color.Firebrick;
gameState.Size = new Size(150, 30); gameState.Location = new Point(270, 90); gameState.Text = $"{board.playersTurn}";
// Events //
// Label events
newGame.Click += reset;
help.Click += calculateHelp;
// Board events
board.MouseClick += clicked;
//Paint event
Paint += paint;
}
// Event-handlers //
// Label event-handlers
private void reset(object e, EventArgs ea)
{
board.Reset();
countBlue.Text = $"{board.countBlue} stones";
countRed.Text = $"{board.countRed} stones";
gameState.Text = $"{board.playersTurn}";
}
private void calculateHelp(object e, EventArgs ea)
{
board.SetHelp();
}
// Board event-handlers
private void clicked(object e, MouseEventArgs mea)
{
board.Clicked(mea.Location);
countBlue.Text = $"{board.countBlue} stones";
countRed.Text = $"{board.countRed} stones";
gameState.Text = $"{board.playersTurn}";
// Now check if there's a legalMove if not
//if (timesNoLegalMove > 1)
gameState.Text = $"{board.playersTurn}";
}
// Paint event-handler
private void paint(object e, PaintEventArgs pea)
{
Graphics gr = pea.Graphics;
gr.FillEllipse(Brushes.CornflowerBlue, 100, 45, 31, 31);
gr.FillEllipse(Brushes.Firebrick, 100, 85, 31, 31);
}
}
// Board class
class Board : Label
{
// Declare all global variables used in this class
private int[,] board;
private int size;
private int turn = 1; // 1 is blue, -1 is red
private bool legalMoveExists = true;
public int timesNoLegalMove = 0;
private bool help = false;
// Create the board and set settings + events
public Board(int gridSize)
{
size = gridSize;
Size = new Size(size * 50, size * 50);
Location = new Point(10 + (25 * (10 - size)), 120 + (25 * (10 - size)));
BackColor = Color.White;
board = new int[size, size];
startingState();
Paint += Draw;
}
// Sets the values of the center 4 squares to that of the starting circles
private void startingState()
{
board[(size / 2)-1, (size / 2)-1] = 1;
board[(size / 2), (size / 2)] = 1;
board[(size / 2), (size / 2) - 1] = -1;
board[(size / 2) - 1, (size / 2)] = -1;
}
// Resets the board when New Game is clicked
public void Reset()
{
for (int row = 0; row < board.GetLength(0); row++)
for (int col = 0; col < board.GetLength(1); col++)
board[row, col] = 0;
startingState();
turn = 1;
Invalidate();
}
public void SetHelp()
{
if (help)
help = false;
else
help = true;
Invalidate();
}
public string playersTurn
{
get
{
if (turn == 1)
return "It's Blue's turn";
if (turn == -1)
return "It's Red's turn";
else
{
if (countBlue > countRed)
return "Blue has won the game!";
if (countRed > countBlue)
return "Red has won the game!";
else
return "It's a draw!";
}
}
}
public int countRed
{
get
{
return board.Cast<int>().Count(n => n == -1);
}
}
public int countBlue
{
get
{
return board.Cast<int>().Count(n => n == 1);
}
}
bool legalMove(int row, int col)
{
// Check if the cell is occupied
if (board[row,col] != 0)
return false;
// Check if there's an opponents circle somewhere around it
for (int i = -1; i<=1; i++)
for (int j = -1; j<=1; j++)
{
if (i == 0 && j == 0)
continue;
int currentRow = row + i;
int currentCol = col + j;
if (currentRow >= 0 && currentRow < board.GetLength(0) && currentCol >= 0 && currentCol < board.GetLength(1) && board[currentRow,currentCol] == -turn)
{
// Now we know that there's an opponents circle somewhere around this space, we now check if it can be captured
while(true)
{
currentRow += i;
currentCol += j;
if (currentRow < 0 || currentRow >= board.GetLength(0) || currentCol < 0 || currentCol >= board.GetLength(1) || board[currentRow, currentCol] == 0)
return false; // Outside of the board or an empty space
else if (board[currentRow,currentCol] == turn)
return true; // No empty spaces between our cell and another cell of ours
}
}
}
return false; // No cell found around ours
}
private void flipCircles(int row, int col)
{
// Check all eight directions from the current position
for (int r = row - 1; r <= row + 1; r++)
{
for (int c = col - 1; c <= col + 1; c++)
{
// Skip the current position
if (r == row && c == col)
continue;
int rr = r;
int cc = c;
// Check if the next position in this direction is a valid position on the board
// and if it is occupied by the opponent's piece
if (rr >= 0 && rr < board.GetLength(0) && cc >= 0 && cc < board.GetLength(1) && board[rr, cc] == -turn)
{
// Keep moving in this direction until we find the current player's piece or an empty cell
while (true)
{
rr += r - row;
cc += c - col;
// If we have reached an invalid position or an empty cell, break out of the loop
if (rr < 0 || rr >= board.GetLength(0) || cc < 0 || cc >= board.GetLength(1) || board[rr, cc] == 0)
break;
// If we have found the current player's piece, flip all the pieces between the current position and the player's piece
if (board[rr, cc] == turn)
{
while (rr != r || cc != c)
{
rr -= r - row;
cc -= c - col;
board[rr, cc] = turn;
}
break;
}
}
}
}
}
}
// Sets the value of a clicked cell to either 1 (Blue), or -1 (Red)
public void Clicked(Point mea)
{
int rowClicked = mea.X / 50;
int colClicked = mea.Y /50;
if (legalMove(rowClicked, colClicked))
{
board[rowClicked, colClicked] = turn;
flipCircles(rowClicked, colClicked);
help = false;
turn = -turn;
legalMoveExists = false;
Invalidate();
}
}
// Draws the entire board and all circles
void Draw(object e, PaintEventArgs pea)
{
Graphics gr = pea.Graphics;
legalMoveExists = false;
for (int row = 0; row < board.GetLength(0); row++)
for (int col = 0; col < board.GetLength(1); col++)
{
// Draw the background tiles
if (row % 2 == 0 && col % 2 == 0 || row % 2 != 0 && col % 2 != 0)
gr.FillRectangle(Brushes.DarkGray, 50 * row, 50 * col, 50, 50);
// Draw circles
if (board[row, col] == 1) // Blue circles
gr.FillEllipse(Brushes.CornflowerBlue, 50 * row - 1, 50 * col - 1, 51, 51);
else if (board[row, col] == -1) // Red circles
gr.FillEllipse(Brushes.Firebrick, 50 * row - 1, 50 * col - 1, 51, 51);
// Check for legal moves and draw help circles if the help button has been pressed
else if (legalMove(row, col))
{
legalMoveExists = true;
timesNoLegalMove = 0;
if (help) // Help circles
gr.DrawEllipse(Pens.Black, 50 * row + 9, 50 * col + 9, 31, 31);
}
}
// Make this a function
if (!legalMoveExists)
{
turn = -turn;
timesNoLegalMove++;
Invalidate();
if (timesNoLegalMove > 1)
turn = 0;
}
}
}
// Main run
class Program
{
static void Main()
{
Application.Run(new Game());
}
}
Your question states that the function you wrote to scan the surrounding cells is failing sporadically and that it's difficult to diagnose. It was easy to reproduce the failures by running your code, but I wasn't able to see an obvious way to effectively debug it.
It "might" be more effective to improve the algorithm where it's more methodical in how it inspects the surrounding cells in the first place, which would also be easier to debug if necessary. One solid way to do this would be to use custom Iterators where you could use a standard foreach pattern to inspect virtual "lines" radiating in the eight directions. At each 'yield' you can check to see whether a determination can be made in terms of either a "legal move" or a "capture".
Here's a proof-of-concept grid that is intended to demonstrate how the iterators work. It doesn't evaluate the cells in terms of game play in any way but you can see how it would lend itself to doing that. The idea here is to click any cell and observe the markup of U-R-D-L. It may also help to see it working so you can clone this sample and set breakpoints.
Left, Right, Up, Down iterator examples are shown - diagonals would follow the same pattern. The mouse down control passes the starting cell coordinate position as a Point:
public IEnumerable<Point> CellsUp(Point point)
{
while (true)
{
point = new Point(point.X, point.Y - 1);
if (point.Y < 0) break;
yield return point;
}
}
public IEnumerable<Point> CellsRight(Point point, int max)
{
while (true)
{
point = new Point(point.X + 1, point.Y);
if (point.X == max) break;
yield return point;
}
}
public IEnumerable<Point> CellsDown(Point point, int max)
{
while (true)
{
yield return point;
point = new Point(point.X, point.Y + 1);
if (point.Y == max) break;
}
}
public IEnumerable<Point> CellsLeft(Point point)
{
while (true)
{
yield return point;
point = new Point(point.X - 1, point.Y);
if (point.X < 0) break;
}
}
The code lays the groundwork for a methodical scan outward from any given point.
private void legalMoveIterationStub(object? sender, EventArgs e)
{
clear();
if(sender is Control control)
{
control.BackColor = Color.Blue;
control.Refresh();
var pos = board.GetCellPosition(control);
var pt = new Point(pos.Column, pos.Row);
Control ctrl;
foreach (var point in CellsUp(pt))
{
ctrl = board.GetControlFromPosition(point.X, point.Y);
ctrl.Text = "U";
ctrl.Refresh();
Thread.Sleep(DEMO_DELAY_MS);
// This is where the cell inspects e.g. for "empty square"
// or color. Chances are, some condition will be met
// and you will break from here rather than iterate
// all the way to the edge of the board each time.
}
foreach (var point in CellsRight(pt, board.ColumnCount))
{
ctrl = board.GetControlFromPosition(point.X, point.Y);
ctrl.Text = "R";
ctrl.Refresh();
Thread.Sleep(DEMO_DELAY_MS);
}
foreach (var point in CellsDown(pt, board.ColumnCount))
{
ctrl = board.GetControlFromPosition(point.X, point.Y);
ctrl.Text = "D";
ctrl.Refresh();
Thread.Sleep(DEMO_DELAY_MS);
}
foreach (var point in CellsLeft(pt))
{
ctrl = board.GetControlFromPosition(point.X, point.Y);
ctrl.Text = "L";
ctrl.Refresh();
Thread.Sleep(DEMO_DELAY_MS);
}
}
}
The demo board has been mocked like this for testing purposes:
public partial class Game : Form
{
public Game()
{
InitializeComponent();
for (int col = 0; col < board.ColumnCount; col++)
{
for (int row = 0; row < board.RowCount; row++)
{
var tile = new Label
{
BorderStyle = BorderStyle.FixedSingle,
Anchor = (AnchorStyles)0xF,
Margin = new Padding(1),
TextAlign = ContentAlignment.MiddleCenter
};
board.Controls.Add(tile, col, row);
tile.MouseDown += legalMove;
}
}
}
void clear()
{
foreach (Control control in board.Controls)
{
control.Text = string.Empty;
control.BackColor = SystemColors.Control;
}
board.Refresh();
}
.
.
.
}
I'm coding a puzzle game where you can slide the tiles horizontally or vertically in a 3x3 gridmap.
If you select a tile, when you press up or down arrow key, the column this selected tile is in moves vertically upwards or downwards by 1 unit. Same applies to horizontal movements.
This will cause the blocks to go over the 3x3 boundary. This is why I have another rule that: when a block is over the 3x3 boundary, it is teleported to the vacant position, filling the grid map. So, for example: the horizontal order of blocks could be (1, 2, 3). After sliding this row of blocks to the left by 1 grid, the order becomes (3, 1, 2). Do it again? It is (2, 3, 1). Here's a screenshot of what the level looks like:
I thought it was a really simple logic to code but it has proven me wrong. It is actually fairly tricky.
I initially assigned each block an order number exactly identical to that of the keypad. So, bottom left block would be 1, then 2 on the right, and 3 on the bottom right... Whenever I pressed number key 1 on keypad and pressed up arrow, I hard-coded it and set the vertical order of blocks (1, 4, 7) to (7, 1, 4).
It doesn't work at all because if I don't reset the position back to normal, and start to change another given row or column, the layout of the map becomes messed up. This is because even if I changed the physical position of the blocks, their assigned order is not changed, which means that if the blocks that are going to be moved are not in their normal position, they can overlap onto other blocks.
Anyways, here is an example of the designed mechanic:
I. Normal position:
II. Slided row (1, 2, 3) right by 1 unit
III. Slided column (2, 5, 8) downwards by 1 unit
Can someone please give me some advice? It doesn't have to be in actual code. I just need some directions to go for... I'm out of ideas now.
As pointed out your images are not quite accurate ^^
Anyway, there might be more efficient and extendable ways but here is what I would do as a first iteration - plain straight forward:
Have a grid component which holds a 3x3 grid and handles all the shift operations
Additionally I will also route all movements through this Grid component in order to easily move tiles together - entire rows or columns - and keep things clean
I hope the comments are clear enough
public class Grid : MonoBehaviour
{
// To make things simple for the demo I simply have 9 "GridElement" objects and place them later based on their index
[SerializeField]
private GridElement[] elements = new GridElement[9];
// stores the current grid state
private readonly GridElement[,] _grid = new GridElement[3, 3];
private void Awake()
{
// go through the grid and assign initial elements to their positions and initialize them
for (var column = 0; column < 3; column++)
{
for (var row = 0; row < 3; row++)
{
_grid[column, row] = elements[row * 3 + column];
_grid[column, row].Initialize(this);
}
}
RefreshIndices();
}
// Shifts the given column one step up with wrap around
// => top element becomes new bottom
public void ShiftColumnUp(int column)
{
var temp = _grid[column, 2];
_grid[column, 2] = _grid[column, 1];
_grid[column, 1] = _grid[column, 0];
_grid[column, 0] = temp;
RefreshIndices();
}
// Shifts the given column one step down with wrap around
// => bottom element becomes new top
public void ShiftColumnDown(int column)
{
var temp = _grid[column, 0];
_grid[column, 0] = _grid[column, 1];
_grid[column, 1] = _grid[column, 2];
_grid[column, 2] = temp;
RefreshIndices();
}
// Shifts the given row one step right with wrap around
// => right element becomes new left
public void ShiftRowRight(int row)
{
var temp = _grid[2, row];
_grid[2, row] = _grid[1, row];
_grid[1, row] = _grid[0, row];
_grid[0, row] = temp;
RefreshIndices();
}
// Shifts the given row one step left with wrap around
// => left element becomes new right
public void ShiftRowLeft(int row)
{
var temp = _grid[0, row];
_grid[0, row] = _grid[1, row];
_grid[1, row] = _grid[2, row];
_grid[2, row] = temp;
RefreshIndices();
}
// Iterates through all grid elements and updates their current row and column indices
// and applies according positions
public void RefreshIndices()
{
for (var column = 0; column < 3; column++)
{
for (var row = 0; row < 3; row++)
{
_grid[column, row].UpdateIndices(row, column);
_grid[column, row].transform.position = new Vector3(column - 1, 0, row - 1);
}
}
}
// Called while dragging an element
// Moves the entire row according to given delta (+/- 1)
public void MoveRow(int targetRow, float delta)
{
for (var column = 0; column < 3; column++)
{
for (var row = 0; row < 3; row++)
{
_grid[column, row].transform.position = new Vector3(column - 1 + (row == targetRow ? delta : 0), 0, row - 1);
}
}
}
// Called while dragging an element
// Moves the entire column according to given delta (+/- 1)
public void MoveColumn(int targetColumn, float delta)
{
for (var column = 0; column < 3; column++)
{
for (var row = 0; row < 3; row++)
{
_grid[column, row].transform.position = new Vector3(column - 1, 0, row - 1 + (column == targetColumn ? delta : 0));
}
}
}
}
And then accordingly have a GridElement component on each grid element to handle the dragging and route the movement through the Grid
public class GridElement : MonoBehaviour, IBeginDragHandler, IEndDragHandler, IDragHandler
{
// on indices within the grid so we can forward thm to method calls later
private int _currentRow;
private int _currentColumn;
// a mathematical XZ plane we will use for the dragging input
// you could as well just use physics raycasts
// but for now just wanted to keep it simple
private static Plane _dragPlane = new Plane(Vector3.up, 1f);
// reference to the grid to forward invoke methods
private Grid _grid;
// the world position where the current draggin was started
private Vector3 _startDragPoint;
// camera used to convert screenspace mouse position to ray
[SerializeField]
private Camera _camera;
public void Initialize(Grid grid)
{
// assign the camera
if(!_camera)_camera = Camera.main;
// store the grid reference to later forward the input calls
_grid = grid;
}
// plain set the indices to the new values
public void UpdateIndices(int currentRow, int currentColumn)
{
_currentRow = currentRow;
_currentColumn = currentColumn;
}
// called by the EventSystem when starting to drag this object
public void OnBeginDrag(PointerEventData eventData)
{
// get a ray for the current mouse position
var ray = _camera.ScreenPointToRay(eventData.position);
// shoot a raycast against the mathemtical XZ plane
// You could as well use Physics.Raycast and get the exact hit point on the collider etc
// but this should be close enough especially in top-down views
if (_dragPlane.Raycast(ray, out var distance))
{
// store the world space position of the cursor hit point
_startDragPoint = ray.GetPoint(distance);
}
}
// Called by the EventSystem while dragging this object
public void OnDrag(PointerEventData eventData)
{
var ray = _camera.ScreenPointToRay(eventData.position);
if (_dragPlane.Raycast(ray, out var distance))
{
// get the dragged delta against the start position
var currentDragPoint = ray.GetPoint(distance);
var delta = currentDragPoint - _startDragPoint;
// we either only drag vertically or horizontally
if (Mathf.Abs(delta.x) > Mathf.Abs(delta.z))
{
// clamp the delta between -1 and 1
delta.x = Mathf.Clamp(delta.x, -1f, 1f);
// and tell the grid to move this entire row
_grid.MoveRow(_currentRow, delta.x);
}
else
{
delta.z = Mathf.Clamp(delta.z, -1f, 1f);
// accordingly tell the grid to move this entire column
_grid.MoveColumn(_currentColumn,delta.z);
}
}
}
// Called by the EventSystem when stop dragging this object
public void OnEndDrag(PointerEventData eventData)
{
var ray = _camera.ScreenPointToRay(eventData.position);
if (_dragPlane.Raycast(ray, out var distance))
{
// as before get the final delta
var currentDragPoint = ray.GetPoint(distance);
var delta = currentDragPoint - _startDragPoint;
// Check against a threashold - if simply went with more then the half of one step
// and shift the grid into the according direction
if (delta.x > 0.5f)
{
_grid.ShiftRowRight(_currentRow);
}else if (delta.x < -0.5f)
{
_grid.ShiftRowLeft(_currentRow);
}
else if (delta.z > 0.5f)
{
_grid.ShiftColumnUp(_currentColumn);
}
else if(delta.z < -0.5f)
{
_grid.ShiftColumnDown(_currentColumn);
}
else
{
// if no direction matched at all just make sure to reset the positions
_grid.RefreshIndices();
}
}
}
}
Finally in order to make this setup work you will need
an EventSystem component anywhere in your scene
a PhysicsRaycaster component on your Camera
Little demo
I'm working on a connect 4 game and I've got the following part done:
I've made the board which is able to display tiles of the board in 3 colors (white, red and yellow). I've also written some code to display a box around the column on which the user is hovering such that he can see where he is placing a dish.
I've also created the code which allows a dish to be added to the board. Both of these processes use the following code:
//Calculate the size of a single cell
int cellX = this.Size.Width / this.Columns;
//calculate the cell which was clicked
int nColumn = (int)Math.Floor((Double)x / cellX);
x is the value of MouseEventArgs.X of the pannel on which this is called. For drawing a boundry box this code works perfectly but for dropping a dish it doesn't. sometimes it drops 1 left of where I want it sometimes one right.
Here is the code for both events:
//draws the bounding box around the column for a given x value.
public void drawBoundingBox(int x)
{
//Calculate the size of a single cell
int cellX = this.Size.Width / this.Columns;
//calculate the cell which was clicked
int nColumn = (int)Math.Floor((Double)x / cellX);
if (nColumn != this.lastColumn)
{
this.Refresh();
Graphics g = CreateGraphics();
Pen pRed = new Pen(Color.Red, 3);
if (nColumn < this.Columns)
{
Rectangle rect = new Rectangle(new Point(nColumn * cellX, 0),
new Size(cellX, this.Size.Height));
g.DrawRectangle(pRed, rect);
}
}
this.lastColumn = nColumn;
}
public Boolean move(int mousePosition) {
int cellX = this.Size.Width / this.Columns;
int nColumn = (int)Math.Floor((Double)mousePosition / cellX);
return board.Move(nColumn);
}
Here is the code for board.move():
public bool Move(int x)
{
bool found = false;
for (int i = Rows - 1; i >= 0 && !found; i--)
{
Console.WriteLine("X equals: " + x + " i equals: " + i);
if (States[i,x] == 0) {
found = true;
States[i, x] = (byte)(((moves++) % 2) + 1);
}
}
return found;
}
Here is a gif showing what I mean:
To me it seams like a rounding error but it's wierd to me that the bounding box works well with the mouse but the clicking action doesn't...
I want to replace the destroyed hexagons with other standing hexagons. Existing hexagons should fall from the top. For example if I destroy (0,2) positioned hexagon in the picture below, the top left hexagon which position of that hexagon is (0,0) should be moved to (0,2) position and i should create a new hexagon and put it on (0,0) which is empty now because we moved the hexagon on (0,0) to (0,2) earlier.
I have a two dimensional array that stores all the references of hexagons with an index of the hexagon's coordinate (x,y).
--IMPORTANT--
Moving the objects is not important. The important part is we have to know which hexagon will be replaced with another. We have to tell the ARRAY that we changed those hexagons and the hexagons that were just moved or created should have exactly one reference in the index of their new (x,y) positions.
VIDEO FOR BETTER EXPLAINING WHAT I WANT TO DO
https://www.youtube.com/watch?v=QYhq0qwFmmY
Any ideas or help would be appreciated!
Hexagon Coordinate system (ignore red arrows)
public void CreateGrid(int gridWidth, int gridHeight)
{
for (int y = 0; y < gridHeight; y++)
{
for (int x = 0; x < gridWidth; x++)
{
GameObject Hexagon = Instantiate(HexagonPre, Vector2.zero, Quaternion.identity, HexGrid);
int RandColor = Random.Range(0, 5);
if (RandColor == 0)
{
Hexagon.GetComponent<SpriteRenderer>().color = Color.blue;
}
else if (RandColor == 1)
{
Hexagon.GetComponent<SpriteRenderer>().color = Color.red;
}
else if (RandColor == 2)
{
Hexagon.GetComponent<SpriteRenderer>().color = Color.green;
}
else if (RandColor == 3)
{
Hexagon.GetComponent<SpriteRenderer>().color = Color.yellow;
}
else if (RandColor == 4)
{
Hexagon.GetComponent<SpriteRenderer>().color = Color.cyan;
}
Vector2 gridPos = new Vector2(x, y);
Hexagon.transform.position = CalcWorldPos(gridPos);
Hexagon.GetComponent<HexCoordinates>().Coordinates = new Vector2Int(x, y);
Hexagon.transform.name = "X: " + x + " | Y: " + y;
}
}
}
Code for destroying hexagons
if (MatchedColors == 2)
{
if(!HexToBeDestroyed.Contains(Hexagons[x, y].gameObject))
HexToBeDestroyed.Add(Hexagons[x, y].gameObject);
if (!HexToBeDestroyed.Contains(Hexagons[x - 1, y].gameObject))
HexToBeDestroyed.Add(Hexagons[x - 1, y].gameObject);
if (!HexToBeDestroyed.Contains(Hexagons[x - 1, y - 1].gameObject))
HexToBeDestroyed.Add(Hexagons[x - 1, y - 1].gameObject);
}
MatchedColors = 0;
}
}
}
}
foreach (GameObject G in HexToBeDestroyed)
{
if (G != null)
{
Destroy(G.gameObject);
}
}
Explanation for code is in comments:
void HexagonFall(GameObject[,] hexArray)
{
// Handle fall for base columns and for offset columns
for (int offset = 0 ; offset < 2 ; offset++)
{
// Handle fall for each column at current offset
for (int x = 0 ; x < hexArray.GetLength(0) ; x++)
{
int bottomYIndex = hexArray.GetLength(1) - offset - 1;
// List of indices of where each hexagon in that column will come from.
// We will fill from bottom to top.
List<Vector2Int> sourceIndices = new List<Vector2Int>();
for (int y = bottomYIndex ; y >= 0 ; y-=2)
{
// HexExists returns true if the hex isn't empty.
// Something along the lines of ` return input!=null; `
// depending on what "empty" hexes look like in the array
if (HexExists(hexArray[x,y]))
{
sourceIndices.Add(new Vector2Int(x,y));
}
}
// We have a list of where to get each bottom hexes from, now do the move/create
for (int y = bottomYIndex; y >= 0 ; y-=2)
{
if (sourceIndices.Count > 0)
{
// If we have any available hexes in column,
// use the bottommost one (at index 0)
hexArray[x,y] = hexArray[sourceIndices[0].x, sourceIndices[0].y];
// We have now found a home for hex previously at sourceIndices[0].
// Remove that index from list so hex will stay put.
sourceIndices.RemoveAt(0);
}
else
{
// Otherwise, we need to generate a new hex
hexArray[x,y] = MakeNewHexAt(new Vector2Int(x,y));
}
// Tell the hex about its new home
hexArray[x,y].GetComponent<HexCoordinates>().Coordinates = new Vector2Int(x, y);
hexArray[x,y].transform.name = "X: " + x + " | Y: " + y;
}
}
}
}
In your hex destroying code, I would change HexToBeDestroyed to be a List of Vector2Int so you can set the array references to null immediately when you Destroy the gameobject:
List<Vector2Int> HexToBeDestroyed = new List<Vector2Int>();
// ...
if (MatchedColors == 2)
{
if(!HexToBeDestroyed.Contains(new Vector2Int(x, y))
HexToBeDestroyed.Add(new Vector2Int(x, y));
if (!HexToBeDestroyed.Contains(new Vector2Int(x - 1, y))
HexToBeDestroyed.Add(new Vector2Int(x - 1, y));
if (!HexToBeDestroyed.Contains(new Vector2Int(x - 1, y - 1)))
HexToBeDestroyed.Add(new Vector2Int(x - 1, y - 1));
}
// ...
foreach (Vector2Int V in HexToBeDestroyed)
{
if (Hexagons[V.x,V.y] != null)
{
Destroy(Hexagons[V.x,V.y]);
Hexagons[V.x,V.y] = null;
}
}
As far as moving the hexes goes, I would add this in the Update of HexCoordinates:
float fallSpeed = 0.5f;
Vector2 goalWorldPosition = GS.CalcWorldPos(Coordinates);
transform.position = Vector2.MoveTowards(transform.position, goalWorldPosition, fallSpeed * Time.deltaTime);
I'm using Visual Studio 2010, including reference Dynamic Data Display map.
I'm drawing on the map polygon by drawing DragglePoints and Segments between the points.
I found a code that find if point(x,y) is in polygon, but on a map it doesnt work.
pt = e.GetPosition(this.plotter.CentralGrid); // Get the mouse position
ps = this.plotter.Viewport.Transform.ScreenToViewport(pt); // Transform the mouse positon to Screen on chartplotter
// Now ps is the point converting to the map point - it works perfect
// I'm using it for another things.(like Painting a dragglepoint or something else.
for (int k = 0; k <= listPolygons.Count - 1; k++)
{
bool ifInside = PointInPolygon(new Point(ps.X, double.Parse(this.plotter.Viewport.Transform.DataTransform.ViewportToData(ps).Y.ToString())), listPolygons[k]); // Sending to the functing pointInPolygon the point and list of polygons we have
if (ifInside)
{
listPolygons[k].removePolygon(listPolygons[k], plotter);
listPolygons.RemoveAt(k);
break;
}
}
and the function PointInPolygon:
private bool PointInPolygon(Point point, Polygon polygon)
{
List<DraggablePoint> points = polygon.getListPoints();
DraggablePoint pointClicked = new DraggablePoint(new Point(point.X, point.Y));
int i, j, nvert = polygon.getNumberOfVertx();
bool c = false;
for(i = 0, j = nvert - 1; i < nvert; j = i++)
{
if (((points[i].Position.Y) >= pointClicked.Position.Y) != (points[j].Position.Y >= pointClicked.Position.Y) &&
(pointClicked.Position.X <= (points[j].Position.X - points[i].Position.X) * (pointClicked.Position.Y - points[i].Position.Y) / (points[j].Position.Y - points[i].Position.Y) + points[i].Position.X))
c = !c;
}
return c;
}
Here c always returns as false, whether I click inside a polygon or outside.
points[i].Position.Y and points[i].Position.X and pointClicked.Position.Y and pointClicked.Position.X is given a perfect variables - on the map values.