Полноценная игра, сделанная мною в обычной windows консоли

Привет!


Сегодня я опишу в подробностях, как я сделал игру в командной строке, и насколько хороша она получилась.

Откуда идея?


Я вдохновился идеей сделать что-то простое на первый взгляд, но в тоже время интересное в плане разработки. Мне в голову пришла мысль, сделать игру в консоли, это интересно в плане разработки, и посмотреть на это будет интересно даже просто со стороны, как например на эту игру.

Игровой движок


Итак, начнем с того как игра устроена в корне, и какова ее идея работы.

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

Вот таким кодом:

for line_words in OUTPUT_IMAGE:
       for word in line_words:
           print(word, end="")
       print("\n", end="")

Здесь мы рисуем все символы из списка, и переходим на новую строку, чтоб нарисовать следующий список символов.

Вот так выглядит переменная, которая хранит списки символов:

image

Тут cразу мы получаем решение как нам выводить по X и Y объекты, мы теперь можем указывать:

X — символ в списке
Y — список в котором содержится X
Тем самым нарисовать на поле какой-нибудь символ. Это мы будем использовать при рисовании игровых объектов.

Можем попробовать нарисовать на поле «мяч», подставив на место X и Y букву «O».

Для этого напишем такой код:

import os
OUTPUT_IMAGE = [
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        ]

OUTPUT_IMAGE[4][6] = "O"
os.system("cls||clear")
for line_words in OUTPUT_IMAGE:
       for word in line_words:
           print(word, end="")
       print("\n", end="")


image

И вот, мы нарисовали объект на нашем игровом поле. Правда координаты X и Y получились не классическими. Во первых, мы указываем сначала Y, потом X, что не совсем по классике, во вторых, координата Y должна увеличиваться чтоб поднять объект, у нас же наоборот, она должна уменьшатся.

График X и Y в игре:

image

Эту особенность тоже придется учитывать в дальнейшем, когда мы будем делать столкновение объектов в консоли.

Теперь мы можем попробовать перемещать наш объект по игровому полю, т.е. создавать движение.

Нам понадобится очищать консоль, для того чтобы стирать старую картинку игрового поля.
Это мы сделаем командой:

os.system("cls||clear")

Также, нам понадобится переопределять переменную OUTPUT_IMAGE, для того чтобы очищать все ранее нарисованные в игровом поле объекты.

Также нам все это нужно будет поместить в while True.

Добавим в while True функцию time.sleep(1), для того чтобы ограничить FPS.

И вот, код нарисовался на глазах:

from time import sleep
from os import system
OUTPUT_IMAGE = [
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        ]

x = 0
y = 0
while True:
      sleep(1)
      system("cls||clear")
      OUTPUT_IMAGE[y][x] = "O"
      for line_words in OUTPUT_IMAGE:
             for word in line_words:
                 print(word, end="")
             print("\n", end="")
      y += 1
      x += 1
      OUTPUT_IMAGE = [
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            ]

image

Теперь у нас есть возможность распределять объекты по полю.

Правда эти объекты слишком примитивы, и надо бы научится рисовать сложные объекты по типу игроков, домов, еды…

Для того чтобы нарисовать сложный объект, нам нужно понять и придумать, как нарисовать объект указав лишь один раз его X и Y.

Для этого нам понадобится функция, которая принимает картинку (символы), X, Y;

Так и сделаем:

def SetImage(image: str, x: int, y: int):
    pass

Теперь нам нужно ее реализовать. Для этого нужно решить, как нарисовать изображение, которое растягивается по оси X и Y, я придумал так:
рисовать объект разделяя его на символы, и как только встретится символ "\n", прибавить ось Y.

Ось Y как мы уже говорили неправильная, перевернутая наоборот, поэтому к ней мы прибавляем чтобы опустить объект.

Пример изображения который рисуется по моему принципу:

image = " O\n'|'\n |"#игрок

Теперь давайте это опишем в нашей функции:

def SetImage(x: int, y: int, image: str):
  x_start = x
  x = x
  y = y
  for word in image:
      if word == "\n":
          x = x_start
          y += 1
      else:
          x += 1
          try:
            OUTPUT_IMAGE[y][x] = word
          except IndexError:
              break

Добавим try: except() для того чтобы небыло ошибок если объект имеет X и Y слишком мальенькие или слишком большие.

x_start Это X, с которого нужно начинать рисовать при увеличении Y (при символе "\n")

Теперь мы можем использовать нашу функцию, падать в нее X и Y, и картинку которую нужно рисовать:

код
from time import sleep
from os import system
OUTPUT_IMAGE = [
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      ]

def SetImage(x: int, y: int, image: str):
  x_start = x
  x = x
  y = y
  for word in image:
      if word == "\n":
          x = x_start
          y += 1
      else:
          x += 1
          try:
            OUTPUT_IMAGE[y][x] = word
          except IndexError:
              break
while True:
      sleep(1)
      system("cls||clear")
      SetImage(x=3,y=4,image=" O\n'|'\n |")
      for line_words in OUTPUT_IMAGE:
             for word in line_words:
                 print(word, end="")
             print("\n", end="")
      OUTPUT_IMAGE = [
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            ]


И вот что у нас получилось:

image

абсолютно также как и шарик который мы рисовали, его можно двигать по оси X и Y.

код
from time import sleep
from os import system
OUTPUT_IMAGE = [
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      ]
px = 0
py = 0
def SetImage(x: int, y: int, image: str):
  x_start = x
  x = x
  y = y
  for word in image:
      if word == "\n":
          x = x_start
          y += 1
      else:
          x += 1
          try:
            OUTPUT_IMAGE[y][x] = word
          except IndexError:
              break
while True:
      sleep(1)
      system("cls||clear")
      SetImage(x=px,y=py,image=" O\n'|'\n |")
      for line_words in OUTPUT_IMAGE:
             for word in line_words:
                 print(word, end="")
             print("\n", end="")
      px += 1
      py += 1
      OUTPUT_IMAGE = [
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            ]


image

И вот, у нас уже двигает игрок по карте.

Тут мы сделали уже многое, уже есть игрок, уже есть карта, и казалось бы, уже можно сделать игру, но нет. Нам нужна функция высчета столкновений объектов, ведь какая это игра без взаимодействий объектов. Поэтому приступим.

Для начала нам нужно сделать функцию получение широты и высоты объекта, для того чтобы расчитать его хитбокс.

Итак, функцию я решил сделать по такой логике:

X — хитбокс объекта по X ширине, это самое больше количество символов между знаками "\n" в картинке
Y — хитбокс по Y это число символов "\n" в картинке

По этой логике не сложно сделать функцию, которая принимает картинку, считает у нее все символы между "\n", и выбирает из этого самое больше число символов — получилась широта.
И если посчитать символы "\n", как я уже написал — получится высота.

Функция получилась такой:

def GetSizeObject(img: str):
    w = 0
    weights = []
    h = [word for word in img if word == "\n"]

    for word in img:
      if word == "\n":
          weights.append(w)
          w = 0
      else:
          w += 1
      try:
          return {"w": max(weights), "h":len(h)}
      except ValueError:
            return {"w": 0, "h":0}


Зачем здесь ValueError except?
Он здесь чтобы предотвратить ошибку при запуске игры.

Итак, давайте нарисуем нашего игрока, и вычислил его ширину и длину.

код с рисовкой и вычислением широты и высоты
from time import sleep
from os import system
OUTPUT_IMAGE = [
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      ]
px = 3
py = 3
def SetImage(x: int, y: int, image: str):
    global OUTPUT_IMAGE      
    x_start = x
    x = x
    y = y
    for word in image:
        if word == "\n":
            x = x_start
            y += 1
        else:
            x += 1
            try:
              OUTPUT_IMAGE[y][x] = word
            except IndexError:
                break

def GetSizeObject(img: str):
    w = 0
    weights = []
    h = [word for word in img if word == "\n"]
    h.append(1)

    for word in img:
        if word == "\n":
            weights.append(w)
            w = 0
        else:
            w += 1
    try:
        return {"w": max(weights), "h":len(h)}
    except ValueError:
        return {"w": 0, "h":0}

player_image = " O\n'|'\n |"
def draw():
      global OUTPUT_IMAGE
      sleep(1)
      system("cls||clear")
      for line_words in OUTPUT_IMAGE:
             for word in line_words:
                 print(word, end="")
             print("\n", end="")
      OUTPUT_IMAGE = [
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            ]
while True:
    SetImage(x=px,y=py,image=player_image)
    print(GetSizeObject(img=player_image))
    draw()



Ура! у нас есть функция вычисления широты и высоты, теперь нам предстоит сделать функцию вычисления хитбокса и столкновений объектов.

Вспомним что у нас система координат не классическая, поэтому классическую функцию увы использовать не сможем, придется делать свою. Я для этого я нарисовал на графике 2 квадрата, которые сталкиваются, и по этой картинке можно придумать условие по которому будет высчитано столкновение

Для простоты понимания я нарисовал хитбоксы, Т.Е. квадраты:

image

Логика на словах
Для вычисления мы подаем

x — X первого объекта
y — Y первого объекта
h — Высота первого объекта
w — Широта первого объекта
x2 — X второго объекта
y2 — Y второго объекта
h2 — Высота второго объекта
w2 — Широта второго объекта

И смотрим:

если
y больше y2 - h2 + h и y - h меньше чем y2 + h2 - h
или же
y2 больше y - h + h2 и y2 - h2 меньше чем y + h - h2
Зачем проверять 2 раза?
Мы сделали проверку 2 раза, просто из-за того чтобы посмотреть на столкновение/не столкновение с разных объектов.

Объекты соприкасаются по оси Y

Дальше смотри соприкосновение по оси X, она такое же что и по оси Y, но вместо yx, а вместо hw.

если:

x больше x2 - w2 + w и x - w меньше чем x2 + w2 - w

или же

x2 больше x - w + w2 и x2 - w2 меньше чем x + w - w2

объекты соприкасаются по оси X

Логика в коде
Логика такая же как и на словах, только в функции:

def IsClash(x: int, y: int, h: int, w: int,x2: int, y2: int, h2: int, w2: int):
    if (y >= y2 - h2 + h and y - h <= y2 + h2 - h) or (y2 >= y - h + h2 and y2 - h2 <= y + h - h2):
        if (x >= x2 - w2 + w and x - w <= x2 + w2 - w) or (x2 >= x - w + w2 and x2 - w2 <= x + w - w2):
            return True

    return False

Функция возвращает True если объекты соприкасаются, и False если нет.

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

И попробовал как работает функция высчета столкновения.

Вот игрок соприкасается и кубом:

image

А вот нет соприкасаются:

image

Код соприкосновения
Это полный код соприкосновения/не соприкосновения:

from time import sleep
from os import system
OUTPUT_IMAGE = [
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      ]

def SetImage(x: int, y: int, image: str):
    global OUTPUT_IMAGE      
    x_start = x
    x = x
    y = y
    for word in image:
        if word == "\n":
            x = x_start
            y += 1
        else:
            x += 1
            try:
              OUTPUT_IMAGE[y][x] = word
            except IndexError:
                break

def GetSizeObject(img: str):
    w = 0
    weights = []
    h = [word for word in img if word == "\n"]
    h.append(1)

    for word in img:
        if word == "\n":
            weights.append(w)
            w = 0
        else:
            w += 1
    try:
        return {"w": max(weights), "h":len(h)}
    except ValueError:
        return {"w": 0, "h":0}

def IsClash(x: int, y: int, h: int, w: int,x2: int, y2: int, h2: int, w2: int):
    if (y >= y2 - h2 + h and y - h <= y2 + h2 - h) or (y2 >= y - h + h2 and y2 - h2 <= y + h - h2):
        if (x >= x2 - w2 + w and x - w <= x2 + w2 - w) or (x2 >= x - w + w2 and x2 - w2 <= x + w - w2):
            return True

    return False

player_image = " O\n'|'\n |"
cube_image = "____\n|  |\n----"
cx = 5#
cy = 4  #Меняйте эти координаты для того чтобы менять позиции игрока и куба
px = 10  #
py = 3#
def draw():
      global OUTPUT_IMAGE
      sleep(1)
      system("cls||clear")
      for line_words in OUTPUT_IMAGE:
             for word in line_words:
                 print(word, end="")
             print("\n", end="")
      OUTPUT_IMAGE = [
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            ]
while True:
    SetImage(x=px,y=py,image=player_image)
    SetImage(x=cx,y=cy,image=cube_image)
    print("is clash: ",IsClash(
      x=px,
      x2=cx,
      y=py,
      y2=cy,
      h=GetSizeObject(img=player_image)["h"],
      h2=GetSizeObject(img=cube_image)["h"],
      w=GetSizeObject(img=player_image)["w"],
      w2=GetSizeObject(img=cube_image)["w"],
      ))
    draw()



Теперь у нас все стартовые функции для игры, собственно их основе я писал свою игру.

Игра


Идея игры в такая:

Есть игрок, вокруг появляется еда, которую он вынужден собрать чтоб не умереть. В игре также присутствуют функции: поднять еду, положить в инвентарь, съесть ее из инвентаря, положить на пол предмет из инвентаря

Я начал с того что сделал игровой цикл в 3 строчки, это просто While True:

from time import sleep
while True:
    sleep(0.1)

Дальше я посчитал нужным, создать класс, в котором будут хранится все функции будующий объектов. Поэтому создал файл main.py и папку lib, в которую поместил файл lib.py в котором был класс игры. Т.Е. файлы игры выглядели так:

+----game
|    + -- 
|    | -- main.py
|    \ --lib
|         +--lib.py -> class Game()
|         \
|
+---

В дальнейшем я работал в основном с классом Game(), в main.py просто вызывал его, создавал стартовые объекты, запускал игру.

В классе game сделал функцию run(), которая заупускает игровой цикл. Также сделал функцию draw_all(), она стирает все прошлые объекты, рисует новые, и печатает на игровое поле.

И так выглядел класс:

from time import sleep


class Game():
    def __init__(self):
        self.OUTPUT_IMAGE = []  # здесь игровое поле

    def draw_all(self):
        for line_words in self.OUTPUT_IMAGE:
            for word in line_words:
                print(word, end="")
            print("\n", end="")

    def run(self):
        while True:
            self.draw_all()
            sleep(0.1)


Добавил все основные функции, по типу set_image(), size_object(), is_clash(), и все те которые являются игровым движком, и которые я описал выше.

Сделал новую функцию create_object() и переменную self.OBJECTS, функцию create_object()я использую для создания объектов, она принимает параметры img, name, x, y, up, rigid, data.

img — картинка объекта
name — имя объекта (дом, трава, житель, еда и.т.п.)
x — X объекта
y — Y объекта
up — если этот параметр True, то объект рисуется над игроком, иначе игрок его перекрывает собой
rigid — твердость, игрок не может пройти через этот объект (еще не реализовано)
data — личные данные объекта, его личные характеристики

create_object()
Эта функцию которая сейчас у меня в игре:

def CreateObject(self,x: int, y: int, img: str, name: str = None, up: bool = False, rigid: bool = False, data: dict = {}):
    size_object = self.GetSizeObject(img=img)
    self.OBJECTS.append(
        {"name": name,
         "x": x,
         "y": y,
         "up": up,
         "rigid": rigid,
         "h":size_object["h"],
         "w":size_object["w"],
         "id":uuid4().hex,
         "data":data,
         "img": img}
    )


На тот момент я уже добавил игрока, дом, траву, и жителя.

И решил использовать тот самый параметр в объекте up, использовать его в объекте Home, Т.Е. чтоб дом закрывал собой игрока. Для этого я сделал функцию CheckAll(), циклом for проходился по всем объектам, и рисовал их на исходящей картинке, Т.Е. использовать функцию SetImage(x: int, y: int, img:str), подавая в нее X и Y объекта, и картинку.

Тем самым рисовал объекты которые мог закрыть собой игрок. В этом же цикле я объявил списокup_of_payer_objects, и если у объекта стоял up=True, то я добавлял его в список, не рисуя его на поле. После рисовал самого игрока, и только этого я проходил циклом for по объектам в up_of_payer_objects, рисуя их, тем самым они были над игроком.

def CheckAll(self):
    up_of_payer_objects = []
    for object_now in range(len(self.OBJECTS)):
        if object_now["up"]:
            up_of_payer_objects.append(object_now)
            continue
        self.SetImage(x=object_now["x"],y=object_now["y"],image=object_now["img"])

Дальше я занялся движением игрока. Для этого я создал его как отдельный объект, который не находится в списке self.OBJECTS, но который хранится в переменной self.PLAYER.

Все его параметры, по типу X, Y, img, и.т.п. получить можно с помощью ключей, проще говоря это словарь (dict). С таким игроком и объектами уже можно было работать, двигать, вычислить столкновения. Я начал с движения.
Начал создавать движение с того что сделал функцию CheckKeysObjects(), которая отвечает за отслеживание нажатия клавиш, и которую я вызываю в функции CheckAll() в самом начале

def CheckAll(self):
    self.CheckKeysObjects()
    ....

Для отслеживания нажатий на клавиши я использовал библиотеку keyboard, и 4 переменные:

self.WALK_LEFT_PLAYER
self.WALK_RIGHT_PLAYER
self.WALK_UP_PLAYER
self.WALK_DOWN_PLAYER

И все оказалось просто, отслеживаем клавиши, и если нажата допустим d, то мы переменную self.WALK_RIGHT_PLAYER делаем True.

В самом начале функции объявляем все переменные в False, для того чтобы сбросить все прошлые результаты, а-то игрок не остановится.

CheckKeysObjects()
def CheckKeysObjects(self):
    #делаю все переменные в False, чтоб сбросить прошлые результаты
    self.WALK_LEFT_PLAYER = False
    self.WALK_RIGHT_PLAYER = False
    self.WALK_UP_PLAYER = False
    self.WALK_DOWN_PLAYER = False
    #а тут уже проверяю нажатия
    if keyboard.is_pressed("a"):
        self.WALK_LEFT_PLAYER = True
    elif keyboard.is_pressed("d"):
        self.WALK_RIGHT_PLAYER = True
    if keyboard.is_pressed("w"):
        self.WALK_UP_PLAYER = True
    elif keyboard.is_pressed("s"):
        self.WALK_DOWN_PLAYER = True


После этого я в функции CheckAll() проверяю все перменные отвечающие за движение, узнаю, куда двигается игрок.

Если какая-то в True, узнаем какая, и двигаем предмет в противоположную сторону.

Получившийся код движения
def CheckAll(self):
    self.CheckKeysObjects()  # check moves
    up_of_payer_objects = []
    for object_now in range(len(self.OBJECTS)):
        self.PLAYER["img"] = self.PLAYER["image_normal"]
        if self.WALK_LEFT_PLAYER:
            self.OBJECTS[object_now]["x"] += 1

        elif self.WALK_RIGHT_PLAYER:
            self.OBJECTS[object_now]["x"] -= 1


        if self.WALK_UP_PLAYER:

            self.OBJECTS[object_now]["y"] += 1
        elif self.WALK_DOWN_PLAYER:

            self.OBJECTS[object_now]["y"] -= 1


Да, мы двигаем предметы в противоположную сторону, для того чтобы создать иллюзию движения. Если игрок идет на право, то все предметы окружения смещаются налево.

Дальше я добавил еще предметов окружения, и занялся спавном еды, у игрока цель собирать еду, чтобы не умереть.

Для отсчета времени спавна еды, я использовал простой time.sleep(), и библиотеку threading — для того чтобы запустить 2 функции одновременно, спавн еды и основной игровой цикл. Функция спавна еды SpawnEat() — это просто функция которая при запуске генерирует на случайных местах еду, вызывая для каждой единицы еды функцию CreateObject().

Также, как только я сделал функцию спавна еды, я сделал переменную у игрока self.PLAYER["hungry"], это его голод, в самом начале он равен — 100 ед., его я буду уменьшать если игрок ходит и тратит энегрию (типа энергию, ее в игре нет) или увеличивать если игрок что-то съел.

Также я сделал функцию MinimizeHungry(), она вызывается каждые 5 секунд, и просто отнимает у игрока 2 единицы голода. Это я сделал для того чтобы игроку пришлось двигаться, а не стоять на месте.

И наконец в функции Eat(), эта функция которая вызывается в отдельном потоке от игрового цикла. Она проверяет не слишком ли много еды на карте, если еды больше 10 ед. то НЕ вызывает функцию SpawnEat(), если меньше 10 ед. то вызывает SpawnEat().

Вот какой она получилась:

Eat()
def Eat(self):
    while True:
        sleep(4)
        if len([i for i in self.OBJECTS if i["name"] == "meat"]) < 10:
            self.SpawnEat()
        sleep(1)
        self.MinimizeHungry()


Функция Start(), для запуска основного цикла:

Start()
def Start(self):
    while True:  
        self.CheckAll()
        self.DrawAll()
        sleep(0.01)


И функция run(), которая запускает всю игру.

run()
def run(self):
    proc1 = threading.Thread(target=self.Start)
    proc1.start()
    proc2 = threading.Thread(target=self.Eat)
    proc2.start()


Сам процесс поедания, я реализовал просто в функции CheckAll() и CheckKeysObjects(). В CheckKeysObjects() я проверял не нажал ли игрок на кнопку E. Если нажал, то ставил переменную self.PRESS_E в True.

В цикле CheckAll(), проверял, не еда ли нынешний объект в цикле for, если еда то проверял не сталкивается ли с ним игрок, если сталкивается то проверял переменную self.PRESS_E, и если она в True то тогда просто удалял объект, и увеличивал голод, Т.Е. переменную self.PLAYER["hungry"].

Вот так это в коде
for object_now in range(len(self.OBJECTS)):
    ....
    if self.OBJECTS[object_now]["name"] == "meat":
        items_objects.append(object_now)
        is_clash = self.IsClash(
            x=self.OBJECTS[object_now]["x"],
            y=self.OBJECTS[object_now]["y"],
            h=self.OBJECTS[object_now]["h"],
            w=self.OBJECTS[object_now]["w"],
            x2=self.PLAYER["x"],
            y2=self.PLAYER["y"],
            h2=self.PLAYER["h"],
            w2=self.PLAYER["w"],
        )

        if is_clash:
            if self.PRESS_E:
                try:
                    self.PLAYER["hungry"] += self.HUNGRUY_ADD
                    del self.OBJECTS[object_now]
                    break

                except IndexError:
                    pass


Скажу наперед, это все мне нужно будет переписовать, когда я буду делать инветнарь

Делаю инвентарь


Итак, настало сложное, нам нужно сделать инвентарь.

Сложность в том что все предметы нужно будет отображать, хранить историю, удалять, ставить на пол объекты.

Я начал с того что добавил игроку новый ключ, это был self.PLAYER["inventory"], там хранятся 4 яцчейки, вот в таком виде:

"inventory":{
    "0":{"status":"space","name":"#0", "minimize_image":"#0"},
    "1":{"status":"space","name":"#1", "minimize_image":"#1"},
    "2":{"status":"space","name":"#2", "minimize_image":"#2"},
    "3":{"status":"space","name":"#3", "minimize_image":"#3"},
}

цифры — просто номера ячеек.

status — этот ключ хранит в себе значение, пуста яйчейка или нет. Если пуста то «space», если же там есть предмет, то там хранится имя предмета.

name — хранит в себе имя предмета, оно будет использовано когда игрок будет класть предмет.

minimize_image — эта уменьшенная картинка предмета которая изображается в инвентаре игрока.

После, сделал новые проверки в нашем CheckKeysObjects(), при нажатии на X предмет будет бросаться на землю, и также при нажатии на кнопку E будет вызываться функция self.UseEat(), которую мы сейчас будем разбирать.

Итак, функция self.UseEat() представляет из себя проход по всем ячейкам инвентаря, в поисках еды, и если еда найдена, то она удаляется из инвентаря, и к голоду добавляется 10 единиц. Для удаление предмета из инвентаря я сделал функцию self.DestroyItem(), в которую подается индекс ячейки, и вся ячейкой просто становится по «дефолту» пустой и без ничего.

self.DestroyItem()
def DestroyItem(self,index_item: str):
    item = self.PLAYER["inventory"][index_item]
    self.PLAYER["inventory"][index_item] = self.PLAYER["default_inventory_item"](index_item)
    self.PLAYER["inventory_must_update"] = True
    return item


self.CheckKeysObjects()
def CheckKeysObjects(self):
    self.WALK_LEFT_PLAYER = False
    self.WALK_RIGHT_PLAYER = False
    self.WALK_UP_PLAYER = False
    self.WALK_DOWN_PLAYER = False
    if key("a"):
        self.WALK_LEFT_PLAYER = True
    elif key("d"):
        self.WALK_RIGHT_PLAYER = True
    if key("w"):
        self.WALK_UP_PLAYER = True
    elif key("s"):
        self.WALK_DOWN_PLAYER = True
    if key("f"):
        self.KEY_F = True
    else:
        self.KEY_F= False
    if key("e"):
        self.UseEat()


self.UseEat()
def UseEat(self):
    for inventory_item in range(len(self.PLAYER["inventory"])):
        if self.PLAYER["inventory"][str(inventory_item)]["name"] == "meat":
            if self.PLAYER["hungry"] + self.ADD_HUNGRY_COUNT < 100.0:
                self.PLAYER["hungry"] += self.ADD_HUNGRY_COUNT
                self.DestroyItem(index_item=str(inventory_item))


Дальше функция бросания предмета на землю.

Там впрочем ничего сложного, при нажатии на X вызывается функция self.QuitItem(), в ней проходит цикл for по всем ячейкам инвентаря, и если ключ ["status"] не ровняется "space", то эту ячейку удаляем с помощью ранее рассмотренной функции self.DestroyItem(), и создаем объект на основе того что был в ячейке, X и Y ставит игрока, как бы он бросил его возле себя.

self.Quititem()
def QuitItem(self):
    for inventory_item in range(len(self.PLAYER["inventory"])):
        if self.PLAYER["inventory"][str(inventory_item)]["status"] != "space":
            self.CreateObject(
                img=self.PLAYER["inventory"][str(inventory_item)]["img"],
                x=self.PLAYER["x"],
                y=self.PLAYER["y"],
                name=self.PLAYER["inventory"][str(inventory_item)]["name"],
                data=self.PLAYER["inventory"][str(inventory_item)]["data"],
            )
            self.DestroyItem(index_item=str(inventory_item))
            break


И впрочем все, многие вещи я не говорил как сделал, Т.К. они являлись не основной частью игры, хоть и интересной. Например сообщения о возможности поднятие предмета или нет (когда инвентарь полон), о том что я добавил анимацию ходьбы, о том что сделал отдельную библиотеку картинок, и тому прочих вещах.

На этом все?


Нет, я собираюсь в игру добавить нейросеть, используя библиотеку который я писал на Python,
Собираюсь сделать взаимодействие игрока с NPC оснащенными нейросетью,
небольшой, но какой-нибудь сюжет, и также какие-то припасы для игрока типа брони, еды. предметов, возможность строить блоками.

Попробовать игру


Ее можно свободно скачать с моего GitHub, для запуска потребуется лишь Python3, и библиотека keyboard. Запускать нужно файл main.py.

Игра

Комментарии 15

    +5

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

      0
      +. Засекать время вначале работы полезной нагрузки, потом засекать в конце. Ждать 1e3/fps — duration.
      –2
      Выглядит неплохо. Успехов в дальнейшей разработке
        0
        Есть же libtcod…
          +1
          Тааак, конкуренты появляются)
            0
            Почему бы не хранить строки в строках, а не в списке строк из одного символа?

            OUTPUT_IMAGE = [
                    "................",
                    "................",
            
            

              0

              В питоне строки являются неизменяемыми. В таком случае придется работать со срезами для замены пары символов, что может привести к ошибкам и увеличению времени работы

                0
                Уверен, что будет быстрее, чем сейчас.
                  0
                  Так хранить же (чтобы уровни задавать и паттерны), а обрабатывать можно и в массивах
                0

                Спасибо за идею, буду пытаться перенести на шарп

                  0
                  Интересный вариант, мне понравилось решение
                    0
                    Кто-нибудь, расскажите автору про curses (и его врапперы под Шindoшs)
                      +1
                      OUTPUT_IMAGE = [
                      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
                      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],

                      Питон не знаю, но там вроде множество замечательных функции, чтобы не писать подобные портянки, к примеру (псевдокод в данном случае) OUTPUT_IMAGE.append('.' * 15.split('.'))
                        +1

                        Мне кажется лучше вообще не хранить в буффере символы, а сделать два списка: один — с идентификаторами символов, другой — с маппингом этих идентификаторов на карту.
                        Карту желательно свернуть в одномерный список для простоты.

                          +1
                          Это зависит от процесса
                          Если ты — и геймдизайнер и программист и в коде хранишь уровни (простой и надежный вариант для таких простых игр, кстати), то удобно хранить как раз символы по строкам, потому что визуально видны уровни.
                          Казуальный Wysiwyg эдакий

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                      Самое читаемое