I am trying to make a simple tile game engine and running into issues. I get stuck when i have to redraw the tile.
int[,] level = {
{ 0, 0, 0, 0, 0 ,0 },
{ 0, 0, 0, 0, 0 ,0 },
{ 0, 0, 0, 0, 0 ,0 },
{ 0, 0, 0, 0, 0 ,0 },
{ 0, 0, 0, 0, 0 ,0 },
{ 0, 0, 0, 0, 0 ,0 },
};
This is my array and all the values are 0 thus off. Each corresponding value is linked to a seperate tile that will turn on and off as you press the keys.
//Event Handler (W,A,S,D) is used for movements
private void panel1_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e)
{
.
.
.
}
//Responsible for movements
private void tmrMov_Tick(object sender, EventArgs e)
{
level[_x, _y] = 0;
if (_objDirection == Direction.Right)
{
if (_x < _boardWidth - 1 && _x >= 0 && _x + 1 < _boardWidth - 1 && _x + 1 >= 0)
_x += 1;
else
_x = _boardWidth - 1;
}
.
.
.
level[_x, _y] = _k;
Invalidate();
}
This is my timer function that is supposed to 'manipulate' array values so when the program is running, one can decide which tile to turn on/off through keys.
Anyways, my problem is refreshing the image which concerns the 'invalidate()' function call. Though, i do have a feeling it can change array values on the fly, i can't seem to refresh the image to jump to another tile.
Here's the link to the complete project i've been working on: http://www.mediafire.com/?g10a0zzt8hru11v
Here's a similar but different question i asked some days back: Setting up basic game and debugging fundamental problems
Thanks in advance!
So...I hope you don't take any offense to this, but I went through and tidied things up a bit and offered some suggestions/fixes/comments/etc.
Hope this helps! Let me know if any of the suggested changes need explanation.
So, let's start at the designer.cs file:
// Ahh, here's yer problem: the panel control doesn't raise PreviewKeyDowns - the form does, tho
this.panel1.PreviewKeyDown +=
new System.Windows.Forms.PreviewKeyDownEventHandler(
this.panel1_PreviewKeyDown);
// the form will raise these, so bind the handler to it
// (and rename the method)
this.PreviewKeyDown +=
new System.Windows.Forms.PreviewKeyDownEventHandler(
this.Form1_PreviewKeyDown);
Ah, one big problem down - none of your key events were actually getting to your handler. Let's pop back over to the form code-behind.
So, in the constructor - all this can go:
// arrays of T (T[], int[], etc), come initialized to default(T),
// so none of this is needed.
level[0, 0] = 0;
level[0, 1] = 0;
level[0, 2] = 0;
level[0, 3] = 0;
level[0, 4] = 0;
Jump down to the Paint handler:
// The next two checks have the x and y swapped,
// meaning the apparent motion will not match what the
// actual direction should be
//Empty Tile
if (level[y, x] == 0)
{
//Occupied Tile
if (level[y, x] == 1)
{
// Now the render will mactch properly
//Empty Tile
if (level[x, y] == 0)
{
//Occupied Tile
if (level[x, y] == 1)
{
Onward! To the Timer.Tick handler:
#region Timer function
// doing this could cause flickering
// or flashing if the paint fires while
// we're updating things
level[_x, _y] = 0;
#region Timer function
// instead, keep track temporarily
// what they were - we'll come back to this later on
var oldX = _x;
var oldY = _y;
Further on to the if/else chains:
// There's a lot of replication and style choices here that
// will make it harder to debug/troubleshoot
if (_objDirection == Direction.Right)
{
if (_x < _boardWidth - 1 && _x >= 0 && _x + 1 < _boardWidth - 1 && _x + 1 >= 0)
_x += 1;
else
_x = _boardWidth - 1;
}
else if (_objDirection == Direction.Left)
Let's see if we can get rid of some of the repetition:
// let's figure these out ahead of time
var spaceOnLeft = _x > 0;
var spaceOnRight = _x < _boardWidth - 1;
var spaceOnTop = _y > 0;
var spaceOnBottom = _y < _boardHeight - 1;
// switch is a bit like the if/else construct you had
switch (_objDirection)
{
case Direction.Up:
// this means: if(spaceOnTop) y = y-1 else y = height-1
_y = spaceOnTop ? _y - 1 : _boardHeight - 1;
break;
case Direction.Down:
_y = spaceOnBottom ? _y + 1 : 0;
break;
case Direction.Left:
_x = spaceOnLeft ? _x - 1 : _boardWidth - 1;
break;
case Direction.Right:
_x = spaceOnRight ? _x + 1 : 0;
break;
}
Skip to the end...
// now we'll use the old position to clear...
level[oldX, oldY] = 0;
// then set the new position
level[_x, _y] = _k;
// Since we're only writing on the panel,
// we only need to rerender the panel
panel1.Refresh();
One last bit - the key down handler:
// Hah - artificial difficulty due
// to awkward key choice? Out of curiosity,
// why not Keys.Up, Down, Left, Right?
if (e.KeyCode == Keys.E)
{
_objDirection = Direction.Left;
}
else if (e.KeyCode == Keys.D)
{
_objDirection = Direction.Right;
}
else if (e.KeyCode == Keys.W)
{
_objDirection = Direction.Up;
}
else if (e.KeyCode == Keys.S)
{
_objDirection = Direction.Down;
}
// same deal here, but with keys
// Or switch to Up, Down, Left, Right :)
switch (e.KeyCode)
{
case Keys.E:
_objDirection = Direction.Up;
break;
case Keys.D:
_objDirection = Direction.Down;
break;
case Keys.W:
_objDirection = Direction.Left;
break;
case Keys.S:
_objDirection = Direction.Right;
break;
}
Full code drop of the Form1.cs class:
//Listing all the parameters
public partial class Form1 : Form
{
#region Declaring Parameters
enum Direction
{
Left, Right, Up, Down
}
private int _x;
private int _y;
private int _k;
private Direction _objDirection;
Random rand = new Random();
private int _boardWidth;
private int _boardHeight;
private int[,] level;
#endregion
//Giving values to parameters
public Form1()
{
InitializeComponent();
#region Initialial values
_k = 1;
_boardWidth = 6;
_boardHeight = 6;
_x = rand.Next(0, _boardWidth - 1);
_y = rand.Next(0, _boardHeight - 1);
_objDirection = Direction.Left;
//Array that works as a board or platform which we used to distinguish tiles
level = new int[_boardWidth, _boardHeight];
#endregion
}
//Paint is used for drawing purposes only
private void panel1_Paint(object sender, PaintEventArgs e)
{
/*
int[,] level = {
{ 0, 0, 0, 0, 0 ,0 },
{ 0, 0, 0, 0, 0 ,0 },
{ 0, 0, 0, 0, 0 ,0 },
{ 0, 0, 0, 0, 0 ,0 },
{ 0, 0, 0, 0, 0 ,0 },
{ 0, 0, 0, 0, 0 ,0 },
};
*/
#region Looping through tiles
//Initializing first randomly filled tile
level[_x, _y] = _k;
for (int y = 0; y < _boardHeight; y++)
{
for (int x = 0; x < _boardWidth; x++)
{
//Empty Tile
if (level[x, y] == 0)
{
// Create pen.
Pen redPen = new Pen(Color.Red, 1);
// Create rectangle.
Rectangle redRect = new Rectangle(x * 50, y * 50, 50, 50);
// Draw rectangle to screen.
e.Graphics.DrawRectangle(redPen, redRect);
}
//Occupied Tile
if (level[x, y] == 1)
{
// Create solid brush.
SolidBrush blueBrush = new SolidBrush(Color.Blue);
// Create rectangle.
Rectangle rect = new Rectangle(x * 50, y * 50, 50, 50);
// Fill rectangle to screen.
e.Graphics.FillRectangle(blueBrush, rect);
}
}
}
#endregion
}
//Responsible for movements
private void tmrMov_Tick(object sender, EventArgs e)
{
#region Timer function
// instead, keep track temporarily
// what they were
var oldX = _x;
var oldY = _y;
// let's figure these out ahead of time
var spaceOnLeft = _x > 0;
var spaceOnRight = _x < _boardWidth - 1;
var spaceOnTop = _y > 0;
var spaceOnBottom = _y < _boardHeight - 1;
// switch is a bit like the if/else construct you had
switch (_objDirection)
{
case Direction.Up:
// this means: if(spaceOnTop) y = y-1 else y = height-1
_y = spaceOnTop ? _y - 1 : _boardHeight - 1;
break;
case Direction.Down:
_y = spaceOnBottom ? _y + 1 : 0;
break;
case Direction.Left:
_x = spaceOnLeft ? _x - 1 : _boardWidth - 1;
break;
case Direction.Right:
_x = spaceOnRight ? _x + 1 : 0;
break;
}
// now we'll use the old values to clear...
level[oldX, oldY] = 0;
// then set the new value
level[_x, _y] = _k;
#endregion
panel1.Refresh();
}
//Event Handler (W,A,S,D) is used for movements
private void Form1_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e)
{
#region Controls
// same deal here, but with keys
switch (e.KeyCode)
{
case Keys.Up:
e.IsInputKey = true;
_objDirection = Direction.Up;
break;
case Keys.Down:
e.IsInputKey = true;
_objDirection = Direction.Down;
break;
case Keys.Left:
e.IsInputKey = true;
_objDirection = Direction.Left;
break;
case Keys.Right:
e.IsInputKey = true;
_objDirection = Direction.Right;
break;
}
#endregion
}
}
Cheers!
Related
I'm making a slideshow image and the direction of images is going left.
Now my problem is how can I make it an infinite loop? Once the last image appears, I want to show again the first image up to the last image again. How can I make this? Heres my code:
PictureBox[] clouds = new PictureBox[4];
public Form2()
{
InitializeComponent();
}
private void Form2_Load(object sender, EventArgs e)
{
speed = 3;
clouds[0] = pictureBox1;
clouds[1] = pictureBox2;
clouds[2] = pictureBox3;
clouds[3] = pictureBox4;
}
private void timer1_Tick(object sender, EventArgs e)
{
for (int x = 0; x < 4; x++)
{
clouds[x].Left -= 10;
if (clouds[x].Left == 0)
{
clouds[x].Left = +this.Width;
}
}
}
If I understand you right, you want
0, 1, 2, 3, 0, 1, 2, 3, 0, ...
sequence. It can be implemented as
// x = 0 - start with x = 0
// - no condition (infinte loop)
// x = (x + 1) %4 - modulo arithemetics: 0, 1, 2, 3, 0, 1, 2, 3, 0 ....
for (int x = 0; ; x = (x + 1) % 4) {
...
}
In general case for clouds[] array we can get rid of magic number 4
for (int x = 0; ; x = (x + 1) % array.Length) {
...
}
My goal is to prevent partial or total hidden of my form when user move it through mouse.
For instance, if my desktop resolution is 1024x768 the following will be "x/y" minimum/maximum value of coordinates for form location properties: 0, 1024 - form.width, 0, 768 - form.height.
I use some native APi to accomplish all this and all works fine, but often I notice a bad refresh (flickering) of my form (I'm managing all on form_move event).
It seems like SuspendLayout method doesn't works fine, or else the form move event doesn't fire every pixel changed, but maybe more than one (see the below code to realize what I mean).
My code looks like this:
private void Form1_Move(object sender, EventArgs e)
{
this.SuspendLayout();
// Desktop Resolution (API utility)
int x = Desktop.GetXResolution();
int y = Desktop.GetYResolution();
// Taskbar Info (API utility)
Taskbar tb = new Taskbar();
int minX = 0;
int maxX = x - this.Width;
int minY = 0;
int maxY = y - this.Height;
if (!tb.AutoHide)
{
if (tb.Position != TaskbarPosition.Unknown && !tb.Size.IsEmpty)
{
if (tb.Position == TaskbarPosition.Top) minY += tb.Size.Height;
switch (tb.Position)
{
case TaskbarPosition.Top: minY = tb.Size.Height; break;
case TaskbarPosition.Bottom: maxY -= tb.Size.Height; break;
case TaskbarPosition.Left: minX = tb.Size.Width; break;
case TaskbarPosition.Right: maxX -= tb.Size.Width; break;
}
}
}
// Restore X Position
if (this.Location.X < minX) this.Location = new Point(minX, this.Location.Y);
if (this.Location.X > maxX) this.Location = new Point(maxX, this.Location.Y);
// Restore Y Poistion
if (this.Location.Y < minY) this.Location = new Point(this.Location.X, minY);
if (this.Location.Y > maxY) this.Location = new Point(this.Location.X, maxY);
this.ResumeLayout(false);
}
As I already said, often when I restore Location property my winForm take a flickering.
Any suggestion will be appreciated.
Move occurs after a move has completed. The form has moved and then you move it back. It's going to flicker anyway.
Instead, prevent it from moving in the first place:
private const int WM_MOVING = 0x216;
private void WriteTheRect(IntPtr dest, Rectangle rect) {
System.Runtime.InteropServices.Marshal.WriteInt32(dest, 0, rect.Left);
System.Runtime.InteropServices.Marshal.WriteInt32(dest, 4, rect.Top);
System.Runtime.InteropServices.Marshal.WriteInt32(dest, 8, rect.Right);
System.Runtime.InteropServices.Marshal.WriteInt32(dest, 12, rect.Bottom);
}
protected override void WndProc(ref Message m) {
if (m.Msg == WM_MOVING)
{
// RECT structure pointed to by lParam: left, top, right, bottom
Rectangle r = Rectangle.FromLTRB(System.Runtime.InteropServices.Marshal.ReadInt32(m.LParam, 0),
System.Runtime.InteropServices.Marshal.ReadInt32(m.LParam, 4),
System.Runtime.InteropServices.Marshal.ReadInt32(m.LParam, 8),
System.Runtime.InteropServices.Marshal.ReadInt32(m.LParam, 12)
);
Rectangle allowed = Rectangle.FromLTRB(0, 0, 1600, 900);
if (r.Left <= allowed.Left || r.Top <= allowed.Top || r.Right >= allowed.Right || r.Bottom >= allowed.Bottom)
{
int offset_x = r.Left < allowed.Left ? (allowed.Left - r.Left) : (r.Right > allowed.Right ? (allowed.Right - r.Right) : (0));
int offset_y = r.Top < allowed.Top ? (allowed.Top - r.Top) : (r.Bottom > allowed.Bottom ? (allowed.Bottom - r.Bottom) : (0));
r.Offset(offset_x, offset_y);
WriteTheRect(m.LParam, r);
}
}
base.WndProc(ref m);
}
Obviously, replace the constants with your actual desired bounds.
I have this code that draws the boxes on a Qbert board, how would i figure out how to detect what color blocks are stepped on?
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
namespace QBert
{
public class Map
{
public int[,] board;
public Color[] blockColors = new Color[] { Color.Blue, Color.Green }; //this makes it so it takes one time to step to get to green
Texture2D block;
public Map(Texture2D block) //draws the map of the blocks
{
this.block = block;
board = new int[8, 7]
{
{ 0, 0, 0, 1, 0, 0, 0 },
{ 0, 0, 1, 1, 0, 0, 0 },
{ 0, 0, 1, 1, 1, 0, 0 },
{ 0, 1, 1, 1, 1, 0, 0 },
{ 0, 1, 1, 1, 1, 1, 0 },
{ 1, 1, 1, 1, 1, 1, 0 },
{ 1, 1, 1, 1, 1, 1, 1 },
{ -1, -1, -1, -1, -1, -1, -1 },
}
;
}
public Vector2 GetSquareCoords(int x, int y) //cordinates of the block
{
int ofs = block.Width / 2;
ofs *= y % 2;
return new Vector2(x * block.Width + ofs, y * 96); // 96
}
public Vector2 GetSquareCenter(int x, int y) //method for to jump on the middle of a block
{
Vector2 coords = GetSquareCoords(x, y);
return new Vector2(coords.X + block.Width / 2, coords.Y + 32); //32
}
public Vector2 GetNextSquare(bool down, bool left, Vector2 position) //this is how you jump to a next square
{
// If on even row, right is directly below and left is below and to the left
// If on odd row, left is directly below and right is below and to the right
int next_x = 0, next_y = 0;
int x = (int)position.X;
int y = (int)position.Y;
if (down)
{
next_y = y + 1;
if (left)
{
next_x = x - 1; // -1
}
else
{
next_x = x;
}
}
else
{
next_y = y - 1;
}
if (y % 2 == 0)
{
if (left)
next_x = x - 1;
else
next_x = x;
}
else
{
if (left)
next_x = x;
else
next_x = x + 1; //+1
}
if (next_x < 0)
{
next_x += 1;
}
if (next_x > 6)
{
next_x -= 1;
}
if (next_y < 0)
{
next_y += 1;
}
if (next_y > 7)
{
next_y -= 1;
}
if (board[next_y, next_x] == 0)
{
return new Vector2(x, y);
}
else
{
return new Vector2(next_x, next_y);
}
}
public void Draw(SpriteBatch spriteBatch) //draws the blocks and colors of the block
{
int drawXOffset = 30;
int drawYOffset = 60;
for (int x = 0; x < 7; x++)
for (int y = 0; y < 7; y++)
{
Vector2 coord = GetSquareCoords(x, y);
if (board[y, x] > 0)
spriteBatch.Draw(block, new Rectangle(drawXOffset + (int)coord.X, drawYOffset + (int)coord.Y, block.Width, block.Height), blockColors[board[y, x] - 1]);
}
}
}
}
I am trying to have the code detect the number of blocks drawn so that I know when they are all a certain color.
I need to make it a certain color of a block to end the game.
Right now, i have it starting out as a Blue Block Color then changing to a Green Block, how would i make it detect that if all the green blocks are stepped on that the game ends?
Somewhere in your Update method, you will want something like this:
bool finished = true;
for (int x = 0; x < 7; x++)
{
for (int y = 0; y < 7; y++)
{
if (board != 0 && board != 2) // 2 is green
{
finished = true;
break;
}
}
if (finished)
break;
}
if (finished)
{
// Move to next level
}
I think somthing like this is what you want
public Vector2 GetNextSquare(bool down, bool left, Vector2 position)
{
int x = (int)position.X;
int y = (int)position.Y;
//...other code
if (blockColors[board[next_y, next_x]] == Color.Green)
{
//End
}
else if (board[next_y, next_x] == 0)
{
return new Vector2(x, y);
}
else
{
return new Vector2(next_x, next_y);
}
}
Usually you have some sort of data representing your game field and rendering code simply renders visual representation of the field. Your game code only works with internal field representation (i.e. in your case set of cubes objects with "Color" property).
You definitely can check color on the screen, but it will require significantly more effort.
#Jaview "what do you mean, can you show me an example how i can check?"
Here is an example how to get a pixel in XNA:
ResolveTexture2D backBufferData;
backBufferData = new ResolveTexture2D(
graphics.GraphicsDevice,
graphics.GraphicsDevice.PresentationParameters.BackBufferWidth,
graphics.GraphicsDevice.PresentationParameters.BackBufferHeight,
1,
graphics.GraphicsDevice.PresentationParameters.BackBufferFormat
);
Rectangle sourceRectangle = new Rectangle(Mouse.GetState().X, Mouse.GetState().Y, 1, 1);
Color[] retrievedColor = new Color[1];
graphics.GraphicsDevice.ResolveBackBuffer(backBufferData);
backBufferData.GetData<Color>(
0,
sourceRectangle,
retrievedColor,
0,
1);
selectedColor = retrievedColor[0];
It's difficult to tell what is being asked here. This question is ambiguous, vague, incomplete, overly broad, or rhetorical and cannot be reasonably answered in its current form. For help clarifying this question so that it can be reopened, visit the help center.
Closed 11 years ago.
I have translated the code from javascript to c# which can be found by going to this excellent demo at http://fractal.qfox.nl/dragon.js
My translation is intended to produce just a single dragon upon clicking the button but I think I have missed something in my version.
See the Wikipedia article: Dragon Curve for more information.
Incomplete dragon fractal output:
Code:
public partial class MainPage : UserControl
{
PointCollection pc;
Int32[] pattern = new Int32[] { 1, 1, 0, 2, 1, 0, 0, 3 };
Int32[] position = new Int32[] { 0, 0, 0, 0, 0, 0, 0, 0 };
Boolean toggle;
Char r = default(Char);
Int32 distance = 10; // line length
Int32 step = 100; // paints per step
Int32 skip = 10; // folds per paint
Double x = 0;
Double y = 0;
Int32 a = 90;
public MainPage()
{
InitializeComponent();
}
private void btnFire_Click(object sender, RoutedEventArgs e)
{
x = canvas.ActualWidth / 3;
y = canvas.ActualHeight / 1.5;
pc = new PointCollection();
var n = step;
while (--n > 0)
{
List<Char> s = getS(skip);
draw(s);
}
Polyline p = new Polyline();
p.Stroke = new SolidColorBrush(Colors.Red);
p.StrokeThickness = 0.5;
p.Points = pc;
canvas.Children.Add(p);
}
List<Char> getS(Int32 n)
{
List<Char> s1 = new List<Char>();
while (n-- > 0) s1.Add(getNext(0));
return s1;
}
void draw(List<Char> s)
{
pc.Add(new Point(x, y));
for (Int32 i = 0, n = s.Count; i < n; i++)
{
pc.Add(new Point(x, y));
Int32 j;
if (int.TryParse(s[i].ToString(), out j) && j != 0)
{
if ((a + 90) % 360 != 0)
{
a = (a + 90) % 360;
}
else
{
a = 360; // Right
}
}
else
{
if (a - 90 != 0)
{
a = a - 90;
}
else
{
a = 360; // Right
}
}
// new target
if (a == 0 || a == 360)
{
y -= distance;
}
else if (a == 90)
{
x += distance;
}
else if (a == 180)
{
y += distance;
}
else if (a == 270)
{
x -= distance;
}
// move
pc.Add(new Point(x, y));
}
}
Char getNext(Int32 n)
{
if (position[n] == 7)
{
r = getNext(n + 1);
position[n] = 0;
}
else
{
var x = position[n] > 0 ? pattern[position[n]] : pattern[0];
switch (x)
{
case 0:
r = '0';
break;
case 1:
r = '1';
break;
case 2:
if (!toggle)
{
r = '1';
}
else
{
r = '0';
}
toggle = !toggle;
break;
}
position[n] = position[n] + 1;
}
return r;
}
}
I cleaned up the code, and tried to get how the pattern and position arrays should work to produce the correct sequence, but I couldn't figure it out. The last item in the pattern array is for example never used...
There is however a simpler method implementing the getNext method using just a counter:
bool getNext() {
cnt++;
return (cnt & ((cnt & -cnt) << 1)) != 0;
}
I have used that method before (about 20 years ago), and I found this implementation on the dragon curve wikipedia page.
The cleaned up code with this getNext implementation looks like this:
public partial class MainPage : UserControl {
PointCollection pc;
int cnt = 0;
int distance = 10; // line length
int steps = 1024; // number of paints
int x = 0;
int y = 0;
int a = 90;
public MainPage() {
InitializeComponent();
}
private void btnFire_Click(object sender, RoutedEventArgs e) {
x = (int)(canvas.ActualWidth / 3);
y = (int)(canvas.ActualHeight / 1.5);
pc = new PointCollection();
draw(getS(steps));
Polyline p = new Polyline();
p.Stroke = new SolidColorBrush(Colors.Red);
p.StrokeThickness = 0.5;
p.Points = pc;
canvas.Children.Add(p);
}
List<bool> getS(int n) {
List<bool> s1 = new List<bool>();
while (n-- > 0) {
s1.Add(getNext());
}
return s1;
}
void draw(List<bool> s) {
pc.Add(new Point(x, y));
foreach (bool z in s) {
a = (a + (z ? 90 : 270)) % 360;
// new target
switch (a) {
case 90: x += distance; break;
case 180: y += distance; break;
case 270: x -= distance; break;
default: y -= distance; break;
}
// move
pc.Add(new Point(x, y));
}
}
bool getNext() {
cnt++;
return (cnt & ((cnt & -cnt) << 1)) != 0;
}
}
I'm attempting to add semi-realistic water into my tile-based, 2D platformer. The water must act somewhat lifelike, with a pressure model that runs entirely local. (IE. Can only use data from cells near it) This model is needed because of the nature of my game, where you cannot be certain that the data you need isn't inside an area that isn't in memory.
I've tried one method so far, but I could not refine it enough to work with my constraints.
For that model, each cell would be slightly compressible, depending on the amount of water in the above cell. When a cell's water content was larger than the normal capacity, the cell would try to expand upwards. This created a fairly nice simulation, abeit slow (Not lag; Changes in the water were taking a while to propagate.), at times. When I tried to implement this into my engine, I found that my limitations lacked the precision required for it to work. I can provide a more indepth explanation or a link to the original concept if you wish.
My constraints:
Only 256 discrete values for water level. (No floating point variables :( ) -- EDIT. Floats are fine.
Fixed grid size.
2D Only.
U-Bend Configurations must work.
The language that I'm using is C#, but I can probably take other languages and translate it to C#.
The question is, can anyone give me a pressure model for water, following my constraints as closely as possible?
How about a different approach?
Forget about floats, that's asking for roundoff problems in the long run. Instead, how about a unit of water?
Each cell contains a certain number of units of water. Each iteration you compare the cell with it's 4 neighbors and move say 10% (change this to alter the propagation speed) of the difference in the number of units of water. A mapping function translates the units of water into a water level.
To avoid calculation order problems use two values, one for the old units, one for the new. Calculate everything and then copy the updated values back. 2 ints = 8 bytes per cell. If you have a million cells that's still only 8mb.
If you are actually trying to simulate waves you'll need to also store the flow--4 values, 16 mb. To make a wave put some inertia to the flow--after you calculate the desired flow then move the previous flow say 10% of the way towards the desired value.
Try treating each contiguous area of water as a single area (like flood fill) and track 1) the lowest cell(s) where water can escape and 2) the highest cell(s) from which water can come, then move water from the top to the bottom. This isn't local, but I think you can treat the edges of the area you want to affect as not connected and process any subset that you want. Re-evaluate what areas are contiguous on each frame (re-flood on each frame) so that when blobs converge, they can start being treated as one.
Here's my code from a Windows Forms demo of the idea. It may need some fine tuning, but seems to work quite well in my tests:
public partial class Form1 : Form
{
byte[,] tiles;
const int rows = 50;
const int cols = 50;
public Form1()
{
SetStyle(ControlStyles.ResizeRedraw, true);
InitializeComponent();
tiles = new byte[cols, rows];
for (int i = 0; i < 10; i++)
{
tiles[20, i+20] = 1;
tiles[23, i+20] = 1;
tiles[32, i+20] = 1;
tiles[35, i+20] = 1;
tiles[i + 23, 30] = 1;
tiles[i + 23, 32] = 1;
tiles[21, i + 15] = 2;
tiles[21, i + 4] = 2;
if (i % 2 == 0) tiles[22, i] = 2;
}
tiles[20, 30] = 1;
tiles[20, 31] = 1;
tiles[20, 32] = 1;
tiles[21, 32] = 1;
tiles[22, 32] = 1;
tiles[33, 32] = 1;
tiles[34, 32] = 1;
tiles[35, 32] = 1;
tiles[35, 31] = 1;
tiles[35, 30] = 1;
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
using (SolidBrush b = new SolidBrush(Color.White))
{
for (int y = 0; y < rows; y++)
{
for (int x = 0; x < cols; x++)
{
switch (tiles[x, y])
{
case 0:
b.Color = Color.White;
break;
case 1:
b.Color = Color.Black;
break;
default:
b.Color = Color.Blue;
break;
}
e.Graphics.FillRectangle(b, x * ClientSize.Width / cols, y * ClientSize.Height / rows,
ClientSize.Width / cols + 1, ClientSize.Height / rows + 1);
}
}
}
}
private bool IsLiquid(int x, int y)
{
return tiles[x, y] > 1;
}
private bool IsSolid(int x, int y)
{
return tiles[x, y] == 1;
}
private bool IsEmpty(int x, int y)
{
return IsEmpty(tiles, x, y);
}
public static bool IsEmpty(byte[,] tiles, int x, int y)
{
return tiles[x, y] == 0;
}
private void ProcessTiles()
{
byte processedValue = 0xFF;
byte unprocessedValue = 0xFF;
for (int y = 0; y < rows; y ++)
for (int x = 0; x < cols; x++)
{
if (IsLiquid(x, y))
{
if (processedValue == 0xff)
{
unprocessedValue = tiles[x, y];
processedValue = (byte)(5 - tiles[x, y]);
}
if (tiles[x, y] == unprocessedValue)
{
BlobInfo blob = GetWaterAt(new Point(x, y), unprocessedValue, processedValue, new Rectangle(0, 0, 50, 50));
blob.ProcessMovement(tiles);
}
}
}
}
class BlobInfo
{
private int minY;
private int maxEscapeY;
private List<int> TopXes = new List<int>();
private List<int> BottomEscapeXes = new List<int>();
public BlobInfo(int x, int y)
{
minY = y;
maxEscapeY = -1;
TopXes.Add(x);
}
public void NoteEscapePoint(int x, int y)
{
if (maxEscapeY < 0)
{
maxEscapeY = y;
BottomEscapeXes.Clear();
}
else if (y < maxEscapeY)
return;
else if (y > maxEscapeY)
{
maxEscapeY = y;
BottomEscapeXes.Clear();
}
BottomEscapeXes.Add(x);
}
public void NoteLiquidPoint(int x, int y)
{
if (y < minY)
{
minY = y;
TopXes.Clear();
}
else if (y > minY)
return;
TopXes.Add(x);
}
public void ProcessMovement(byte[,] tiles)
{
int min = TopXes.Count < BottomEscapeXes.Count ? TopXes.Count : BottomEscapeXes.Count;
for (int i = 0; i < min; i++)
{
if (IsEmpty(tiles, BottomEscapeXes[i], maxEscapeY) && (maxEscapeY > minY))
{
tiles[BottomEscapeXes[i], maxEscapeY] = tiles[TopXes[i], minY];
tiles[TopXes[i], minY] = 0;
}
}
}
}
private BlobInfo GetWaterAt(Point start, byte unprocessedValue, byte processedValue, Rectangle bounds)
{
Stack<Point> toFill = new Stack<Point>();
BlobInfo result = new BlobInfo(start.X, start.Y);
toFill.Push(start);
do
{
Point cur = toFill.Pop();
while ((cur.X > bounds.X) && (tiles[cur.X - 1, cur.Y] == unprocessedValue))
cur.X--;
if ((cur.X > bounds.X) && IsEmpty(cur.X - 1, cur.Y))
result.NoteEscapePoint(cur.X - 1, cur.Y);
bool pushedAbove = false;
bool pushedBelow = false;
for (; ((cur.X < bounds.X + bounds.Width) && tiles[cur.X, cur.Y] == unprocessedValue); cur.X++)
{
result.NoteLiquidPoint(cur.X, cur.Y);
tiles[cur.X, cur.Y] = processedValue;
if (cur.Y > bounds.Y)
{
if (IsEmpty(cur.X, cur.Y - 1))
{
result.NoteEscapePoint(cur.X, cur.Y - 1);
}
if ((tiles[cur.X, cur.Y - 1] == unprocessedValue) && !pushedAbove)
{
pushedAbove = true;
toFill.Push(new Point(cur.X, cur.Y - 1));
}
if (tiles[cur.X, cur.Y - 1] != unprocessedValue)
pushedAbove = false;
}
if (cur.Y < bounds.Y + bounds.Height - 1)
{
if (IsEmpty(cur.X, cur.Y + 1))
{
result.NoteEscapePoint(cur.X, cur.Y + 1);
}
if ((tiles[cur.X, cur.Y + 1] == unprocessedValue) && !pushedBelow)
{
pushedBelow = true;
toFill.Push(new Point(cur.X, cur.Y + 1));
}
if (tiles[cur.X, cur.Y + 1] != unprocessedValue)
pushedBelow = false;
}
}
if ((cur.X < bounds.X + bounds.Width) && (IsEmpty(cur.X, cur.Y)))
{
result.NoteEscapePoint(cur.X, cur.Y);
}
} while (toFill.Count > 0);
return result;
}
private void timer1_Tick(object sender, EventArgs e)
{
ProcessTiles();
Invalidate();
}
private void Form1_MouseMove(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
int x = e.X * cols / ClientSize.Width;
int y = e.Y * rows / ClientSize.Height;
if ((x >= 0) && (x < cols) && (y >= 0) && (y < rows))
tiles[x, y] = 2;
}
}
}
From a fluid dynamics viewpoint, a reasonably popular lattice-based algorithm family is the so-called Lattice Boltzmann method. A simple implementation, ignoring all the fine detail that makes academics happy, should be relatively simple and fast and also get reasonably correct dynamics.