Форум программистов, компьютерный форум, киберфорум
Storm23
Войти
Регистрация
Восстановить пароль
Карта форума Блоги Сообщество Поиск Заказать работу  
Рейтинг: 3.67. Голосов: 3.

Задумал я сделать игрушку...

Запись от Storm23 размещена 26.03.2016 в 14:21
Обновил(-а) Storm23 27.03.2016 в 21:12

В этой записи я буду писать много всякого бреда, отрывочных мыслей и бессвязных кусков кода. Это своеобразный дневник разработки игры. А какая разработка, такой и дневник... :[sarcasm]
Писать буду стараться каждый день, описывать разные стадии разработки. Не все решения доживут до финала.
Постараюсь также выкладывать промежуточные версии исходников и скриншоты текущего состояния программы.


День1. Powder Toy.

Натолкнулся я недавно вот на такую игрульку The Powder Toy. Это песочница. В буквальном смысле - с песком :о) Смысл в том, что вы можете сыпать песочек, водичку и другие материалы, которые сталкиваются и каким-то образом взаимодействуют друг с другом. Узнал об игре из этой темы.

Играть в такие игры я не особо люблю, я предпочитаю шутеры. Но задело меня в этой игре то что она имеет маленький размер, написана в стиле олд-скул-пиксел-арт, и то, что в ней высокое быстродействие при том что считается довольно серьезная (как казалось) физика. При этом, судя по всему, она не использует графических или физических движков, и не использует шейдеры. При этом она моделирует огромное число взаимодействующих частиц. Даже при размере небольшого игрового поля 600*400, на нем может поместиться до 240 тысяч частиц. Если реализовать взаимодействие в лоб (чего конечно в реальности никто не делает, но тем не менее), то это будет O(n^2) и нужно обрабатывать 58 миллиардов(!) попарных взаимодействий. И это нужно делать с FPS не ниже 30. А в Powder Toy FPS под 60! Вы конечно можете сказать, что в современных играх тоже есть огромное число взаимодействующих объектов, но в реальности это совсем не то. Физика в игре моделируется только вокруг игрока и самих взимодействующих объектов не так уж и много. И уж точно меньше чем песчинок в куче песка. Если вы стоите в комнате, то там есть всего лишь десяток объектов, которые могут двигаться, и для которых считается физика. А здесь каждая песчинка на экране обрабатывается как полноценный физический объект. Конечно, в современных играх в таких случаях используются партиклы, и их обычно много. Но партиклы никогда не обрабатывают коллизии и не могут сталкиваться друг с другом. В Powder же ситуация совсем другая. Все песчинки взаимодействуют, сталкиваются. И это удивительно. Удивительно как такая маленькая простенькая програмка может моделировать такую сложную физическую систему. Тут я конечно немного преувеличиваю свое удивление, но все таки меня заинтересовало как это все работает.

Вы можете подумать, что я далее буду описывать как сделать игру наподобие Powder Toy, а вот и нет. Потому что делать я буду совсем другую игру :о)

Зато Powder Toy натолкнула меня на много интересных методов, которые неплохо было бы использовать в других разработках. И да, простенький аналог Powder я все же сделал.

Получилось неплохо :о) Быстродействие достаточное, моделирование физики и вывод графики работало довольно шустро. В процессе разработки придумал некоторые методы оптимизации, с которыми и хочу поделиться.

Далее я буду описывать свою версию программы. Как точно работает настоящий Powder Toy - я точно не знаю, я смотрел код лишь поверхностно, но думаю что работает примерно так же.

В чем же секрет Powder? Секрет как раз в тех самых пикселах. А точнее в элементарных ячейках, в которых могут находиться песчинки.

Работает это так: каждая песчинка имеет свою позицию, задаваемую двумерным вектором. Она также имеет вектора скорости и ускорения. Вектора имеют компоненты типа float. То есть координаты не дискретные, и частица может занимать любое положение в пространстве. Движение частицы моделируется обычным образом - через закон Ньютона силы делятся на массу, получается ускорение. Затем находится скорость и приращивается координата местоположения. Это стандартный набор для любого физического движка.

Однако, после того, как координаты песчинок пересчитаны, они помещаются в двумерную сетку. Вектора округляются до целых и находится ячейка сетки, куда должна попасть песчинка.

И тут есть главная хитрость: в каждую ячейку сетки помещается ровно одна песчинка. Если ячейка, куда должна попасть песчинка - занята, значит песчинка не может занять эту ячейку и должна "искать" себе другую. В таком случае считается что произошло столкновение частиц. Скорость частицы обнуляется, а координата возвращается к своему значению на предыдущем шаге.

Песчинки, которые не поместились в ячейки - не отображаются. Вместо этого им придается случайное ускорение в произвольную сторону. Здесь вторая хитрость. Если бы мы считали столкновения "честно", то нам нужно было бы находить вектора, направления соударения и так далее. Но у нас ведь песок, мелкие частицы. Пользователь все равно не видит форму частицы и не видит точку соударения частиц. Поэтому фактически мы можем присваивать случайный "отскок" без потери правдоподобности. Будет выглядеть так, что песчинки как-то там ударились и одна из них отскочила в случайную сторону.

Далее процесс продолжается, снова высчитываются силы, ускорения, скорости и новые местоположения. И снова частицы заносятся в ячейки и так далее.

Такой простой алгоритм позволяет нам моделировать огромное число частиц и их столкновений практически мгновенно. Красиво, да? Конечно, метод сеток я знал и раньше, но все таки, обычно он используется немного по-другому.

Как это выглядит на практике можно посмотреть на скриншотах и в иcходных кодах.
Приложение вышло очень простое, компактное (20 кб exe файл) и быстрое.
В режиме моделирования около 100 тысяч частиц выдается производительность 45 fps для физики, и 52 fps - отрисовка. Работа ведется в два потока, занимает два ядра процессора.

Класс Particle
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
    /// <summary>
    /// Базовый класс частицы
    /// </summary>
    abstract class Particle
    {
        public PointF Location { get; set; }
        public PointF Velocity { get; set; }
        public PointF Acceleration { get; set; }
        public float Mass { get; set; }
        public virtual Color Color { get; set; }
 
        public int X { get { return (int)Location.X; } }
        public int Y { get { return (int)Location.Y; } }
 
        public Particle()
        {
            Mass = 1;
            Color = Color.Orange;
            //сила тяжести
            Acceleration = new PointF(0, 9.8f);
        }
 
        /// <summary>
        /// Обновление сил, скоростей и координат
        /// </summary>
        public virtual void Update(Sandbox sb, float dt)
        {
            var PrevLocation = Location;
 
            //приращение скорости
            Velocity = Velocity.Add(Acceleration, dt * dt);
            //приращение координаты
            Location = Location.Add(Velocity);
            //мы ударились о другую частицу?
            var cell = sb[X, Y];
            if (cell != null && cell != this)
            {
                //остаемся на месте, обрабатываем столкновение
                Location = PrevLocation;
                OnCollision(sb, cell);
            }
        }
 
        /// <summary>
        /// Событие соударения
        /// </summary>
        public virtual void OnCollision(Sandbox sb, Particle other)
        {
            if (other.Mass < Mass)//меняем местами, если столкнулись с более тяжелой частицей
            {
                var loc = other.Location;
                other.Location = Location;
                Location = loc;
            }
            else
                //отскок в сторону
                Velocity = new PointF(FastRnd.Next(), 0);
        }
    }


Класс Sandbox
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
    /// <summary>
    /// Песочница - контейнер частиц
    /// </summary>
    class Sandbox
    {
        public const int WIDTH = 600;
        public const int HEIGHT = 400;
 
        public LinkedList<Particle> Particles = new LinkedList<Particle>();
        public Particle[,] Cells = new Particle[WIDTH, HEIGHT];
 
        public Particle this[int x, int y]
        {
            get
            {
                if (x < 0 || x >= WIDTH || y < 0 || y >= HEIGHT) return null;
                return Cells[x, y];
            }
        }
 
        public void Update(float dt)
        {
            //обновляем частицы
            var pp = Particles.First;
            while (pp != null)
            {
                var next = pp.Next;
                pp.Value.Update(this, dt);
                pp = next;
            }
 
            //заносим частицы в ячейки
            var newCells = new Particle[WIDTH, HEIGHT];
            pp = Particles.First;
            while(pp != null)
            {
                var next = pp.Next;
                var p = pp.Value;
                var xx = (int)p.Location.X;
                var yy = (int)p.Location.Y;
                //частица вышла за пределы поля?
                if (xx < 0 || xx >= WIDTH || yy < 0 || yy >= HEIGHT)
                    lock (Particles) Particles.Remove(pp);//удаляем
                else
                    newCells[xx, yy] = p;
                pp = next;
            }
 
            Cells = newCells;
        }
    }


Это то, что касается физики. Теперь еще один момент - как это все выводить на экран? В Powder не используется ни DirectX, ни OpenGL. Значит вариант один - обычный GDI. Но тут нужно обеспечить максимальную производительность. Как мы это сделаем? Об этом будет День Второй...

День 2. DirectX? Нет, не слышал...

В любой игре есть два главных момента - физика и графика. С физикой мы немного разобрались. Теперь разберемся с графикой.

Я мог бы взять игровой движок типа Unity или XNA, или хотя бы WPF. И рисовать все в нем. Но это не интересно. Какое удовольствие я получу от того что буду использовать то что кто то сделал уже до меня? К тому же мы будем делать такие вещи, которые сложно сделать в стандартных движках. Вы поймете о чем я в следующие дни.

Итак. Рисовать мы будем просто через стандартные методы WinForms/GDI+.
Я перепробовал разные варианты, какие-то работали получше, какие-то похуже. Что-то было быстрее, а что-то давало больше возможностей. В итоге я остановился на таком варианте:
SandboxPanel
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
    /// <summary>
    /// Панель отрисовки модели
    /// </summary>
    partial class SandboxPanel : UserControl
    {
        private Bitmap bmp;
        public Sandbox Sandbox { get; set; }
 
        public SandboxPanel()
        {
            InitializeComponent();
 
            SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint | ControlStyles.Opaque, true);
 
            //создаем буферный битмап
            bmp = new Bitmap(Sandbox.WIDTH, Sandbox.HEIGHT, PixelFormat.Format32bppPArgb);
        }
 
        protected override void OnPaintBackground(PaintEventArgs e)
        {
            //игнорируем отрисовку бекграунда
        }
 
        public int FrameCounter { get; set; }
 
        protected override void OnPaint(PaintEventArgs e)
        {
            if (Sandbox == null)
                return;
 
            FrameCounter++;
 
            e.Graphics.InterpolationMode = InterpolationMode.NearestNeighbor;
            e.Graphics.PixelOffsetMode = PixelOffsetMode.None;
 
            //отрисовываем частицы на битмапе
            var particles = Sandbox.Cells;
            
            using(var wr = new ImageWrapper(bmp, false))
            for (int x = 0; x < Sandbox.WIDTH; x++)
            for (int y = 0; y < Sandbox.HEIGHT; y++)
            {
                var p = particles[x, y];
                if (p != null)
                {
                    var c = p.Color;
                    wr.SetPixelUnsafe(x, y, c.R, c.G, c.B);
                }
            }
 
            //отрисовываем битмап
            e.Graphics.DrawImageUnscaled(bmp, Point.Empty);
        }
    }

Собственно это весь код графики в PowderToy.

Игровое поле имеет фиксированный размер 600x400. В конструкторе панели сразу создается битмап такого же размера. Этот битмап нужен для более быстрой отрисовки графики. Формат пикселей - PArgb 32bpp. Я использую PArgb (premultiplied ARGB) потому что он отрисовывается быстрее чем простой Argb.
Также, в конструкторе я задаю стандартные параметры для буферизации отрисовки, предотвращающие мигание:
C#
1
2
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | 
          ControlStyles.UserPaint | ControlStyles.Opaque, true);
Параметр ControlStyles.Opaque выставляется для того, что бы система не тратила время на очистку бекграунда в каждом кадре. ControlStyles.OptimizedDoubleBuffer - включает двойную буферизацию.

Далее. Вся отрисовка происходит в методе OnPaint.
Сначала задаются параметры графики, увеличивающие скорость вывода изображений:
C#
1
2
            e.Graphics.InterpolationMode = InterpolationMode.NearestNeighbor;
            e.Graphics.PixelOffsetMode = PixelOffsetMode.None;
Эти настройки отключают интерполяцию изображения, и выключают точное выравнивание пикселей. Нам эти свойства не нужны.

Далее, используя класс ImageWrapper, дающий unsafe доступ к пикселам буферного битмапа, мы просто пробегаемся по всем ячейкам нашего поля и если там есть песчинка, заносим ее цвет в пиксел буферного битмапа (забегая вперед, скажу, что это не самый быстрый способ, впоследствии я откажусь от него).

Далее просто отрисовываем битмап на канве:
C#
1
2
            //отрисовываем битмап
            e.Graphics.DrawImageUnscaled(bmp, Point.Empty);
Отрисовка запускается в отдельном потоке от физики (ну собственно это и есть GUI поток). Код дает 50-60 FPS при размере поля 600x400 (в дальнейшем я увеличу быстродействие до 65 FPS).

Про Powder Toy все. Програмка получилась простая и быстрая. Как я люблю.
Дальше я буду использовать ее идеи для собственно другой игры, которая будет намного больше и сложнее.

Об этом будет День Третий.

Продолжение следует...

Исходник: PowderToy_source
Exe: PowderToy_bin
Миниатюры
Нажмите на изображение для увеличения
Название: 00_1.png
Просмотров: 536
Размер:	130.1 Кб
ID:	3680  
Размещено в C#, WinForms
Показов 6083 Комментарии 3
Всего комментариев 3
Комментарии
  1. Старый комментарий
    Аватар для 8Observer8
    Я запускал exe и из исходников запускал, у меня просто окно с чёрным фоном и ComboBox
    Запись от 8Observer8 размещена 25.02.2017 в 16:57 8Observer8 вне форума
  2. Старый комментарий
    Аватар для Storm23
    Цитата:
    Сообщение от 8Observer8 Просмотреть комментарий
    Я запускал exe и из исходников запускал, у меня просто окно с чёрным фоном и ComboBox
    Ну правильно. Мышкой нужно рисовать стены, сыпать песочек.
    Запись от Storm23 размещена 25.02.2017 в 17:39 Storm23 вне форума
  3. Старый комментарий
    Аватар для 8Observer8
    Вау! Классно!
    Запись от 8Observer8 размещена 25.02.2017 в 18:54 8Observer8 вне форума
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2024, CyberForum.ru