WPF 3D Кубик Рубика

Все исходники / Язык программирования C# / OS Windows / Desktop / WPF программирование / WPF 3D Кубик Рубика
Оглавление:
  1. 3D кубик Рубика
  2. Кубики для кубика Рубика
  3. Класс создания кубика Рубика
  4. Принцип создания кубика Рубика
  5. Координатные системы маленьких кубиков
  6. Выбор сегмента для поворота
  7. Закраска сторон кубика Рубика
  8. Управление поворотами
  9. Исходник приложения Кубик Рубика

3D кубик Рубика

Перемешивание и сборка кубика Рубика

WPF 3D API позволяет создавать не только простейшие трехмерные фигуры, но и более сложные 3D объекты, состоящие из примитивов. Создание в WPF трехмерных предметов гораздо проще чем если подобное писать на основе библиотеки DirectX. В WPF, в комплекте с дискретной 3D трансформацией, программистам предлагается лёгкая реализация анимации перемещения, вращения и масштабирования.

Следующий шаг после тестирования класса единичного трёхмерного кубика Cube3D - это применение его на практике для разработки компьютерной игрушки Кубик Рубика. Предлагаемое приложение служит отличным стартом создания реалистичной трёхмерной игры. В исходнике действующая модель кубика Рубика создаётся двумя классами: маленькие кубики Cube3D и полная сборка RubiksCube.

Кубики для кубика Рубика

Сегменты трехмерного кубика Рубика созданы на основе объектов класса Cube3D описанном на странице с исходником WPF вращение 3D кубиков. Класс Cube3D сводит к минимуму программный код визуализации правильного гексаэдра. Минимально достаточно только создание объекта Cube3D, без каких-либо настроек свойств, и "запчасть" для кубика Рубика готова. Окрашивание и управление сегментами реализуется классом RubiksCube.

Класс создания кубика Рубика

Создание трехмерной фигуры из маленьких кубиков происходит в одном классе RubiksCube, наследующего от ModelVisual3D. Принадлежность к классам Visual3D разрешает RubiksCube быть дочерним элементом области просмотра трехмерной пространства, поскольку ViewPort3D может содержать только потомков Visual3D.

Класс RubiksCube создаёт из сегментов кубик Рубика, закрашивает стороны в установленные цвета и управляет анимацией поворотов групп кубиков игрушки. RubiksCube не имеет открытых свойств настроек, но при необходимости их легко реализовать на основе данного исходника. Визуально кубик Рубика создаётся в файле разметки XAML, а повороты инициируются программным кодом.

Принцип создания кубика Рубика

Класс RubiksCube создаёт игровой гексаэдр по сегментам, сверху вниз, вдоль оси Y. Каждый сегмент - это 9 маленьких кубиков: первый сегмент находится в области положительных значений, второй - в центре оси координат, третий - со смещением в отрицательную область оси Y. Центральный гексаэдр скрыт другими кубиками, но оставлен для простоты кода.

Сегментация условная, каждый маленький кубик добавляется в коллекцию RubiksCube.Children отдельно и является самостоятельной единицей большого кубика Рубика. Цель сегментации - упростить создание кубика Рубика путём уменьшения количества программного кода: три сегмента создаются одним методом.

Маленькие гексаэдры в сборке большого куба располагаются симметрично относительно трёх осей координат. Размещаются вплотную к друг друг на расстоянии небольшого зазора, переменная ответственная за зазор - double _gap. Зазоры между кубиками создают впечатление реальной механической игрушки.

Программный код метода создания кубика Рубика из сегментов:
// Создание кубика сегментами из 9 кубиков сверху вниз по оси Y.
// size - размер кубиков и он же задаёт 
// смещение кубиков относительно соответствующих осей.
// gap - зазор между кубиками.
private void Create(double size, double gap)
{
    // Верхний сегмент
    Segment(size, size + gap, gap);
    // Сегмент в середине
    Segment(size, 0, gap);
    // Нижний сегмент
    Segment(size, -(size + gap), gap);

    // Раскраска кубика Рубика
    ColorFill();
}

private void Segment(double size, double y, double gap)
{
    // Каждый маленький кубик - отдельный дочерний элемент.
    // После поворотов сегментов кубики изменяют
    // своё положение в пределах конструкции кубика Рубика.
    Children.Add(new Cube3D()
    { 
        // Первичное смещение по осям.
        Transform = new TranslateTransform3D
        {
            OffsetX = -(size + gap),
            OffsetY = y,
            OffsetZ = size + gap
        },
        // Размер кубика
        Size = size
    });


    Children.Add(new Cube3D()
    {
        Transform = new TranslateTransform3D
        {
            OffsetX = 0,
            OffsetY = y,
            OffsetZ = size + gap
        },
        Size = size
    });

    Children.Add(new Cube3D()
    {
        Transform = new TranslateTransform3D
        {
            OffsetX = size + gap,
            OffsetY = y,
            OffsetZ = size + gap
        },
        Size = size
    });


    //
    Children.Add(new Cube3D()
    {
        Transform = new TranslateTransform3D
        {
            OffsetX = -(size + gap),
            OffsetY = y,
            OffsetZ = 0
        },
        Size= size
    });

    Children.Add(new Cube3D()
    {
        Transform = new TranslateTransform3D
        {
            OffsetX = 0,
            OffsetY = y,
            OffsetZ = size + gap
        },
        Size = size
    });

    Children.Add(new Cube3D()
    {
        Transform = new TranslateTransform3D
        {
            OffsetX = 0,
            OffsetY = y,
            OffsetZ = 0
        },
        Size = size
    });

    Children.Add(new Cube3D()
    {
        Transform = new TranslateTransform3D
        {
            OffsetX = size + gap,
            OffsetY = y,
            OffsetZ = 0
        },
        Size = size
    });


    //
    Children.Add(new Cube3D()
    {
        Transform = new TranslateTransform3D
        {
            OffsetX = -(size + gap),
            OffsetY = y,
            OffsetZ = -(size + gap)
        },
        Size = size
    });

    Children.Add(new Cube3D()
    {
        Transform = new TranslateTransform3D
        {
            OffsetX = 0,
            OffsetY = y,
            OffsetZ = -(size + gap)
        },
        Size = size
    });

    Children.Add(new Cube3D()
    {
        Transform = new TranslateTransform3D
        {
            OffsetX = size + gap,
            OffsetY = y,
            OffsetZ = -(size + gap)
        },
        Size = size
    });
}

Координатные системы маленьких кубиков

Первоначальная позиция позиция кубиков определяется свойством Cube3D.Position = (0,0,0) и располагает маленькие кубики в центре своих осей координат. В дальнейшем позиция каждого маленького гексаэдра остаётся неизменной относительно своей координатной системы, доказательством этого служит равенство Cube3D.Position = (0,0,0) после любой трансформации.

Перемещения и вращения составляющих кубиков в исходнике приложения происходит преобразованием их координатных систем. Для позиционирования маленьких кубиков при создании сегментов и вращения сегментов во время игры в методах RubiksCube.Segment(...) и RubiksCube.Rotate(...) определён программный код трансформации системы координат каждого гексаэдра кубика Рубика.

Программный код первичной расстановки маленьких гексаэдров:
// size - смещение относительно осей координат.
// gap - зазор между кубиками.
private void Segment(double size, double y, double gap)
{
    Children.Add(new Cube3D()
    {
        // Перенос координатной системы текущего кубика на необходимое смещение. 
        Transform = new TranslateTransform3D
        {
            OffsetX = -(size + gap),
            OffsetY = y, 
            OffsetZ = size + gap
        },
        Size = size
    });
}

Каждый кубик хранит матрицу своей трансформации в свойстве Cube3D.Transform. Хранение координат положения начинается во время первичной расстановки по местам маленьких гексаэдров и сохраняется до закрытия приложения.

Во время игры, вращение соответствующих сегментов происходит путём комбинирования трансформации переноса кубиков (TranslateTransform3D) с трансформацией вращения (RotateTransform3D). Если применить трансформацию вращения к кубикам без учёта их положения - смещение аннулируется и каждый кубик будет вращаться вокруг своей первоначальной позиции, т.е. (x = 0; y = 0; z = 0).

Метод вращения выбранного сегмента:
private void Rotate(bool clockwise, Vector3D axisRotation, Func selectSegment)
{
    // Управление последовательностью анимации.
    if (_isCanAnimation == false) return;
    _isCanAnimation = false;

    // Выбор оси вращения.
    RotateTransform3D rotate = new(new AxisAngleRotation3D(axisRotation, 0));

    foreach (Cube3D item in Children)
    {
        // Функция критерия отбора кубиков для сегмента поворота.
        if (selectSegment(item) == true)
        {
            // Комбинирование предыдущей трансформации с трансформацией вращения.
            item.Transform = new Transform3DGroup()
            {
                Children = { item.Transform, rotate }
            };
        }
    }
    // Программный код анимации поворота.
    DoubleAnimation rotateAnimation = new()
    {
        Duration = TimeSpan.FromSeconds(_animationDuration)
    };

    rotateAnimation.By = clockwise == false ? 90 : -90;

    rotateAnimation.Completed += RotateAnimation_Completed;
    rotate.Rotation.BeginAnimation(AxisAngleRotation3D.AngleProperty, rotateAnimation);

    void RotateAnimation_Completed(object? sender, EventArgs e)
    {
        _isCanAnimation = true;
    }
}

Выбор сегмента для поворота

Координатная система кубика рубика

Маленькие кубики располагаются со смещением относительно осей координат, группы по 9 кубиков имеют равное смещение только по одной координатной оси. После поворотов во время игры, кубики изменяют своё положение, состав групп меняется, но в абсолютно всегда создаются вновь сегменты из 9 кубиков имеющих равные координаты только по одной координатной оси. В исходном коде приложения это свойство конструкции кубика Рубика определяет принцип выбора сегмента для вращения.

При определении сегмента поворота используется равенство смещения кубиков по оси вращения. Например: центральный вертикальный сегмент - это кубики со смещением по оси Х равным 0. Для поворота этого сегмента необходимо выбрать все кубики с координатой item.Transform.Value.OffsetX = 0.

После поворота, из за особенностей вычисления чисел с плавающей запятой, координата Х может иметь значение не равное нулю, например вот такое: item.Transform.Value.OffsetX = 2.353672812205332Е-16. Поэтому положение сегмента вычисляется с поправкой на неточность:
item.Transform.Value.OffsetX > -0.1 && item.Transform.Value.OffsetX < 0.1.
Соответственно, крайние сегменты выбираются аналогично, по равному смещению вдоль оси поворота.

Методы класса RubiksCube для выбора и вращения сегментов относительно оси Х:
#region  === Группы вращения вокруг оси Х ===
// Поворот левого вертикального сегмента
public void RotateLeftX(bool clockwise = false)
{
    // Критерий выбора кубиков для поворота.
    static bool select(Cube3D item) => item.Transform.Value.OffsetX < -0.1;
    RotateX(clockwise, select);
}
// Поворот центрального вертикального сегмента
public void RotateMiddleX(bool clockwise = false)
{
    static bool select(Cube3D item) => item.Transform.Value.OffsetX > -0.1 && item.Transform.Value.OffsetX < 0.1;
    RotateX(clockwise, select);
}
// Поворот правого вертикального сегмента
public void RotateRightX(bool clockwise = false)
{
    static bool select(Cube3D item) => item.Transform.Value.OffsetX > 0.1;
    RotateX(clockwise, select);
}
// Унифицированный метод выбора оси поворота.
private void RotateX(bool clockwise, Func select)
{
    Rotate(clockwise, new Vector3D(1, 0, 0), select);
}

#endregion

Закраска сторон кубика Рубика

Выбор стороны для закраски определённым цветом осуществляется аналогично механике выбора сегмента для поворота. Окрашиваются наружные стороны кубиков расположенных в одинаковой позиции. Например, верхняя сторона - это все кубики с координатой оси Y > 0.1 пространственной единицы, нижние кубики отбираются координатой Y < -0.1 единицы.

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

Программный код метода раскраски кубика Рубика в приложении:
private void ColorFill()
{
    foreach (Cube3D item in Children)
    {
        // Закраска передней стороны
        if (item.Transform.Value.OffsetZ > 0.1) item.Front = Brushes.Red;

        // Закраска задней стороны
        if (item.Transform.Value.OffsetZ < -0.1) item.Back = Brushes.Orange;

        // Левой
        if (item.Transform.Value.OffsetX < -0.1) item.Left = Brushes.Green;

        // Правой
        if (item.Transform.Value.OffsetX > 0.1) item.Right = Brushes.Blue;

        // Верхней
        if (item.Transform.Value.OffsetY > 0.1) item.Top = Brushes.White;

        // Нижней
        if (item.Transform.Value.OffsetY < -0.1) item.Bottom = Brushes.Yellow;
    }
}

Управление поворотами

Повороты осуществляются при нажатии клавиши Пробел. Управление простейшее: 9 поворотов для перемешивания сегментов и 9 поворотов в обратную сторону для сборки кубика Рубика. На основе этого управления можно легко создать код произвольного выбора сегмента для поворота, например с помощью кнопок.

Исходник приложения Кубик Рубика

Исходный код приложения написан в интегрированной среде программирования MS Visual Studio 2022, платформа .NET6. Исходник упакован вместе с файлом приложения.

Скачать исходник

Тема: «WPF 3D Кубик Рубика» Язык программирования C# Wpf3DRubiksCube-vs17.zip Размер:120 КбайтЗагрузки:546