Related
Hi im new to c# and I am trying to learn how to (make a) scale in a panel. I have created a Mandelbrot, but now I have to zoom in on -2 to 2 in both axes.
My question is, how do I make a scale (e.g x,y 0.01) for this panel in which I can choose the center of the panel myself? I want my graph to go from -2 to 2 in both axes. I know I should be using Graphics.ScaleTransform to scale it and e.Graphics.TranslateTransform to center it, but I just can't figure out how.
This is my panel method :
public void panel1_Paint(object sender, PaintEventArgs e)
{
for (double p = 0; p < 400; p++)
{
for (double j = 0; j < 400; j++)
//over here I zoomed it in manually, but it was just to make sure the Mandelbrot was working
{
int mandel = mandelgetalberekening((p - 200)*0.01, (j-200)*0.01);
if (mandel % 2 == 0)
{
e.Graphics.FillRectangle(Brushes.Purple, (float)p, (float)j, 1, 1);
}
else
{
e.Graphics.FillRectangle(Brushes.White, (float)p, (float)j, 1, 1);
}
}
}
and if I try to use something like this I just gives me an empty window:
public void panel1_Paint(object sender, PaintEventArgs e)
{
e.Graphics.ScaleTransform((float)0.01, (float)0.01);
for (double p = 0; p < 400; p++)
{
for (double j = 0; j < 400; j++)
{
int mandel = mandelgetalberekening((p - 200)*0.01, (j-200)*0.01);
if (mandel % 2 == 0)
{
e.Graphics.FillRectangle(Brushes.Purple, (float)p, (float)j, 1, 1);
}
else
{
e.Graphics.FillRectangle(Brushes.White, (float)p, (float)j, 1, 1);
}
}
}
``
This question already has answers here:
Drawing glitches when using CreateGraphics rather than Paint event handler for custom drawing
(1 answer)
Why does PaintEventArgs.Graphics behave differently from Control.CreateGraphics?
(2 answers)
Closed 3 years ago.
I'm trying to write a code that draws a set of pixels from array of indices that point to a color value from another array (essentially palettes). I'm very new to drawing images on screen aside using picture box, so I have no proper experience with stuff like this. According to my research this code should work, but nothing gets drawn on the form. Any ideas what I'm doing wrong here?
public string[] colors = new string[] { "#FFFF0000", "#FF00FF00", "#FF0000FF", "#FFFFFF00", "#FFFF00FF", "#FF00FFFF" };
public byte[] pixels = new byte[] { 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5 };
public byte scale = 2;
public void PaintPixels()
{
Graphics g = CreateGraphics();
int x = 0;
int y = 0;
for (int p = 0; p < pixels.Length; p++)
{
using (var brush = new SolidBrush(ColorTranslator.FromHtml(colors[pixels[p]])))
{
g.FillRectangle(brush, x, y, scale, scale);
}
x += scale;
if(x > 255)
{
y += scale;
x = 0;
}
}
}
private void Form1_Load(object sender, EventArgs e)
{
Width = 256 * scale;
Height = 240 * scale;
PaintPixels();
}
Just override Form.OnPaint(PaintEventArgs e) like this:
public partial class Form1 : Form
{
private static readonly string[] colors =
new string[] { "#FFFF0000", "#FF00FF00", "#FF0000FF", "#FFFFFF00", "#FFFF00FF", "#FF00FFFF" };
private static readonly byte[] pixels =
new byte[] { 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5 };
private static readonly byte scale = 10;
public Form1()
{
InitializeComponent();
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
for (int p = 0, x = 0, y = 0; p < pixels.Length; p++, x += scale)
{
if (x > 255)
{
x = 0;
y += scale;
}
using (var brush = new SolidBrush(ColorTranslator.FromHtml(colors[pixels[p]])))
e.Graphics.FillRectangle(brush, x, y, scale, scale);
}
}
}
It gives:
I've been trying to create a bitmap, and use said bitmap to create an image which needs to be shown inside a picturebox. So far Google hasn't been of any help. The bitmap needs to be filled with black/white pixels defined in an array, but I've used Aliceblue for now.
When I run the code I get the error "value cant be null" at this line
Bitmap afbeelding = new Bitmap(resolutie, resolutie, g);
Here's what I've tried:
public void draw(Array array)
{
Bitmap afbeelding = new Bitmap(resolutie, resolutie, g);
for(int x = 0; x < array.Length; x++)
{
for (int y = 0; y < array.Length; y++)
{
afbeelding.SetPixel(x, y, Color.AliceBlue);
}
}
pictureBox1.Image = afbeelding;
//afbeelding = pictureBox1.CreateGraphics();
}
Does anyone know how to solve this? I'm not sure how to fill g since there isn't a DrawPixel function in Graphics
Assuming the array contains the definition of an image, you should probably fill the rows of the image with a chunk of the array, instead of filling the image horizontally and vertically with the array.
Let's suppose the array is for a 10 x 10 image, which would make the array 100 bytes long. You need to assign the first 10 bytes to the first row of the image, and so on. You also need to check the value of the array member whether to draw a pixel or not.
Example:
public void draw(bool[] array)
{
Bitmap afbeelding = new Bitmap(resolutieX, resolutieY);
for(int y = 0; y < resolutieY; y++)
{
for (int x = 0; x < resolutieX; x++)
{
if (array[y * resolutieX + x] == true)
afbeelding.SetPixel(x, y, Color.Black);
else
afbeelding.SetPixel(x, y, Color.White);
}
}
pictureBox1.Image = afbeelding;
}
To test this (assuming you have a button1 on the form):
int resolutieX = 100;
int resolutieY = 100;
Random R = new Random();
private void button1_Click(object sender, EventArgs e)
{
bool[] bArray = new bool[resolutieX * resolutieY];
for (int i = 0; i < bArray.Length; i++)
bArray[i] = R.Next(0, 2) == 1 ? true : false;
draw(bArray);
}
public void draw(int[] array)
{
Bitmap afbeelding = new Bitmap(11, 11);
for (int i = 0; i < array.Length; i++)
{
afbeelding.SetPixel(array[i], array[i], Color.Black);
}
pictureBox1.Image = afbeelding;
//afbeelding = pictureBox1.CreateGraphics();
}
private void Form1_Load(object sender, EventArgs e)
{
draw(new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
}
Why not locking the pixel array? Check this out, it is faster:
BitmapData Class
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!
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.