Как создать Minecraft на Python? Обзор библиотеки Ursina Engine
- четверг, 8 декабря 2022 г. в 00:43:12
Пояснения внутри кодовых вставок — неотъемлемая часть статьи. Обращайтесь к ним, если что-то непонятно. На более сложные вопросы по работе с библиотекой могу ответить в комментариях под текстом.
from ursina import *
app = Ursina()
# здесь будет описана игровая логика
app.run()
Дисклеймер: Все, что описано в этом подразделе, лучше использовать для сцен с небольшим количеством объектов. Для более сложных полигонов генерацию нужно оптимизировать — об этом подробнее в следующих разделах. Но пропускать этот блок не советую: здесь объясняю принцип работы с ключевыми элементами библиотеки.
...
app = Ursina()
# создаем объекты модели cube, с текстурой white_cube и заданными координатами
for x in range(16):
for z in range(16):
Entity(model="cube", texture="white_cube", position=Vec3(x,0,z))
app.run()
# импортируем объект
from ursina.prefabs.first_person_controller import FirstPersonController
...
# добавляем персонажа
player = FirstPersonController()
# активируем невесомость, чтобы персонаж не упал в пустоту
player.gravity = 0.0
app.run()
...
По умолчанию персонажем можно управлять с помощью мышки и кнопок W, A, S, D. Но есть «фича»: если переключиться на русскую раскладку, то при нажатии кнопки сработает исключение TypeError. Поэтому лучше добавить автоматическое переключение раскладки при запуске программы — например, с помощью win32api.
...
# загружаем текстуру
grass_texture = load_texture('assets/grass.png')
for x_dynamic in range(16):
for z_dynamic in range(16):
# настраиваем объект Entity, загружаем модель block.obj
Entity(model='assets/block', scale=0.5, texture=grass_texture, position=Vec3(x_dynamic,0,z_dynamic))
...
...
# загружаем текстуру руки
arm_texture = load_texture('assets/arm_texture.png')
# объявляем объект hand, привязываем к камере camera.ui, загружаем модель и размещаем ее в правом нижнем углу
hand = Entity(parent = camera.ui, model = 'assets/arm',
texture = arm_texture, scale = 0.2,
rotation = Vec3(150, -10,0), position = Vec2(0.5,-0.6))
...
...
sky_texture = load_texture('assets/sky_texture.png')
sky = Entity(
model = 'sphere', texture = sky_texture,
scale = 1000, double_sided = True
)
...
Для создания перехода изо дня в ночь можно использовать функцию update. Она в параллельном потоке программы способна отслеживать время, координаты и другие параметры, а также — модифицировать объекты «на лету».
... def update(): print(player.x, player.y, player.z) ...
Пример: функция параллельного вывода координат.
Результат: программа отслеживает актуальные координаты.
...
def input(key):
if key == 'o': # кнопка выхода из игры
quit()
if key == 'shift': # кнопка быстрого бега
global shift_click
if shift_click % 2 == 0:
player.speed = normal_speed + 3 # увеличиваем скорость при нажатии
shift_click += 1
else:
player.speed = normal_speed
shift_click += 1
...
...
# создаем новый класс на базе Button и задаем стартовые параметры
class Voxel(Button):
def __init__(self, position=(0, 0, 0), texture=grass_texture):
super().__init__(
parent=scene, model='assets/block',
scale=0.5, texture=texture, position=position,
origin_y=0.5, color = color.color(0,0,random.uniform(0.9,1))
)
# добавляем input — встроенную функцию взаимодействия с блоком Voxel:
# если нажали на ПКМ — появится блок
# если нажали на ЛКМ — удалится
def input(self, key):
if self.hovered:
if key == 'right mouse down':
Voxel(position=self.position + mouse.normal, texture=texture)
if key == 'left mouse down':
destroy(self)
# генерация платформы из блоков Voxel
for x_dynamic in range(16):
for z_dynamic in range(16):
Voxel(position=(x_dynamic,0,z_dynamic))
...
Арендуйте выделенный сервер с запуском от 2 минут и бесплатной заменой комплектующих. И используйте его ресурсы для гейминга.
Я бы не написал этот раздел, если бы не канал Red Hen dev. К слову, на нем уже больше года выходят видео по Ursina Engine. Сегодня это лучшая неофициальная документация. Поэтому если вы хотите углубиться, например, в процедурную генерацию мира из Mesh-блоков, переходите по ссылке.
...
# создаем объект Mesh
e = Entity(model=Mesh(), texture=this.textureAtlas)
# подгружаем конкретную ячейку из атласа текстур (с помощью масштабирования)
# атлас текстур — это обычное изображение, в котором собраны текстуры разных блоков
e.texture_scale *= 64/e.texture.width
def genBlock(this, x, y, z):
model = this.subsets[0].model
uu = 8
uv = 7
model.uvs.extend([Vec2(uu, uv) + u for u in this.block.uvs])
def genTerrain(this):
x = 0
z = 0
y = 0
o_width = int(this.subWidth*0.5)
for x_dynamic in range(-o_width, o_width):
for z_dynamic in range(-o_width, o_width):
# обращаемся к genBlock(), генерируем блоки типа Mesh
this.genBlock(x+x_dynamic, y, z+z_dynamic)
this.subsets[0].model.generate()
...
from numpy import floor
from perlin_noise import PerlinNoise
import matplotlib.pyplot as plt
noise = PerlinNoise(octaves=2, seed=4522)
amp = 6
freq = 24
terrain_width = 300
landscale = [[0 for i in range(terrain_width)] for i in range(terrain_width)]
for position in range(terrain_width**2):
x = floor(position / terrain_width)
z = floor(position % terrain_width)
y = floor(noise([x/freq, z/freq])*amp)
landscale[int(x)][int(z)] = int(y)
plt.imshow(landscale)
plt.show()
...
for x_dynamic in range(-o_width, o_width):
for z_dynamic in range(-o_width, o_width):
# генерация Mesh-блока в заданной точке, координату y берем из алгоритма Перлина
this.genBlock(x+x_dynamic, this.landscale[x+x_dynamic][z+z_dynamic], z+z_dynamic)
...
Подобным образом возможно генерировать не только ландшафты, но и, например, шахты. Только в последнем случае используются черви Перлина. Подробнее о разных алгоритмах и принципах генерации мира можно узнать тут.
from ursina.shaders import basic_lighting_shader
...
e = Entity(..., shader = basic_lighting_shader)
...
...
scene.fog_density=(0,95)
# scene, как и window, тоже один из основных элементов библиотеки. Иногда его можно встретить в параметре наследования parent. Хотя, по моему опыту, его использование скорее опционально, чем обязательно.
scene.fog_color=color.white
...
# импортируем основные объекты. Предварительно нужно развернуть репозиторий UrsinaLighting внутри своего проекта.
from UrsinaLighting import LitObject, LitInit
...
# важно! нужно инициализировать главный объект.
lit = LitInit()
...
# заполняем нижние уровни ландшафта водой (y = -1.1), создаем текстуру воды размером с ширину ландшафта. Проседать FPS не будет, тк water — это один объект, который просто «растянут» вдоль игровой сцены
water = LitObject(position = (floor(terrain.subWidth/2), -1.1, floor(terrain.subWidth/2)), scale = terrain.subWidth, water = True, cubemapIntensity = 0.75, collider='box', texture_scale=(terrain.subWidth, terrain.subWidth), ambientStrength = 0.5)
...
...
punch_sound = Audio('assets/punch_sound',loop = False, autoplay = False)
...
class Voxel(Button):
...
def input(key):
if key == 'left mouse down':
punch_sound.play()
...
Возможно, эти тексты тоже вас заинтересуют:
→ Как запустить динозаврика Google на тачбаре? Обзор Python-библиотеки PyTouchBar
→ Каким должен быть Feature Store, чтобы оптимизировать работу с ML-моделями
→ 7 полезных книг по Python для старта и развития
# в отдельном файле menu.py
from ursina import *
app = Ursina(title='Minecraft-Menu')
# создаем объект на базе Entity, настраиваем камеру и бэкграунд
class MenuMenu(Entity):
def __init__(self, **kwargs):
super().__init__(parent=camera.ui, ignore_paused=True)
self.main_menu = Entity(parent=self, enabled=True)
self.background = Sky(model = "cube", double_sided = True, texture = Texture("textures/skybox.jpg"), rotation = (0, 90, 0))
# стартовая надпись Minecraft
Text("Minecraft", parent = self.main_menu, y=0.4, x=0, origin=(0,0))
def switch(menu1, menu2):
menu1.enable()
menu2.disable()
# вместо print_on_screen можно вписать lambda-функцию для запуска игры
ButtonList(button_dict={
"Start": Func(print_on_screen,"You clicked on Start button!", position=(0,.2), origin=(0,0)),
"Exit": Func(lambda: application.quit())
},y=0,parent=self.main_menu)
main_menu = MenuMenu()
app.run()
Код из статьи доступен на GitHub. Делайте «форк» и используйте его в качестве референса, а также предлагайте свои улучшения.
Что думаете по поводу этой библиотеки вы? Поделитесь мнением в комментариях.