Моделирование на основе клеток: модель локального давления?

Я пытаюсь добавить полуреалистичную воду в мой 2D-платформер на основе плитки. Вода должна действовать несколько реалистично, с моделью давления, которая работает полностью локально. (IE может использовать только данные из ближайших к нему ячеек) Эта модель необходима из-за характера моей игры, где вы не можете быть уверены, что нужные вам данные не находятся внутри области, которая не находится в памяти.

Я пробовал один из методов до сих пор, но я не мог доработать его достаточно для работы с моими ограничениями.

Для этой модели каждая ячейка будет слегка сжимаемой, в зависимости от количества воды в указанной ячейке. Когда содержание воды в ячейке было больше нормальной емкости, ячейка попыталась бы развернуться вверх. Это создало неплохую симуляцию, медленную медленную (Not lag; изменения в воде занимали время, чтобы размножаться.), Временами. Когда я попытался реализовать это в своем движке, я обнаружил, что у моих ограничений не хватает точности, необходимой для работы. Я могу предоставить более подробное объяснение или ссылку на оригинальную концепцию, если вы пожелаете.

Мои ограничения:

  • Только 256 дискретных значений для уровня воды. (Нет переменных с плавающей запятой:() - EDIT. Поплавки в порядке.
  • Фиксированный размер сетки.
  • Только 2D.
  • Конфигурации U-Bend должны работать.

Язык, который я использую, - это С#, но я, вероятно, могу взять другие языки и перевести его на С#.

Вопрос в том, может ли кто-нибудь дать мне модель давления для воды, следуя моим ограничениям как можно ближе?

Ответ 1

Попробуйте обработать каждую непрерывную область воды как единую область (например, заливку наводнения) и дорожку 1) самую низкую ячейку (ы), где может выйти вода, и 2) самая высокая ячейка (я), из которой может наступить вода, вода сверху вниз. Это не локально, но я думаю, что вы можете обрабатывать края области, в которую хотите повлиять, как не подключенные, и обрабатывать любое подмножество, которое вы хотите. Пересчитайте, какие области смежны в каждом кадре (повторное наводнение на каждом кадре), так что, когда сгустки сходятся, они могут начать обрабатываться как один.

Вот мой код из демонстрации Windows Forms этой идеи. Может потребоваться тонкая настройка, но, похоже, в моих тестах неплохо работает:

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;
     }
  }
}

Ответ 2

Как насчет другого подхода?

Забудьте о поплавках, которые задают проблемы округления в конечном счете. Вместо этого, как насчет единицы воды?

Каждая ячейка содержит определенное количество единиц воды. На каждой итерации вы сравниваете ячейку с ней 4 соседи и двигаетесь, скажем, 10% (измените это, чтобы изменить скорость распространения) разницы в количестве единиц воды. Функция отображения преобразует единицы воды в уровень воды.

Чтобы избежать проблем с порядком расчета, используйте два значения: одно для старых единиц, одно для нового. Вычислите все, а затем скопируйте обновленные значения обратно. 2 ints = 8 байт на ячейку. Если у вас есть миллион клеток, которые по-прежнему имеют только 8 МБ.

Если вы на самом деле пытаетесь имитировать волны, вам нужно также сохранить значения потока - 4, 16 мб. Чтобы заставить волну поместить некоторую инерцию в поток - после того, как вы вычислите нужный поток, переместите предыдущий поток, скажем, на 10% пути к желаемому значению.

Ответ 3

С точки зрения гидродинамики разумно популярным семейством алгоритмов на основе решетки является так называемый метод Lattice Boltzmann. Простая реализация, игнорируя все тонкие детали, которые радуют ученых, должна быть относительно простой и быстрой, а также получить разумно правильную динамику.