Карточная игра на Unity. Динамическая колода
Этот туториал отличается от моих предыдущих, поскольку он ориентирован на геймджемы и прототипирование, в особенности, карточных игр. Мы собираемся создать 2D колоду игральных карт в Unity не используя ни единого рисунка — исключительно с помощью кода.
Компоненты игральной колоды карт
В игральной колоде содержится всего 52 карты с 13 картами каждой из 4 разных мастей. Чтобы создать пригодный для использования код, нам необходимо создать эти 4 масти, закругленную прямоугольную основу карты и дизайн обложки карт.
Дизайном обложки у карты может быть любой абстрактный рисунок, и существует множество способов создать его. Мы сделаем простой бесшовный узор, который для создания дизайна потом замостит поверхность. Никакого специального дизайна для тузов (A), королей (K), дам (Q) и валетов (J) у нас не будет.
Альтернативные решения
Перед тем как начать, я должен упомянуть, что существуют более простые решения, которые мы могли бы использовать для создания колоды карт. Некоторые из них перечислены ниже.
- Очевидным является использование для всех эскизов предварительно отрисованного арта.
- Менее очевидно использовать шрифт, который содержит все необходимые символы. Вышеупомянутый шрифт мы также можем превратить в битмаповый, чтобы уменьшить число вызовов отрисовки и повысить производительность.
Решение на основе шрифтов самое быстрое и простое, если вы ходите делать быстрые прототипы.
Создание текстур во время исполнения
Первым шагом изучим как создать Texture2D с использованием кода, который потом будет использован для создания Sprite в Unity. Следующий код показывает создание пустой текстуры 256×256.
Texture2D texture = new Texture2D(256, 256, TextureFormat.ARGB4444, false); texture.filterMode=FilterMode.Trilinear; texture.wrapMode=TextureWrapMode.Clamp; texture.Apply();
Идея в том, чтобы нарисовать все эскизы до того, как мы будем использовать метод Apply. Мы можем нанести эскиз на текстуру пиксель за пикселем, используя метод SetPixel, как показано ниже.
texture.SetPixel(x, y, Color.white);
Например, если мы хотели заполнить всю текстуру цветом, мы могли бы использовать метод следующим образом.
private void PaintRectangle(Texture2D texture, Rect rectBounds, Color color) { for (int i=(int)rectBounds.x;i<rectBounds.x+rectBounds.width;i++){ for (int j=(int)rectBounds.y;j<rectBounds.y+rectBounds.height;j++){ texture.SetPixel(i, j, color); } } } // PaintRectangle(texture,new Rect(0,0,256,256),Color.red);
Как только мы создали Texture2D, мы можем использовать ее для создания Sprite, выводимого на экран.
Sprite sprite = Sprite.Create(texture, new Rect(0.0f, 0.0f, texture.width, texture.height), new Vector2(0.5f, 0.5f),1);
Сложной частью всего этого является создание на текстуре нужных эскизов.
Создание фигуры для червовой масти
Когда дело доходит до создания фигуры для червей, мы можем использовать несколько различных подходов, среди которых есть как сложные уравнения, так и простое смешение фигур. Как показано ниже мы будем использовать метод смешения фигур, в частности, тот что с треугольником.
Как вы заметили, мы можем использовать два круга и квадрат или треугольник, чтобы создать базовую фигуру сердца. Это означает, что у сердца не будет тех самых красивых изгибов, но оно идеально подойдет для нашей цели.
Рисование круга
Давайте освежим некоторые уравнения, чтобы нарисовать круг. Для круга с центром в начале координат и радиусом r уравнение для точки (x, y) на окружности имеет вид x2 + y2 = r2. Теперь, если центр круга находится в точке (h, k), уравнение становится (x-h) 2 + (y-k) 2 = r2. Таким образом, если у нас есть ограничивающий прямоугольник, то мы можем перебрать все точки внутри этого прямоугольника и определить, какие из них попадают в круг, а какие нет. Основываясь на этом понимании, мы можем с легкостью создать метод PaintCircle, как показано ниже.
private void PaintCircle(Texture2D texture,float radius, Vector2 midPoint, Color color){ Rect circleBounds=new Rect(); circleBounds.x=Mathf.Clamp(midPoint.x-(radius),0,resolution); circleBounds.y=Mathf.Clamp(midPoint.y-(radius),0,resolution); circleBounds.width=Mathf.Clamp(2*radius,0,resolution); circleBounds.height=Mathf.Clamp(2*radius,0,resolution); float iValue; for (int i=(int)circleBounds.x;i<circleBounds.x+circleBounds.width;i++){ for (int j=(int)circleBounds.y;jmidPoint.x-iValue&&i<midPoint.x+iValue){ texture.SetPixel(i, j, color); } } } } PaintCircle(texture,radius,mid,Color.red);
Как только у нас есть метод PaintCircle, мы можем продолжить создание фигуры червей как показано ниже.
void PaintHearts(Texture2D texture){ //2 circles on top float radius =resolution*0.26f; Vector2 mid=new Vector2(radius,resolution-radius); PaintCircle(texture,radius,mid,Color.red); mid=new Vector2(resolution-radius,resolution-radius); PaintCircle(texture,radius,mid,Color.red); //triangle at bottom float width=resolution*0.58f; int endJ=(int)(resolution*0.65f); int startJ=(int)(resolution*0.1f); float delta=(width/endJ); float midI=resolution*0.5f; for (int i=0;i<resolution;i++){ for (int j=startJ;j(midI-(delta*(j-startJ)))&&i<(midI+(delta*(j-startJ)))){ texture.SetPixel(i, j, Color.red); } } } }
Переменная resolution является шириной и высотой текстуры.
Создание фигуры для бубновой масти
Мы обсудим два способа нарисовать бубны.
Рисование простого ромба
Проще всего расширить код использованный для треугольника и добавить перевернутый треугольник наверху, чтобы создать необходимую фигуру как показано ниже.
void PaintDiamond(Texture2D texture){ float width=resolution*0.35f; for (int i=0;i<resolution;i++){ for (int j=0;jmidJ){ j=resolution-j; if(i>(midI-(delta*j))&&i(midI-(delta*j))&&i<(midI+(delta*j))){ isValid=true; } } return isValid; } PaintDiamond(texture);
Рисование изогнутого ромба
Второй способ – это использовать еще одно уравнение для создания улучшенной, изогнутой версии ромба. Мы будем использовать его для создания узора на обратной стороне нашей карты. Уравнение для круга получается из исходного уравнения для эллипса, оно выглядит как (x/a)2 + (y/b)2 = r2.
Это уравнение совпадает с уравнением окружности, когда переменные a и b равны 1. Затем уравнение эллипса можно расширить в уравнение суперэллипса для аналогичных фигур, просто изменив степень, (x/a)n + (y/b)n = rn. Поэтому когда n равно 2, у нас есть эллипс, а для других значений n у нас будут разные фигуры, одной из которых является наш ромб. Мы можем использовать подход, использованный для получения метода PaintCircle и получить новый метод PaintDiamond.
private void PaintDiamond(Texture2D texture, Rect rectBounds, Vector2 midPoint, Color color, float n=0.8f) { float iValue; int a=(int)(rectBounds.width/2); int b=(int)(rectBounds.height/2); float nRoot=1/n; float delta; float partialOne; rectBounds.width=Mathf.Clamp(rectBounds.x+rectBounds.width,0,resolution); rectBounds.height=Mathf.Clamp(rectBounds.y+rectBounds.height,0,resolution); rectBounds.x=Mathf.Clamp(rectBounds.x,0,resolution); rectBounds.y=Mathf.Clamp(rectBounds.y,0,resolution); for (int i=(int)rectBounds.x;i<rectBounds.width;i++){ for (int j=(int)rectBounds.y;jmidPoint.x-iValue && i<midPoint.x+iValue){ texture.SetPixel(i, j, color); } } } }
Рисование закругленного прямоугольника
Изменяя значение n, это же уравнение можно использовать для создания базовой формы в виде закругленного прямоугольника для нашей карты.
private void PaintRoundedRectangle(Texture2D texture) { for (int i=0;i<resolution;i++){ for (int j=0;jmid.x-iValue && i<mid.x+iValue){ isValid=true; } return isValid; }
Рисование бесшовного узора
Используя этот метод PaintDiamond, мы можем нарисовать пять ромбов и создать бесшовный узор для задней стороны нашей карты.
Код для рисования бесшовного узора будет такой, как ниже.
private void PaintTilingDesign(Texture2D texture, int tileResolution) { Vector2 mid=new Vector2(tileResolution/2,tileResolution/2); float size=0.6f*tileResolution; PaintDiamond(texture,new Rect(mid.x-size/2,mid.y-size/2,size,size),mid,Color.red); mid=new Vector2(0,0); PaintDiamond(texture,new Rect(mid.x-size/2,mid.y-size/2,size,size),mid,Color.red); mid=new Vector2(tileResolution,0); PaintDiamond(texture,new Rect(mid.x-size/2,mid.y-size/2,size,size),mid,Color.red); mid=new Vector2(tileResolution,tileResolution); PaintDiamond(texture,new Rect(mid.x-size/2,mid.y-size/2,size,size),mid,Color.red); mid=new Vector2(0,tileResolution); PaintDiamond(texture,new Rect(mid.x-size/2,mid.y-size/2,size,size),mid,Color.red); }
Создание фигуры для пиковой масти
Фигура для пик – это просто вертикальное отражение фигуры червей, наложенное на базовую фигуру. Эта базовая фигура останется одинаковой для треф. На рисунке ниже показано, как для создания базовой фигуры мы можем использовать два круга.
Метод PaintSpades будет таков, как показано ниже.
void PaintSpades(Texture2D texture){ //2 круга посередине float radius =resolution*0.26f; Vector2 mid=new Vector2(radius,resolution-2.2f*radius); PaintCircle(texture,radius,mid,Color.black); mid=new Vector2(resolution-radius,resolution-2.2f*radius); PaintCircle(texture,radius,mid,Color.black); //треугольник наверху float width=resolution*0.49f; int startJ=(int)(resolution*0.52f); float delta=(width/(resolution-startJ)); float midI=resolution*0.5f; int alteredJ; radius=resolution*0.5f; float midJ=resolution*0.42f; float iValue; for (int i=0;i<resolution;i++){ //верхний треугольник for (int j=startJ;j(midI-(delta*alteredJ))&&i<(midI+(delta*alteredJ))){ texture.SetPixel(i, j, Color.black); } } //ножка for (int k=0;kmid.x+iValue){ mid=new Vector2(resolution,midJ); iValue=(Mathf.Sqrt(radius*radius-((k-mid.y)*(k-mid.y))));//+mid.x; if(i<mid.x-iValue){ texture.SetPixel(i, k, Color.black); } } } } }
Создание фигуры для масти треф
В этом месте я уверен, что вы можете самостоятельно догадаться, как просто стало сделать фигуру для треф. Нам нужны всего лишь две окружности и базовая фигура, которую мы создали для пик.
Метод PaintClubs будет таков, как показано ниже.
void PaintClubs(Texture2D texture){ int radius=(int)(resolution*0.24f); //3 круга Vector2 mid=new Vector2(resolution*0.5f,resolution-radius); PaintCircle(texture,radius,mid,Color.black); mid=new Vector2(resolution*0.25f,resolution-(2.5f*radius)); PaintCircle(texture,radius,mid,Color.black); mid=new Vector2(resolution*0.75f,resolution-(2.5f*radius)); PaintCircle(texture,radius,mid,Color.black); //ножка radius=(int)(resolution*0.5f); float midY=resolution*0.42f; int stalkHeightJ=(int)(resolution*0.65f); float iValue; for (int i=0;i<resolution;i++){ for (int j=0;jmid.x+iValue){ mid=new Vector2(resolution*1.035f,midY); iValue=(Mathf.Sqrt(radius*radius-((j-mid.y)*(j-mid.y))));//+mid.x; if(i<mid.x-iValue){ texture.SetPixel(i, j, Color.black); } } } } }
Упаковка текстур
Если вы изучите исходные файлы Unity для этого проекта, вы найдете класс TextureManager, который выполняет всю тяжелую работу. После того как мы создали все необходимые текстуры, класс TextureManager использует метод PackTextures, чтобы объединить их в одну текстуру, тем самым уменьшая количество вызовов отрисовки, требуемых при использовании этих фигур.
Rect[] packedAssets=packedTexture.PackTextures(allGraphics,1);
Используя массив packAssets, мы можем получить ограничивающие рамки отдельных текстур из основной текстуры с именем packTexture.
public Rect GetTextureRectByName(string textureName){ textureName=textureName.ToLower(); int textureIndex; Rect textureRect=new Rect(0,0,0,0); if(textureDict.TryGetValue(textureName, out textureIndex)){ textureRect=ConvertUVToTextureCoordinates(packedAssets[textureIndex]); }else{ Debug.Log("no such texture "+textureName); } return textureRect; } private Rect ConvertUVToTextureCoordinates(Rect rect) { return new Rect(rect.x*packedTexture.width, rect.y*packedTexture.height, rect.width*packedTexture.width, rect.height*packedTexture.height ); }
Заключение
Создав все необходимые компоненты, мы можем приступить к созданию нашей колоды карт, так как это просто вопрос правильного расположения фигур. Для составления карт мы можем использовать пользовательский интерфейс Unity, или можно создавать карты в виде отдельных текстур. Вы можете изучить пример кода, чтобы понять, как для создания макетов карт я использовал первый из способов.
Мы можем использовать этот же метод для создания любого динамического арта во время выполнения в Unity. Создание графики во время выполнения – это операция, требующая высокой производительности, но ее можно произвести только один раз, если мы эффективно сохраним и повторно используем эти текстуры. Путем упаковки динамически созданных ресурсов в одиночную текстуру, мы также получаем преимущества от использования атласа текстур.
Теперь, когда у нас есть колода игральных карт, дайте мне знать, какие игры вы планируете создать с ее помощью.