Время на прочтение
7 мин
Количество просмотров 23K
Добро пожаловать в самую запутанную архитектуру проекта. Да я умею писать вступление…
Попробуем сделать небольшую демку minecraft в браузере. Пригодятся знания JS и three.js.
Немного условностей. Я не претендую на звание лучшее приложение столетия. Это всего лишь моя реализация для данной задачи. Также есть видео версия для тех кому лень читать(там тот же смысл, но другими словами).
В конце статьи есть все нужные ссылки. Постараюсь как можно меньше воды в тексте. Объяснять работу каждой строки не буду. Вот теперь можно начать.
Для начала чтобы понимать какой будет итог, то вот демка игры.
Разделим статью на несколько частей:
- Структура проекта
- Игровой цикл
- Настройки игры
- Генерация карты
- Камера и управление
Структура проекта
Вот так выглядит структура проекта.
index.html — Расположение канваса, немного интерфейса и подключение стилей, скриптов.
style.css — Стили только для внешнего вида. Самое важное это кастомный курсор для игры который располагается в центре экрана.
texture — Здесь лежат текстуры для курсора и блока земли для игры.
core.js — Основной скрипт где происходит инициализация проекта.
perlin.js — Это библиотека для шума Перлина.
PointerLockControls.js — Камера от three.js.
controls.js — Управление камерой и игроком.
generationMap.js — Генерация мира.
three.module.js — Сам three.js в виде модуля.
settings.js — Настройки проекта.
index.html
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="style/style.css">
<title>Minecraft clone</title>
</head>
<body>
<canvas id="game" tabindex="1"></canvas>
<div class="game-info">
<div>
<span><b>WASD: </b>Передвижение</span>
<span><b>ЛКМ: </b> Поставить блок</span>
<span><b>ПКМ: </b> Удалить блок</span>
</div>
<hr>
<div id="debug">
<span><b></b></span>
</div>
</div>
<div id="cursor"></div>
<script src="scripts/perlin.js"></script>
<script src="scripts/core.js" type="module"></script>
</body>
</html>
style.css
body {
margin: 0px;
width: 100vw;
height: 100vh;
}
#game {
width: 100%;
height: 100%;
display: block;
}
#game:focus {
outline: none;
}
.game-info {
position: absolute;
left: 1em;
top: 1em;
padding: 1em;
background: rgba(0, 0, 0, 0.9);
color: white;
font-family: monospace;
pointer-events: none;
}
.game-info span {
display: block;
}
.game-info span b {
font-size: 18px;
}
#cursor {
width: 16px;
height: 16px;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-image: url("../texture/cursor.png");
background-repeat: no-repeat;
background-size: 100%;
filter: brightness(100);
}
Игровой цикл
В core.js нужно провести инициализацию three.js, настроить его и добавить все нужные модули от игры + обработчики событий… ну и игровой цикл запустить. В учет того, что все настройки стандартные, то объяснять их нет смысла. Поговорить можно про map (он принимает сцену игры для добавления блоков) и contorls т.к. он принимает несколько параметров. Первый это камера от three.js, сцену для добавления блоков и карту чтобы можно было взаимодействовать с ней. update отвечает за обновление камеры, GameLoop — игровой цикл, render- стандарт от three.js для обновления кадра, событие resize также стандарт для работы с канвасом (это реализация адаптива).
core.js
import * as THREE from './components/three.module.js';
import { PointerLockControls } from './components/PointerLockControls.js';
import { Map } from "./components/generationMap.js";
import { Controls } from "./components/controls.js";
// стандартные настройки three.js
const canvas = document.querySelector("#game");
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x00ffff);
scene.fog = new THREE.Fog(0x00ffff, 10, 650);
const renderer = new THREE.WebGLRenderer({canvas});
renderer.setSize(window.innerWidth, window.innerHeight);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(50, 40, 50);
// Создание карты
let mapWorld = new Map();
mapWorld.generation(scene);
let controls = new Controls( new PointerLockControls(camera, document.body), scene, mapWorld );
renderer.domElement.addEventListener( "keydown", (e)=>{ controls.inputKeydown(e); } );
renderer.domElement.addEventListener( "keyup", (e)=>{ controls.inputKeyup(e); } );
document.body.addEventListener( "click", (e) => { controls.onClick(e); }, false );
function update(){
// передвижение/камера
controls.update();
};
GameLoop();
// Игровой цикл
function GameLoop() {
update();
render();
requestAnimationFrame(GameLoop);
}
// Рендер сцены(1 кадра)
function render(){
renderer.render(scene, camera);
}
// обновление размера игры
window.addEventListener("resize", function() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
Настройки
В настройки можно было вынести и другие параметры, например, настройки three.js, но я сделал без них и сейчас здесь лишь пара параметров отвечающие за размер блоков.
settings.js
export class Settings {
constructor() {
// площадь блока
this.blockSquare = 5;
// размер и площадь чанка
this.chunkSize = 16;
this.chunkSquare = this.chunkSize * this.chunkSize;
}
}
Генерация карты
В классе Map у нас есть несколько свойство которые отвечают за кеш материалов и параметры для шума Перлина. В методе generation мы загружаем текстуры, создаем геометрию и меш. noise.seed отвечает за стартовое зерно для генерации карты. Можно рандом заменить на статичное значение чтобы карты всегда была одинаковая. В цикле по X и Z координатам начинаем расставлять кубы. Y координата генерируется за счет библиотеки pretlin.js. В конечном итоге мы добавляем куб с нужными координатами на сцену через scene.add( cube );
generationMap.js
import * as THREE from './three.module.js';
import { Settings } from "./settings.js";
export class Map {
constructor(){
this.materialArray;
this.xoff = 0;
this.zoff = 0;
this.inc = 0.05;
this.amplitude = 30 + (Math.random() * 70);
}
generation(scene) {
const settings = new Settings();
const loader = new THREE.TextureLoader();
const materialArray = [
new THREE.MeshBasicMaterial( { map: loader.load("../texture/dirt-side.jpg") } ),
new THREE.MeshBasicMaterial( { map: loader.load('../texture/dirt-side.jpg') } ),
new THREE.MeshBasicMaterial( { map: loader.load('../texture/dirt-top.jpg') } ),
new THREE.MeshBasicMaterial( { map: loader.load('../texture/dirt-bottom.jpg') } ),
new THREE.MeshBasicMaterial( { map: loader.load('../texture/dirt-side.jpg') } ),
new THREE.MeshBasicMaterial( { map: loader.load('../texture/dirt-side.jpg') } )
];
this.materialArray = materialArray;
const geometry = new THREE.BoxGeometry( settings.blockSquare, settings.blockSquare, settings.blockSquare);
noise.seed(Math.random());
for(let x = 0; x < settings.chunkSize; x++) {
for(let z = 0; z < settings.chunkSize; z++) {
let cube = new THREE.Mesh(geometry, materialArray);
this.xoff = this.inc * x;
this.zoff = this.inc * z;
let y = Math.round(noise.perlin2(this.xoff, this.zoff) * this.amplitude / 5) * 5;
cube.position.set(x * settings.blockSquare, y, z * settings.blockSquare);
scene.add( cube );
}
}
}
}
Камера и управление
Я уже говорил, что controls принимает параметры в виде камеры, сцены и карты. Также в конструкторе мы добавляем массив keys для клавиш и movingSpeed для скорости. Для мыши у нас есть 3 метода. onClick определяет какая кнопка нажата, а onRightClick и onLeftClick уже отвечают за действия. Правый клик(удаление блока) происходит через raycast и поиска пересеченных элементов. Если их нет, то прекращаем работу, если есть, то удаляем первый элеент. Левый клик работает по схожей системе. Для начала создаем блок. Запускаем рейкаст и если есть блок который пересек луч, то получаем координаты этого блока. Далее определяем с какой стороны произошел клик. Меняем координаты для созданного куба в соответствии со стороной к которой мы добавляем блок. градация в 5 единиц т.к. это размер блока(да здесь можно было использовать свойство из settings).
Как работает управление камерой?! У нас есть три метода inputKeydown, inputKeyup и update. В inputKeydown мы добавляем кнопку в массив keys. inputKeyup отвечает за очистку кнопок из массива которые отжали. В update идет проверка keys и вызывается moveForward у камеры, параметры которые принимает метод это скорость.
controls.js
import * as THREE from "./three.module.js";
import { Settings } from "./settings.js";
export class Controls {
constructor(controls, scene, mapWorld){
this.controls = controls;
this.keys = [];
this.movingSpeed = 1.5;
this.scene = scene;
this.mapWorld = mapWorld;
}
// клик
onClick(e) {
e.stopPropagation();
e.preventDefault();
this.controls.lock();
if (e.button == 0) {
this.onLeftClick(e);
} else if (e.button == 2) {
this.onRightClick(e);
}
}
onRightClick(e){
// Удаление элемента по клику
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera( new THREE.Vector2(), this.controls.getObject() );
let intersects = raycaster.intersectObjects( this.scene.children );
if (intersects.length < 1)
return;
this.scene.remove( intersects[0].object );
}
onLeftClick(e) {
const raycaster = new THREE.Raycaster();
const settings = new Settings();
// Поставить элемент по клику
const geometry = new THREE.BoxGeometry(settings.blockSquare, settings.blockSquare, settings.blockSquare);
const cube = new THREE.Mesh(geometry, this.mapWorld.materialArray);
raycaster.setFromCamera( new THREE.Vector2(), this.controls.getObject() );
const intersects = raycaster.intersectObjects( this.scene.children );
if (intersects.length < 1)
return;
const psn = intersects[0].object.position;
switch(intersects[0].face.materialIndex) {
case 0:
cube.position.set(psn.x + 5, psn.y, psn.z);
break;
case 1:
cube.position.set(psn.x - 5, psn.y, psn.z);
break;
case 2:
cube.position.set(psn.x, psn.y + 5, psn.z);
break;
case 3:
cube.position.set(psn.x, psn.y - 5, psn.z);
break;
case 4:
cube.position.set(psn.x, psn.y, psn.z + 5);
break;
case 5:
cube.position.set(psn.x, psn.y, psn.z - 5);
break;
}
this.scene.add(cube);
}
// нажали на клавишу
inputKeydown(e) {
this.keys.push(e.key);
}
// отпустили клавишу
inputKeyup(e) {
let newArr = [];
for(let i = 0; i < this.keys.length; i++){
if(this.keys[i] != e.key){
newArr.push(this.keys[i]);
}
}
this.keys = newArr;
}
update() {
// Движение камеры
if ( this.keys.includes("w") || this.keys.includes("ц") ) {
this.controls.moveForward(this.movingSpeed);
}
if ( this.keys.includes("a") || this.keys.includes("ф") ) {
this.controls.moveRight(-1 * this.movingSpeed);
}
if ( this.keys.includes("s") || this.keys.includes("ы") ) {
this.controls.moveForward(-1 * this.movingSpeed);
}
if ( this.keys.includes("d") || this.keys.includes("в") ) {
this.controls.moveRight(this.movingSpeed);
}
}
}
Ссылки
Как и обещал. Весь материал который пригодится.
Если есть желание, то на можете добавить свой функционал к проекту на гитхаб.
perlin.js
three.js
GitHub
-
Часть 6.2 будет доступна примерно 09.11.13
Серия уроков «Программирование на Java. Знакомство с Forge и MCP.»
- Часть 1. Введение.
- Часть 2. Синтаксис Java.
- Часть 3. Hello World.
- Часть 4. Базовый уровень создания модов.
- Часть 5. Forge или Mod Loader ?
- Часть 6.1. Первый мод — вступление.
- Часть 6.2. Первый мод — основы.
- Часть 6.3. Первый мод — компиляция.
- Часть 6.4. Первый мод — добавляем Test Item.
- Часть 6.5. Первый мод — добавляем Test Block.
- Часть 6.6. Первый мод — взаимодействие.
- Часть 7.1. Продвинутый уровень — Мобы и NPC.
- Часть 7.2. Продвинутый уровень — Генерация руды.
- Часть 8.1. Высший пилотаж — GUI и HUD.
- Часть 8.1. Высший пилотаж — Red Stone схемы.
- Часть 8.1. Высший пилотаж — Изменение AI.
- Часть 9. Пара слов о написании плагинов.
- Часть 10. Подведение итогов.
Дополнения от читателей.
- Как подготовить NetBeans для аналогичных действий ?
Последнее редактирование: 17 ноя 2013 -
Часть 1. Введение.
Доброго всем времени суток. Если вы читаете это, то скорее всего вы хотите научится основам Java, и в частности изучить принцип создания модов. Я искренне надеюсь, что в дальнейшем вам помогут мои уроки и советы. Итак, всем приятного чтения, мы приступаем.
В данной серии уроков, которая будет разбита примерно на десять частей, мы познакомимся с основами синтаксиса, принципа создания модов и подтянем ваши знания Java. Внимание: Я не буду обучать вас нулевым основам Java, даже в уроке «Hello World,» я хотел бы затронуть интерфейс, так как в дальнейшем он нам поможет. От себя посоветую прочитать две книги (которые мне сильно помогли) — «Герберт Шилдт — Полный справочник по Java» и «Брюс Эккель — Философия Java.» Предупреждаю, прочитав эти книги — вас не возьмут в Mojang на должность главного программиста Minecraft, однако эти книги познакомят вас с основами синтаксиса. Хотя мы его затронем во второй части.
Что вам понадобится приносить ко мне на уроки:MCP и Исходники Forge. Когда нам это понадобится я обязательно предоставлю ссылки на загрузку.Часть 2. Синтаксис Java.
Всем привет, это второй урок не большого курса «Программирование на Java. Знакомство с Forge и MCP.» В этом уроке, я сидя за чашечкой кофе расскажу вам о основах синтаксиса.
Для начала давайте же все таки разъесним что такое синтаксис. Многие (из читателей, намек типо) знакомы со школьних лет и уроков Русского (и других) Языка с «Синтаксическим разбором.» Фактически вы разбиваете что либо на элементы, которые заранее подучиваете. Такие же элементы есть и в Java, ниже дана их перечень, со временем будем знакомится ближе.- abstract
- assert
- boolean
- break
- byte
- case
- catch
- char
- class
- const
- continue
- default
- do
- double
- else
- enum
- extends
- final
- finally
- float
- for
- goto
- if
- implements
- import
- instanceof
- int
- interface
- long
- native
- new
- package
- private
- protected
- public
- return
- short
- static
- strictfp
- super
- switch
- synchronized
- this
- throw
- throws
- transient
- try
- void
- volatile
- while
К синтаксису так же можно отнести так называемый «Порядок ввода.»
Каждое Java (+Swing) приложение начинается с открытия пакета.package ИмяПакета;
Далее идет определение класса. Импорт.
import Имя класса;
После чего идет само приложение.
Подведем итог: Этих знаний вам хватит ровно на Hello World, если вы до сих пор не прочитали те книги которые я посоветовал в первом уроке, то боюсь вас тут держит лишь гордость и уверенность в себе. Поверьте, этого не достаточно.Часть 3. Hello World.
В этом уроке мы будем знакомится со средой разработки и напишем свой первый Java апплет. Сразу скажу что я использую Net Beans IDE версии 7.3.х. На мой взгляд он гораздо удобнее и интуитивно понятней по сравнению с тем же Eclispe Juno.В данном уроке мы создадим простую программу, которая будет иметь примитивный интерфейс. Чтобы ваша жизнь не казалась медом, я не буду писать пример кода сюда. Я буду прикладывать скриншот который вы будете по желанию переписывать. Во первых есть большая вероятность что вы запомните, что написали, а во вторых это будет не тупой копипаст.
Итак. Каждое приложение начинается с имени проекта и импорта, обратите внимание на вторую строку. Здесь находится не знакомый импорт JOptionPane (Swing). Импорт данного свинга отвечает за интерфейс, который нам так знаком из загрузочного окна Forge, далее он нам понадобится для ввода определенных команд.
Смотрим дальше, видим строку (5), просто введите ее, вернемся к этому в уроке 6.1. Далее (7) стринги, нет не подумайте, не те стринги которые вы будете снимать с телочек после того как напишите супер программу которая взорвет мозги всего мира. Данную строку, на русском можно описать так:
«Я даю команду для X, слушай меня раб.» И на 8-10 строке я давал эту команду.
Восьмая строка, тот самый JOptionPane и приказ о выводе диалога. В конце строки не забудьте поставить «;». На девятой строке мы говорим «X» о том что хотим увидеть строку для ввода, и то что пользователь туда введет примем за «X.» Далее идет не понятное «+X+». Это то место куда будет вводится переменная которая кешируется на девятой строчке.Обратите внимание на фигурные скобки («{» «}») это открытие и закрытие. То что мы открыли на 4 строке мы закрыли на 12, а то что на 6 закрыли на 11, все должно четко совпадать! Будьте внимательней.
Вот и подошел к концу третий урок, надеюсь вы все поняли и вам это пригодится. Спасибо за чтение.
-
Часть 4. Базовый уровень создания модов.
Всем привет, и это четвертый урок программирования на Java, был не много занят потому и задержал. Итак мы начинаем.
Сегодня мы с вами познакомимся со способами создания модов и подготовимся к практике. Для начала давайте же обсудим, что нам понадобится. В первую очередь — свободное время, да да да! Его нам надо будет много. Второе — Исходники Minecraft и Forge. «Где же их взять» спросите вы. Отвечаю, исходники Forge всегда можно найти здесь, напротив интересующей вас версии нажав на «src» (source). Исходники Minecraft легко достать с помощью MCP (Minecraft Coder Pack). О декомпеляции можно почитать на wiki. Скачать MCP можно здесь. Так же, для декомпеляции minecraft вам понадобится не только клиент но и ванильный сервер. Последнюю версию всегда можно взять на официальном сайте, но что же делать с устаревшими ? Ответ прост, скачать утилиту Minecraft Version Changer, выбрать нужную версию и нажать кнопку «server.» Третье, что нам нужно это среда разработки, я использую Net Beans, я использую готовую сборку «Net Beans 7.3.1 with JDK Bundle» от Oracle. Вроде с основным разобрались, перед следующим советую заново прочитать мои предыдущие уроки и пойти по моим советам.
Ставьте лайк и ждите следующий урок, который будет уже совсем скоро. -
Hephest
Старожил
Пользователь- Баллы:
- 153
- Skype:
- hephest1904
Молодчина, продолжай, интересно
-
Часть 5. Forge или Mod Loader.
Всем привет. Вы читаете продолжение курса «Программирование на Java. Знакомство с Forge и MCP.» Сегодня последний урок в котором я вам буду промывать мозги, готовьтесь: уже в следующей части будет практика, будет мод и будет счастье, однако это часть я напишу далеко не сразу, и буду писать ее в текстовом редакторе, так что ждать завтра-послезавтра не стоит.
Итак, вернемся к уроку, этот урок будет особенным сегодня вы выберете для себя глобальную черту для изучения Java, в зависимости от выбора будет зависеть ваш труд и успех. Сегодня мы выбираем то, по до что мы будем «кодить.»Итак назначаю кастинг открытым. //* Музочка (Туц туц) *//
Претендент номер 1: Minecraft Coder Pack.
Плюсы:- Вас хрен кто декомпелирует.
- Вы можете менять все, вплоть до изменения названия файла, структуры и расположения на системе.
- Ваш мод будет полностью уникальным, при определенной защите.
- Ваш проект в «Среде» будет аккуратен и будет напрямую работать с Minecraft без прочих API.
- Не так часто приходится обновлять исходники minecraft.
Минусы.
- Проблемно будет поставить другой мод или лоадер.
- В начале будет много говнокода.
- Тяжелее переносить на новую ваниллу.
Претендент номер 2: Forge. FML.
Плюсы.- За пару минут обновление на новую ваниллу.
- Упрощенный принцип создания модов.
- Разработчик сам предоставляет исходники.
- Нет говнокода в исходниках.
Минусы.
- Значительно меньший функционал по сравнению с кодингом под чистый майнкрафт.
- Чтобы взять ваш мод, придется просто залезть в определенную папку и взять Jar’ник, не поможет даже изменение FML кода, однако можно вшить в клиент но получится обычный Minecraft, с копирайтами фордж и увеличенным весом.
- Кушает производительность.
Претендент номер 3: ModLoader
——кто это дерьмо юзает ?——Итак вроде все разъяснил, я как и прежде остаюсь на MCP, но уроки будут на Forge, т.к. его использовать проще. Всем спасибо, ставьте лайки и пишите супер моды. Пока.
Новая часть будет в двадцатых числах (в лучшем случае.)
Почти написал уже, ждите -
Ждем
-
Часть 6.1. Первый мод — вступление.
В предыдущей части урока мы выбирали себе платформу и среду разработки. На этом мы переходим к созданию модов. Пока не поздно, напоминаю. Без знаний Java, дальше того чего я вас научу вы не уйдете, а копировать и вставлять мой шаблон, как то по школярски на мой взгляд.
Начнем сегодня с подготовки. Для начала обновляем Java, очень рекомендую. Остальное мы загрузили в одном из предыдущих уроков.
Далее заходим в папку с исходниками Forge, и видим install.bat. Запускаем, ждем. Забыл напомнить, что теперь я перешел на Eclipse, будьте бдительны.
Когда процесс установки дойдет до конца, и без ошибок советую кричать и радоваться, я лично от счастья нажрался водяры за 20 рублей и уснул на лавке перед домом. Если нет, то попробуйте взять бубен, и запустить процесс заново, напевая шаманские мотивы и стуча в бубен, если даже так не поможет, значит не судьба, ждем билдов форджа по новее.
Итак, если у вас все же, все оказалась более чем успешно и фордж сделал всю работу на ура, заходим в Eclipse (я использую Kelper). Что мы видим ? Окошко, которое просит нас указать на workspace. Туда нам нужно указать папку eclipse, которая находится в папке forge (Forge>mcp>eclipse), ВНИМАНИЕ! Не ее содержимое, а именно папку eclipse. Теперь ждем некоторое время, пока загрузится, первый запуск может затянуться.
Теперь когда все загрузилось, слева в колонке Package Editor, появилась папка Minecraft, откройте ее и напротив JRE System Library выставите Java SE-1.7, если конечно у вас там что то другое.
Теперь с целью эксперимента можно запустить ваш Minecraft, нажав в верху на зеленую кнопку Run Client, не пугайтесь красных надписей внизу, это норма. Кстати, так можно запускать клиенты с Forge на новых версиях (Это так, к вопросу о лаунчерах.) Однако вы не будете иметь логина и пароля :c
Позже научу как это исправить.Последнее редактирование: 20 окт 2013 -
Hephest
Старожил
Пользователь- Баллы:
- 153
- Skype:
- hephest1904
Можно дописать, что даже если MCP не скачан, достаточно просто скачать src Forge и запустить батник. Установщик в командной строке сам всё скачает
-
Прекрасно что вы это знаете
-
У кого не грузит библиотеки во время установки Forge,скачайте архив по ССЫЛКЕ и распакуйте его в корень папки с mcp и в папку jars.Потом нажмите «install.bat» в папке forge
-
С чего бы ему не грузить ? Не разу не сталкивался, однако добавлю в урок.
-
Смотря какая версия,если 1.5.2-1.6.4,то все хорошо скачивает,а если до 1.5.2,то там ошибки,а если вообще 1.2.5,то там нужна библиотека fernflower + lib(Могу тоже выложить.)
-
Hephest
Старожил
Пользователь- Баллы:
- 153
- Skype:
- hephest1904
Интересно, кто под такие старые версии пишет моды/прочее?
-
Я)
Ну может кому нужно будет.У нас же урок не под одну версию.
Библиотека fernflower для корректной установки forge для версии minecraft 1.2.5Установка: mcp/runtime/bin
Где «mcp» ваша папка с mcp+forge
Жду новой части туториала!Вложения:
-
Dr.Death
Старожил
Пользователь- Баллы:
- 153
- Skype:
- asn008
- Имя в Minecraft:
- DrDeath
Я под 1.8.1 beta пишу, на новые версии майна вообще не смотрю
caNek и Dereku нравится это.
-
Плюс, правда я чуть по новее. 1.2.5 и иногда 1.4.7.
Насилую документацию к Forge чтобы донести все максимально понятно (ну или она меня, хз). -
Зачем максимально понятно? В этой теме должны обитать люди которые знают java хотя-бы на среднем уровне
-
Возможно, но тогда туториал не будет отличатся от сотен других. Но как бы то ни было, думаю в воскресенье будет новая часть.
-
Если вы хотите использовать NetBeans в качестве среды для изменения кода,вместо eclipse,то вам нужно проделать несколько шагов:
-
Заходим в вашу папку с mcp,в дальнейшем будем называть её «root»
-
Переходим по такому пути «root/eclipse/Minecraft«
-
Видим файл .project,открываем его с помощью адекватного текстового редактора(Notepad++).
-
В нем видим следующие строчки:
-
В нем ищем строчку «MCP_LOC» и после неё вставляем СВОЙ путь до вашей папки с mcp(%root%),найдите и измените все ТРИ варианта пути.
-
Открываем: NetBeans/импорт проекта/eclipse
-
Выбираем в качестве папки с проектом папку eclipse
-
Нажимаем далее и выбираем проект minecraft (он там один) и жмем финиш!
-
После если вы все сделали правильно у вас откроется проект,но радоваться еще рано.
-
Кликаем ПКМ по проекту (minecraft) выбираем «свойства»,в свойствах слева выбираем Библиотеки,потом выбираем «Выполнение«,а в нем «Скомпилированные исходные коды».Там двигаем Скомпилированные исходные коды вверх,при помощи кнопки «Переместить Вверх».
-
После этого идем во вкладку «Выполнение» (слева) и там указываем параметр для VM —xincgc -Xmx1024M -Xms1024M -Djava.library.path=«тут полный путь до вашей папки с mcp + jars/bin/natives/«
Все,теперь можно спокойно изменять код и тестировать ваш клиент (зеленая кнопка в меню кода)Последнее редактирование: 3 ноя 2013
BleaZzZ и caNek нравится это.
-
Поделиться этой страницей
Всем привет! Уже столько времени прошло с прошлой статьи, в которой я писал про реализацию своей небольшой версии, написанной на Go, как всегда исходный код доступен на GitHub. Сразу думаю сказать, что за это время успел уже перейти на линукс(Mint Cinnamon), получить проблемы с интегрированной GPU, но в конце концов наконец я смог нормально работать с редактором от JetBrains и сделать переход с Go на Rust, это было сделано так-как я думал изначально писать на расте, но было очень проблематично компилировать… Но вот и был сделан всё-таки переход с улучшениями как производительности так и возможностей!)
Причина перехода с Go на Rust
-
Изначально я задумывался о создании на нём, но не мог нормально скомпилировать код.
-
Код на расте работает в разы быстрее и безопаснее.
-
Теперь скорость можно измерять в наносекундах…)
Немного важных уточнений
При переходе на Rust я решил, что стоит делать по-умолчанию английский язык, так-как его знают большинство, а это значит если человек, который не знает русского — сможет вполне использовать, из-за этого все комментарии в статье и коде на GitHub будут написаны на английском языке, при этом может быть и много ошибок… Получилось так-же много файлов, но есть которые почти пустые и только обьединяют несколько модулей, поэтому такие файлы я не буду комментировать, а только оставлю код.
Продолжение #1.1
Код на расте вышел всё-таки в разы больше так-как я пытался использовать как можно больше своего, но всё-таки для лучшего результата использовал много, но важных библиотек и прописал их в Cargo.toml:
Содержимое Cargo.toml
[package]
name = "ule"
version = "0.1.0"
edition = "2021"
publish = true
authors = [
"Distemi <distemi.bot@mail.ru>"
]
homepage = "https://github.com/Distemi/ULE"
repository = "https://github.com/Distemi/ULE"
[dependencies]
# Быстрый HashMap и другое.
ahash = "0.7.6"
# Глобальные переменные
lazy_static = "1.4.0"
# Struct <-> JSON
serde = { version = "1.0.136", features = ["derive"] }
serde_json = "1.0.78"
# Утилиты логирования
log = "0.4.14"
fern = { version = "0.6", features = ["colored"] }
# Время
chrono = "0.4.19"
# Асинхронность(скоро точно понадобится)
async-std = "1.10.0"
# Однопоточный TCP и UDP сервер
[dependencies.mio]
version = "0.8.0"
default-features = false
features = [
"os-ext",
"net"
]
[profile.release]
opt-level = "z"
Как раз наш Cargo.toml является одним из основных файлов для проекта, а следющий по важности src/main.rs:
Наш main.rs
#![allow(unused_must_use)]
use crate::config::{ADDRESS, ADDRESS_PORT};
use crate::logger::start_input_handler;
use crate::network::network_server_start;
use fern::colors::Color;
use std::error::Error;
use std::process;
use std::sync::mpsc::channel;
use std::sync::Arc;
use std::time::SystemTime;
use std::{fmt, thread};
use utils::logger;
// Use a macros from serde(Serialize and Deserialize), log(Logging) and lazy_static(Global variables)
#[macro_use]
extern crate serde;
#[macro_use]
extern crate log;
#[macro_use]
extern crate lazy_static;
pub mod config;
pub mod network;
pub mod utils;
// Main function of application
fn main() {
let start = SystemTime::now();
// Initialize logger
println!("Starting ULE v1.0.0...");
if let Err(err) = logger::setup_logger() {
eprintln!("Failed to initialize logger: {}", err);
process::exit(1);
}
// Creating channel for multithreading communication with main's thread and network's thread
let (tx, rx) = channel::<bool>();
// Generate server's address and make it accessible with thread safe
let address = Arc::new(String::from(format!(
"{}:{}",
ADDRESS,
ADDRESS_PORT.to_string()
)));
// Start network in another thread
thread::spawn({
let address = address.to_string();
move || {
// Start network
// If failed to start when return error
if let Err(err) = network_server_start(address, &tx) {
error!("{}", err);
tx.send(false);
}
}
});
// Wait for status from server's network
if rx.recv().unwrap_or(false) {
// If Server successful started
info!("Server started at {}", address);
// Showing about the full launch and showing the time to start
{
let elapsed = start.elapsed().unwrap();
info!(
"The server was successfully started in {}",
if elapsed.as_secs() >= 1 {
format!("{}s", elapsed.as_secs())
} else if elapsed.as_millis() >= 1 {
format!("{}ms", elapsed.as_millis())
} else {
format!("{}ns", elapsed.as_nanos())
}
);
drop(elapsed);
};
} else {
// If Failed to start Server
error!("Failed to start server on {}.", address);
process::exit(1);
}
// Remove channel
std::mem::drop(rx);
// Start console input handler(input commands)
start_input_handler();
}
// Custom error(yes, not std::io:Error)
#[derive(Debug)]
pub struct SimpleError(String, Option<std::io::Error>);
impl Error for SimpleError {}
impl fmt::Display for SimpleError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// Check is error provided
if self.1.is_some() {
write!(f, "{}: {:?}", self.0, self.1)
} else {
write!(f, "{}", self.0)
}
}
}
// Custom Result with custom Error
pub type SResult<T> = Result<T, SimpleError>;
В нашем main.rs в самом начале инициализируем отсчёт времени, который вскоре будем использовать для показа время запуска. Потом мы пытаемся инициализировать логгер(fern + log), при неудаче — выводим ошибку и «убиваем» процесс. Следующим шагом у нас идёт создания некого канала, а после строки адресса сервера, но канал, который на самом деле выглядит по методам как UDP, но с блокировкой потоков, он нам нужен для ожидания основного потока, который ждёт результат запуска от потока сетевого сервера(TCP сервер), получилось — выводим информацию об успешном запуске сетевого сервера и за сколько времени запустилось, при ошибке — выводим информацию о проблеме запуска. Если получилось запустить наш TCP сервер, то удаляем наш канал связи и начинаем слушать ввод с консоли. Как видно есть типы SResul(из сокращения SimpleResult) и SimpleError, первый говорит сам за себя, как и второй, но для которого идёт приминение разных trait для показа ошибок.
Наш метод инициализации логгера лежит в файле src/utils/logger/input.rs, но я покажу так-же src/utils/mod.rs и src/utils/logger/mod.rs, так-как они зависимы:
src/utils/mod.rs
pub mod chat;
pub mod logger;
Просто импортируем модули чата и логгера публично
src/utils/logger/mod.rs
mod input;
mod log_lib;
pub use {input::start_input_handler, log_lib::setup_logger};
Тут идёт импорт input.rs и log_lib.rs, а так-же экспорт методов start_input_handler и setup_logger
Содержимое файла с методом инициализации логгера
use crate::Color;
use fern::colors::ColoredLevelConfig;
use std::fs;
// Logger's initialize(fern, color and log)
pub fn setup_logger() -> Result<(), fern::InitError> {
// Removing latest log if exists
fs::remove_file("latest.log");
// Setting colors
let colors = ColoredLevelConfig::new()
.info(Color::BrightBlack)
.warn(Color::Yellow)
.error(Color::Red)
.trace(Color::BrightRed);
// Setting fern
fern::Dispatch::new()
// Setting custom format to logging
.format(move |out, message, record| {
out.finish(format_args!(
"{} [{}] {}",
chrono::Local::now().format("[%m-%d %H:%M:%S]"),
colors.color(record.level()),
message
))
})
// Setting log-level
.level(log::LevelFilter::Info)
// Setting target's logger
.chain(std::io::stdout())
// Setting log's file
.chain(fern::log_file("latest.log")?)
// Applying settings
.apply()?;
// If successful setting - returning ok
Ok(())
}
При инициализации логера мы в первую очередь удаляем файл последнего лога latest.log, потом устанавливаем на каждый уровень лога свой цвет(INFO = серый, WARN — жёлтый, ERROR — красный, TRACE — ярко-красный). Позже идёт инициализация самого логгера fern и для него мы устанавливаем формат: [ДАТА] [УРОВЕНЬ] СООБЩЕНИЕ, цвет имеет только уровень, а дата и сообщение стандартным цвветом консоли. Для логгера устанавливаем вывод в stdout(консоль вывода), минимальный уровень вывода — INFO, а так-же вывод в лог-файл и принимаем эти изменения. Если не было ошибок при этих действиях — возращяем успешный пустой результат.
Далее у нас через main.rs создаётся канал mpsc, который передаётся в другой поток сетевого сервера TCP и это делается через network_server_start из пакета network, который имеет много «пустых» файлов, но оттуда нам нужен лишь протокол, сервер, буферы и обработчики. Сам сетевой сервер располагается по пути src/network/server.rs:
Содержимое сетевого сервера
use crate::network::handler::{handshaking, status_handler};
use crate::network::network_client::ConnectionType::HANDSHAKING;
use crate::network::network_client::NetworkClient;
use ahash::AHashMap;
use mio::net::TcpListener;
use mio::{Events, Interest, Poll, Token};
use std::io;
use std::sync::mpsc::Sender;
use std::sync::Mutex;
use std::time::Duration;
// Declare global variables
lazy_static! {
// Server need to shut down? (true - yes, needs to shutdown network server).
pub static ref SHUTDOWN_SERVER: Mutex<bool> = Mutex::new(false);
// Server's works status.
pub static ref NET_SERVER_WORKS: Mutex<bool> = Mutex::new(true);
}
// Server's Token(ID)
const SERVER: Token = Token(0);
// Next Token
fn next(current: &mut Token) -> Token {
let next = current.0;
current.0 += 1;
Token(next)
}
// Start a network server
pub fn network_server_start(address: String, tx: &Sender<bool>) -> std::io::Result<()> {
// Creating Network Pool
let mut poll = Poll::new()?;
// Creating Network Events Pool
let mut events = Events::with_capacity(256);
// Converting String's address to SocketAddr
let addr = address.parse().unwrap();
// Starting a Network Listener
let mut server = TcpListener::bind(addr)?;
// Register server's Token
poll.registry()
.register(&mut server, SERVER, Interest::READABLE)?;
// Creating a list of connections
let mut connections: AHashMap<Token, NetworkClient> = AHashMap::new();
// Creating a variable with latest token.
let mut unique_token = Token(SERVER.0 + 1);
// Send over the channel that the server has been successfully started
tx.send(true);
// Network Events getting timeout
let timeout = Some(Duration::from_millis(10));
// Infinity loop(while true) to handing events
loop {
// Checks whether it is necessary to shutdown the network server
if *SHUTDOWN_SERVER.lock().unwrap() {
*NET_SERVER_WORKS.lock().unwrap() = false;
info!("Network Server Stopped!");
return Ok(());
}
// Getting a events from pool to event's pool with timeout
poll.poll(&mut events, timeout)?;
// Handing a events
for event in events.iter() {
// Handing event by token
match event.token() {
// If it server's event
// Reading a all incoming connection
SERVER => loop {
// Accepting connection
let (mut connection, _) = match server.accept() {
// If successful
Ok(v) => v,
// If not exists incoming connection
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
break;
}
// If failed to get incoming connection
Err(e) => {
return Err(e);
}
};
// Generating new token for this connection
let token = next(&mut unique_token);
// Registering connection with token
poll.registry().register(
&mut connection,
token,
Interest::READABLE.add(Interest::WRITABLE),
)?;
// Pushing connection into connection's list
connections.insert(
token,
NetworkClient {
stream: connection,
conn_type: HANDSHAKING,
},
);
},
// Handing event from client
token => {
// Handing event by connection's stage
let done = if let Some(connection) = connections.get_mut(&token) {
let m = match &connection.conn_type {
HANDSHAKING => handshaking,
_ => status_handler,
};
// Trying to handing
m(connection, &event).unwrap_or(false)
} else {
false
};
// If needs to close connection - removing from list, unregister and close connection's stream
if done {
if let Some(mut connection) = connections.remove(&token) {
poll.registry().deregister(&mut connection.stream)?;
connections.remove(&token);
}
}
}
}
}
}
}
Да, уже целых 125 строчек, но это ещё мало
В нём мы инициализируем глобальные переменные используя lazy_static, обратите внимание, что тип bool завёрнут в оболочку Mutex, который гарантирует мультипоточный доступ к переменной, к чтении и записи, но для получения этих переменных блокируется поток ожидая информации. Создаётся так-же простая примитивная структура SERVER, который имеет значение айди сервера в сетевом сервере. Далее идёт next, который работает как ++ для переменных(в расте не ++, а +=1)
, а следует за этой функцией уже другая — network_server_start. Функция сетевого сервера на старте инициализирует два пула, один отвечает за хранилища событий и дальше идёт парсинг строки в адресс, а следом попытка запустить TCP сервер на этом адрессе, который регестрируем в пуле как только-чтение, после мы создаём список подключений и уникальный токен на новое подключение, если не было ошибок, то отправляем в канал связи — true, который означать о успешном запуске. Переменная timeout используется как лимит ожидания событий, чтобы начать цикл обработки запросов заного, что есть в цикле: проверка на нужду в отключении сервера и если надо, то просто устанавливаем статус, что сервер выключен и останавливаем цикл, остальную работу по одключении слушателя и тд делаем сам компилятор. Если же останавливать нам не надо, то мы ждём до 10мс сетевые события и позже обрабатываем существующие события. Серверные события бывают только — принятие нового подключения, поэтому мы создаём ещё цикл в котором принимаем все подключения, регистрируем их. В случае если это события связанные с клиентами(присланный пакет например) — в зависимости от типа подключения(HandShaking, Status и тд) передаём соответствующему обработчику и в случае если обработчик возращает true, то мы разрываем соединение и удаляем его из хранилища подключений.
Протокол, обработка подключений, чтение и создание пакетов
Так-как ядро я пытаюсь сделать менее зависимым от библиотек, то для чата и протокола будет всё написано с 0 и для обеспечения большего контроля над чтением и записью.
Было решено хранить буферы пакетов в виде векторов к которым добавлены методы чтения и записи(только для Vec<u8>):
Чтение буферов
use crate::{SResult, SimpleError};
/// Reader [Vec] of bytes
pub trait PacketReader {
// 1-Byte
fn get_u8(&mut self) -> u8;
fn get_i8(&mut self) -> i8;
// 2-Byte
fn get_u16(&mut self) -> u16;
fn get_i16(&mut self) -> i16;
// 4-Byte
fn get_varint(&mut self) -> SResult<i32>;
// 8-Byte
fn get_i64(&mut self) -> i64;
// Another
fn get_string(&mut self) -> SResult<String>;
fn read_base(&mut self) -> SResult<(i32, i32)>;
}
// Apply reader to Vec
impl PacketReader for Vec<u8> {
// Read a single byte as u8 ( 8-Bit Unsigned Integer )
fn get_u8(&mut self) -> u8 {
self.remove(0)
}
// Read a single byte as i8 ( 8-Bit Integer )
fn get_i8(&mut self) -> i8 {
self.remove(1) as i8
}
// Read a two bytes as u16 ( 16-Bit Unsigned Integer )
fn get_u16(&mut self) -> u16 {
u16::from_be_bytes([self.get_u8(), self.get_u8()])
}
// Read a two bytes as i16 ( 16-Bit Integer )
fn get_i16(&mut self) -> i16 {
i16::from_be_bytes([self.get_u8(), self.get_u8()])
}
// Read a VarInt ( Dynamic-length 32-Bit Integer )
fn get_varint(&mut self) -> SResult<i32> {
// Result variable
let mut ans = 0;
// Read up to 4 bytes
for i in 0..4 {
// Read one byte
let buf = self.get_u8();
// Calculate res with bit moving and another
ans |= ((buf & 0b0111_1111) as i32) << 7 * i;
// If it's limit when stop reading
if buf & 0b1000_0000 == 0 {
break;
}
}
// Return result as successful
Ok(ans)
}
// Read a Long ( 64-Bit Integer )
fn get_i64(&mut self) -> i64 {
// Yes, read 8 bytes
i64::from_be_bytes([
self.get_u8(),
self.get_u8(),
self.get_u8(),
self.get_u8(),
self.get_u8(),
self.get_u8(),
self.get_u8(),
self.get_u8(),
])
}
// Read a String ( VarInt as len; bytes[::len] )
fn get_string(&mut self) -> SResult<String> {
// Getting string-length
let len = self.get_varint()?;
// Create String's bytes buffer
let mut buf = Vec::new();
// Reading Bytes
for _ in 0..len {
buf.push(self.get_u8())
}
// Convert Bytes to UTF8 String
match String::from_utf8(buf) {
Ok(v) => Ok(v),
Err(_) => Err(SimpleError(String::from("Failed to parse chars"), None)),
}
}
// Read first two VarInt(Packet's length and id)
fn read_base(&mut self) -> SResult<(i32, i32)> {
let len = self.get_varint()?;
let pid = self.get_varint()?;
Ok((len, pid))
}
}
Тут мы можем наглядно увидеть чтение VarInt, String, Long и другое, что пока надо было при написании ядра.
Запись в буферы пакетов
/// Writer [Vec] of bytes
pub trait PacketWriter {
// 1-Byte
fn write_u8(&mut self, value: u8);
fn write_i8(&mut self, value: i8);
// 2-Byte
fn write_u16(&mut self, value: u16);
fn write_i16(&mut self, value: i16);
// 4-Byte
fn write_varint(&mut self, value: i32);
// 8-Byte
fn write_i64(&mut self, value: i64);
// Another
fn write_vec_bytes(&mut self, bytes: Vec<u8>);
fn write_string(&mut self, value: String);
fn create_packet(&mut self, pid: i32) -> Vec<u8>;
}
impl PacketWriter for Vec<u8> {
// Writing byte
fn write_u8(&mut self, value: u8) {
self.push(value);
}
// Writing byte
fn write_i8(&mut self, value: i8) {
self.push(value as u8)
}
// Writing 2-byte unsigned integer
fn write_u16(&mut self, value: u16) {
self.extend_from_slice(&value.to_be_bytes());
}
// Writing 2-byte unsigned integer
fn write_i16(&mut self, value: i16) {
self.extend_from_slice(&value.to_be_bytes());
}
// Writing bytes as VarInt
fn write_varint(&mut self, mut value: i32) {
// Bytes buffer
let mut buf = vec![0u8; 1];
// Byte's length
let mut n = 0;
// Converts value to bytes
loop {
// Break if it's limit
if value <= 127 || n >= 8 {
break;
}
// Pushing a byte to buffer
buf.insert(n, (0x80 | (value & 0x7F)) as u8);
// Moving value's bits on 7
value >>= 7;
value -= 1;
n += 1;
}
// Pushing byte, because it lower that 256(<256)
buf.insert(n, value as u8);
n += 1;
// Pushing converted bytes into byte's buffer
self.extend_from_slice(&buf.as_slice()[..n])
}
// Writing Long ( 64-Bit Integer )
fn write_i64(&mut self, value: i64) {
self.extend_from_slice(value.to_be_bytes().as_slice())
}
// Alias of extend_from_slice, but works with Vec, not Slice
fn write_vec_bytes(&mut self, mut bytes: Vec<u8>) {
self.append(&mut bytes);
}
// Write String (VarInt as len and string's bytes)
fn write_string(&mut self, value: String) {
// Getting String as Bytes
let bytes = value.as_bytes();
// Writing to buffer a length as VarInt
self.write_varint(bytes.len() as i32);
// Writing to buffer a string's bytes
self.extend_from_slice(bytes);
}
// Packet's base builder
fn create_packet(&mut self, pid: i32) -> Vec<u8> {
// Creating empty packet's buffer
let mut packet = Vec::new();
// Creating length's bytes buffer and fill it as VarInt
let mut len_bytes: Vec<u8> = Vec::new();
len_bytes.write_varint(pid);
// Writing full packet's length(content + length's bytes)
packet.write_varint((self.len() + len_bytes.len()) as i32);
// Writing length bytes
packet.extend_from_slice(len_bytes.as_slice());
// Drop(Free) length bytes buffer
drop(len_bytes);
// Writing some packet's content
packet.extend_from_slice(self.as_slice());
// Returning result
packet
}
}
Запись к буферам выглядит в разы интересней из-за больших требований к стандарту протокола MineCraft.
Обработчики пакетов на статусы 0(HandShaking) и 1(Status) расположены в одном файле src/network/handler.rs и в нём на каждый тип своя функция.
Вот например HandShaking:
pub fn handshaking(conn: &mut NetworkClient, event: &Event) -> SResult<bool> {
// Checking if we can read the package
if !event.is_readable() {
return Ok(false);
}
// Reading packet
let handshake = read_handshake_packet(conn);
// Checking if is error
if handshake.is_err() {
return Ok(true);
}
// Getting results
let (_, _, _, next_state) = handshake.unwrap();
// Change types
conn.conn_type = match next_state {
1 => STATUS,
_ => STATUS,
};
Ok(false)
}
Хоть функция и имеет 20 строчек, но в ней мы требуем лишь чтения первого пакета для определения следующего статуса ну и основное чтение пакета происходит в read_handshake_packet:
Функция чтения HandShake
pub fn read_handshake_packet(client: &mut NetworkClient) -> SResult<(u32, String, u16, u32)> {
// Read bytes from client
let (ok, p, err) = match client.read() {
Ok((ok, p)) => (ok, Some(p), None),
Err(err) => (false, None, Some(err)),
};
// If failed to read when...
if !ok || err.is_some() {
return Err(SimpleError(
String::from("Failed to read packet"),
if err.is_some() { err.unwrap().1 } else { None },
));
}
// Reading packet
let mut p: Vec<u8> = p.unwrap();
// Try to read Length and PacketID from packet(on handshaking stage only 0x00)
p.read_base()?;
// Reading version, address and etc.
let ver = p.get_varint()? as u32;
let address = p.get_string()?;
let port = p.get_u16();
let next_state = p.get_varint()? as u32;
// States can be only 1 - status, 2 - play
if next_state >= 3 {
return Err(SimpleError(String::from("Invalid client"), None));
}
// Returning results
Ok((ver, address, port, next_state))
}
Мы читаем пакет и при ошибке возращаем её, а если получилось прочитать полностью, то и возращаем результаты чтения.
Для обработки статуса у нас есть иная функция:
pub fn status_handler(conn: &mut NetworkClient, event: &Event) -> SResult<bool> {
// Checking if we can read and write
if !event.is_readable() || !event.is_writable() {
return Ok(false);
}
// Getting a input's bytes
let (ok, p, err) = match conn.read() {
Ok((ok, p)) => (ok, Some(p), None),
Err(err) => (false, None, Some(err)),
};
// Checking if a read or not
if !ok {
return Ok(err.is_some());
}
// Packet's bytes
let mut p: Vec<u8> = p.unwrap();
// Cloning bytes(for ping-pong)
let bytes = p.clone();
// Reading a packet's length(and remove...) and PacketID
let (_, pid) = p.read_base()?;
match pid {
// Is Ping List
0x00 => {
drop(bytes);
conn.stream.write_all(&*create_server_list_ping_response());
}
// Is Ping-Pong
0x01 => {
conn.stream.write_all(bytes.as_slice());
match conn.stream.peer_addr() {
Ok(v) => info!("Server pinged from {}", v),
Err(_) => {
info!("Server pinged.")
}
}
}
_ => {}
}
Ok(false)
}
В ней снова при вызове в первую очередь проверяем на возможность не только чтения, но и записи так-как на этом этапе мы всегда отдаём некий результат. Снова читаем буффер и пытаемся прочитать его начал сохранив при этом айди пакета(0x00 — Список, 0x01 — Пинг-Понг) я так-же реализовал небольшой генератор буффера для списка:
Сам генератор ответа на список
// Structs for status MOTD response
#[derive(Debug, Serialize)]
pub struct ListPingResponse {
pub version: ListPingResponseVersion,
pub players: ListPingResponsePlayers,
pub description: ChatMessage,
}
#[derive(Debug, Serialize)]
pub struct ListPingResponseVersion {
pub name: String,
pub protocol: u32,
}
#[derive(Debug, Serialize)]
pub struct ListPingResponsePlayers {
pub max: u32,
pub online: u32,
pub sample: Vec<ListPingResponsePlayerSample>,
}
#[derive(Debug, Serialize)]
pub struct ListPingResponsePlayerSample {
pub name: String,
pub id: String,
}
/// Build packet's bytes as result
pub fn create_server_list_ping_response() -> Vec<u8> {
// Initialize empty byte's vector
let mut bytes = Vec::new();
// Generating String and convert to bytes.
// String generated as JSON by serde and serde_json libraries
bytes.write_string(
serde_json::to_string(&ListPingResponse {
version: ListPingResponseVersion {
name: String::from("ULE"),
protocol: PROTOCOL_VERSION,
},
players: ListPingResponsePlayers {
max: 10,
online: 0,
sample: vec![],
},
// Some clients can read colors and so on without convert into JSON
description: ChatMessage::str("&a&lHello!"),
})
.unwrap(),
);
// Build completed packet. Server List Ping - PacketID is 0x00
bytes.create_packet(0x00)
}
Для него мы сначала используем информацию о том как должен выглядеть JSON ответа и для ответа мы делаем пустой буффер, записываем байты переведённой сструктуры в JSON и генерируем пакет с айди 0x00. Для серелизации используем serde и serde_json.
Если пингануть сервер, то можно увидеть будет результат. При получении пинг-понг мы просто отправляем копию буфера так-как это более экономный вариант так-как иначе бы пришлось читать Long и другое, что нагружало бы процессор и ОЗУ больше чем просто копия буффера.
Последнее… Input в консоли или же STDIN.
Последняя функция из main.rs — ввод комманд, пока он будет очень примитивный и иметь всего stop и вывод введённого. Так-как имеется не так много возможностей казалось бы, то и ничего важного не будет, но нет! Он будет нам блокировать основной поток приложения возволяя ему работать, ведь если основной поток будет остановлен, то и вся программа остановится. Поэтому как выглядит функция так:
use crate::network::{NET_SERVER_WORKS, SHUTDOWN_SERVER};
use std::time::Duration;
use std::{io, process, thread};
// Loop for handling input
pub fn start_input_handler() -> std::io::Result<()> {
// Input buffer
let mut inp = String::new();
// STDIN - os input
let stdin = io::stdin();
// loop for infinity handling
loop {
// Before write buffer we need to clear buffer
inp.clear();
// Reading a line
stdin.read_line(&mut inp)?;
// Clearing input's buffer
inp = inp.replace("n", "");
// Simple realisation of stop command, but in updates be removed from here in another place
if inp.starts_with("stop") {
// Sending status to shutdown network server
*SHUTDOWN_SERVER.lock().unwrap() = true;
info!("Stopping server...");
// Running process killing in 6 secs if failed to common shutdown
thread::spawn(|| {
thread::sleep(Duration::from_secs(6));
process::exit(0);
});
// Waiting for shutdown network's server
loop {
if *NET_SERVER_WORKS.lock().unwrap() == true {
thread::sleep(Duration::from_millis(25));
} else {
break;
}
}
// Disabling the input
return Ok(());
}
// If it's not stop command - when display buffer, but in updates be removed
info!("Entered: {}", inp);
}
}
Мы сначала создаём буффер и stdin, а так-же запускаем цикл в котором сначала очищаем буффер от прошлого ввода и потом блокируем поток в ожидании ввода. При получении ввода проверяем на содержмиое и если это stop, то устанавливаем сетевому серверу информацию о том, что надо выключать слушатель и дожидаемся выключения, но если за 6 секунд не произошло отключения — завершаем процесс с кодом 0, если же у нас была введена иная комманда, то просто выводим её в консоль с уровнем INFO, да, просто выводим.
Итог части #1.1
Данная часть была как-бы заменой #1 и в данной конечно из важного было:
-
Переход на Rust с языка Go
-
Основной язык проекта — Английский
-
Улучшение производительности в разы благодаря Rust
-
Создание логгера и ввода
-
Полная работа ядра пока в 2 потоках, а не как выходило в Go
Вот такие изменения я думаю стоили такого перехода, тем более учитывая, что Rust мне больше нравиться благодаря своей приближённости к устройству и отличная работа с ОЗУ:
Сервер пингуется легко, потребляя 128КБ на Linux, а на моём 4300U запускался за 735236ns.
Я надеюсь вам интересно читать статьи о разработке ядра и снова скажу:
Исходный код ядра доступен на GitHub и если вы хотите поддержать меня валютой, то у меня есть patreon.
Напишите можалуйста ваше мнение о моём процессе. Буду стараться отвечать на все.
Взаранее скажу, что плагины скоро буду вводить и они будут на основе WASM, скорее всего благодаря движку Wasmer.
A few weeks ago, YouTube recommended me a Minecraft video from Dream’s Channel in which he was trying to beat the game while his friend George attempted to stop him. That video was really fun and got me to explore more of their content.
Right now, there’s a bunch of people recording and uploading Minecraft videos to YouTube, but these two found a way to make their content different. Basically, they build their own plugins to change the rules of the game and then they record themselves attempting to beat the modified game. All I can say is that I love their content, and it’s awesome to see what you can accomplish with the power of code.
A few days later, I had an idea to develop a Minecraft mod and thought Why not? It will be fun!
Selecting the tools
Just like in Minecraft, we need some tools but in this case, they will help us in the creation process of our first mod.
Multiple tools help you build Minecraft mods, and I chose Fabric because one of the mods that I usually play with was built with it.
Minecraft uses Java and so does Fabric, which means we also need to have the Java Development Kit or JDK installed. To be more specific, we need the JDK 8 to be able to compile our mod. You can download it on this page.
Last but not least, we need to pick a code editor, in this case, I chose Visual Studio Code because it’s my favorite editor. Nevertheless, you can use whatever editor you like for this guide, as most of the steps will be performed in the CLI.
Setting up the project
For this guide, we will be using a starter to move quickly into actually building our first mod. Let’s go through the following steps:
1. Clone / Download the repository
If you use Git, just clone the repo:
$ git clone https://github.com/HorusGoul/fabric-mod-starter.git
Enter fullscreen mode
Exit fullscreen mode
Otherwise, click this link to download it.
2. Open the project folder with your code editor
Using Visual Studio Code:
$ code fabric-mod-starter
Enter fullscreen mode
Exit fullscreen mode
3. Open a terminal inside the project folder and run the client
$ cd fabric-mod-starter
$ ./gradlew runClient
Enter fullscreen mode
Exit fullscreen mode
NOTE: In Windows, you’ll need to run .gradlew.bat runClient
instead.
4. Check that everything is working
A Minecraft instance should be running now on your computer, and the console should have printed these two lines alongside others:
...
[main/INFO]: [STDOUT]: Hello Fabric world!
...
[main/INFO]: [STDOUT]: This line is printed by an example mod mixin!
...
Enter fullscreen mode
Exit fullscreen mode
If that’s not the case for you, recheck everything and if nothing seems to work, leave a comment or send me a PM and I’ll try to help you.
Getting to know the project
At the moment, we can already get our hands dirty by starting to code, but let’s get familiarized with some of the files.
gradle.properties
In this file, we can configure some values that will be used when building our mod. For example, we can change the Minecraft version, the fabric loader version, the mod version and other properties that we may need to change if we want to use new features of Fabric or Minecraft.
# Done to increase the memory available to gradle.
org.gradle.jvmargs=-Xmx1G
# Fabric Properties
# check these on https://fabricmc.net/use
minecraft_version=1.15.1
yarn_mappings=1.15.1+build.1
loader_version=0.7.3+build.176
# Mod Properties
mod_version = 1.0.0
maven_group = starter
archives_base_name = starter
# Dependencies
# currently not on the main fabric site, check on the maven: https://maven.fabricmc.net/net/fabricmc/fabric-api/fabric-api
fabric_version=0.4.25+build.282-1.15
Enter fullscreen mode
Exit fullscreen mode
src/main
Inside the src
folder, we will be able to find another folder called main
. That’s where the code and resources of our mod are located.
src/main/java
All the Java code is located inside this folder. There we can find a package called starter
which contains two items: the StarterMod.java
file and the mixin
package.
We can find the file StarterMixin.java
inside the mixin
package.
TIP: If you’re using Visual Studio Code, I recommend you to install the Java Extension Pack. It will power your editor with a bunch of utilities to make Java development easier.
StarterMod.java
This is the main entry point of our mod, as we can see, it belongs to the starter
package and implements the onInitialize()
method, which simply prints Hello Fabric world!
to the console.
package starter;
import net.fabricmc.api.ModInitializer;
public class StarterMod implements ModInitializer {
@Override
public void onInitialize() {
// This code runs as soon as Minecraft is in a mod-load-ready state.
// However, some things (like resources) may still be uninitialized.
// Proceed with mild caution.
System.out.println("Hello Fabric world!");
}
}
Enter fullscreen mode
Exit fullscreen mode
StarterMixin.java
This class belongs to the starter.mixin
package. Our mod will be really small so we shouldn’t worry a lot about the file structure of our project. Let’s just assume that all mixins will be located inside the starter.mixin
package.
And what are mixins?
Mixins are in charge of injecting code into existing classes of the game. For example, in StarterMixin.java
, we are injecting a method at the beginning (HEAD) of the init()
method that is implemented in the TitleScreen
class from Minecraft.
Now, if we load this mixin, once Minecraft calls the init()
method of TitleScreen
, our method that includes the System.out.println("This line is printed by an example mod mixin!");
will also be called!
That’s part of the magic of mixins, and this is just the tip of the iceberg, for now, this is all we need to build our mod. If you want more in-depth knowledge you should go check the Mixin docs.
package starter.mixin;
import net.minecraft.client.gui.screen.TitleScreen;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(TitleScreen.class)
public class StarterMixin {
@Inject(at = @At("HEAD"), method = "init()V")
private void init(CallbackInfo info) {
System.out.println("This line is printed by an example mod mixin!");
}
}
Enter fullscreen mode
Exit fullscreen mode
src/main/resources
Inside the resources
folder, we can find the assets
folder, which right now only contains the icon for our mod. Besides that folder, there are two JSON files:
fabric.mod.json
For this file, I recommend you to go into the Fabric docs about fabric.mod.json and read about every field defined inside the file of our project.
If you prefer to read the docs later, just take a look at the entrypoints
and mixins
properties.
I bet you can already see a connection here. In the entrypoints
is where we are telling Fabric which one of our Java classes should act as the main entry point of our mod.
And then, there’s the mixins
property, where we simply tell Fabric the location of any Mixin configuration file we want to include in our mod. In this case, we only have one, starter.mixins.json
.
{
"schemaVersion": 1,
"id": "starter",
"version": "${version}",
"name": "Starter Mod",
"description": "Describe your mod!",
"authors": ["Your Name"],
"contact": {
"homepage": "https://horuslugo.com",
"sources": "https://github.com/HorusGoul/fabric-mod-starter"
},
"license": "MIT",
"icon": "assets/starter/icon.png",
"environment": "*",
"entrypoints": {
"main": ["starter.StarterMod"]
},
"mixins": ["starter.mixins.json"],
"depends": {
"fabricloader": ">=0.7.3",
"minecraft": "1.15.x"
},
"suggests": {
"flamingo": "*"
}
}
Enter fullscreen mode
Exit fullscreen mode
starter.mixins.json
Remember our StarterMixin class? This is how we can tell the toolchain the mixins that we want to include in our mod. The package
property is where we define the Java package where the mixins are located, and inside the mixins
array is where we can put all the mixin classes that we want to include to the game.
Alongside mixins
there are two other properties that allow us to specify the environment where we want to load some mixins. Those properties are server
and client
, but in this case, we’re not using them.
This file follows the specification defined in the Mixin configuration files section of the Mixin Docs. Just the same as before, I recommend you to go to the docs and learn more about this file 😄
{
"required": true,
"package": "starter.mixin",
"compatibilityLevel": "JAVA_8",
"mixins": ["StarterMixin"],
"injectors": {
"defaultRequire": 1
}
}
Enter fullscreen mode
Exit fullscreen mode
Let’s build our mod!
Now that we are familiar with the project, let’s gets our hands dirty and create our mod!
In this case, the mod will simply alter one mechanic of the game: receiving damage. We’ll make it so whenever a player receives damage, it will switch its position and inventory with another player in the server.
For this, we’re going to need a mixin that injects code in the PlayerEntity
class, to be more specific, just before the end of the damage()
method.
Detecting when players receive damage
Let’s create this new mixin in the starter.mixin
package with the name SwitchPlayerEntityMixin.java
:
package starter.mixin;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import net.minecraft.entity.damage.DamageSource;
import net.minecraft.entity.player.PlayerEntity;
@Mixin(PlayerEntity.class)
public class SwitchPlayerEntityMixin {
@Inject(at = @At("RETURN"), method = "damage")
private void onDamage(DamageSource source, float amount, CallbackInfoReturnable info) {
System.out.println("The player received damage!");
}
}
Enter fullscreen mode
Exit fullscreen mode
Don’t forget to add it to the starter.mixins.json
file:
{
"required": true,
"package": "starter.mixin",
"compatibilityLevel": "JAVA_8",
"mixins": ["StarterMixin", "SwitchPlayerEntityMixin"],
"injectors": {
"defaultRequire": 1
}
}
Enter fullscreen mode
Exit fullscreen mode
Now execute the command ./gradlew runClient
in the console, launch a Minecraft world in creative mode, grab some instant damage potions and try to get hurt.
Just like in the GIF, you should be able to see a new line pop up in the console every time the player gets hurt, and that means we can continue to the explanation of what is going on.
Take a look to the mixin code, our purpose was to get the method onDamage
executed at the end of the method damage
, that’s why we are using the string RETURN
instead of HEAD
. Also, we are going to need the damage source
and the amount
of damage inflicted. The last parameter, info
is required by the Mixin framework.
Both the source
and amount
are parameters that the original damage
method receives, and that’s the reason we can just use them in our method.
Accessing the current player
Right now, the mod is just printing a line every time a player gets hurt, our next objective is accessing the player instance.
We must first remember that the onDamage
method is inside of a PlayerEntity
instance. We can take advantage of that and simply use this
to access the instance properties and methods. The problem comes when the compiler yells at us because it thinks that we’re an instance of SwitchPlayerEntityMixin
.
We don’t have a way to tell the compiler that this method is being executed inside of another type of class, so we can use this trick:
PlayerEntity self = (PlayerEntity) (Object) this;
Enter fullscreen mode
Exit fullscreen mode
With this, we are telling the compiler that this
is an Object
and then, we cast the object as a PlayerEntity
. And voilá! We have access to the player that is receiving damage, we can now update our printed line to display the player’s name.
...
@Mixin(PlayerEntity.class)
public class SwitchPlayerEntityMixin {
@Inject(at = @At("RETURN"), method = "damage")
private void onDamage(DamageSource source, float amount, CallbackInfoReturnable info) {
PlayerEntity self = (PlayerEntity) (Object) this;
System.out.println("The player " + self.getGameProfile().getName() + " received damage");
}
}
Enter fullscreen mode
Exit fullscreen mode
Switching positions with another player
Now that we can access the player’s properties and methods, we can use one of those to access the whole world
.
The world
property references the current Minecraft World that is being played, and one of the things we can do with it is getting the list of the online players.
With that list, we can pick one of those players and later, swap their positions as you can see in the following code:
package starter.mixin;
import java.util.List;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import net.minecraft.entity.damage.DamageSource;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.util.math.BlockPos;
@Mixin(PlayerEntity.class)
public class SwitchPlayerEntityMixin {
@Inject(at = @At("RETURN"), method = "damage")
private void onDamage(DamageSource source, float amount, CallbackInfoReturnable info) {
PlayerEntity self = (PlayerEntity) (Object) this;
// Get all the players in the current minecraft world
List<PlayerEntity> players = (List<PlayerEntity>) self.world.getPlayers();
// The player we'll switch positions with.
PlayerEntity otherPlayer;
// Stop the execution if the player is playing alone.
if (players.size() <= 1) {
return;
}
// Get a random player from the players list.
// Repeat this process until we have a player that is
// not the player who got hurt.
do {
int index = (int) Math.floor(Math.random() * players.size());
otherPlayer = players.get(index);
} while (otherPlayer == self);
// Get the block position of both players
BlockPos selfPos = self.getBlockPos();
BlockPos otherPlayerPos = otherPlayer.getBlockPos();
// Teleport damaged player to the other player's coordinates
// We set the Y to 300 in order to avoid a collision with the other player.
//
// We add 0.5 to both X and Z because that's the center point of a block
// and the players could suffocate under certain circumstances if we didn't
self.teleport(otherPlayerPos.getX() + 0.5, 300, otherPlayerPos.getZ() + 0.5);
// Teleport the other player to the position of the damaged player.
otherPlayer.teleport(selfPos.getX() + 0.5, selfPos.getY(), selfPos.getZ() + 0.5);
// Finally change the Y to the real value and complete the teleport of both
// players.
self.teleport(otherPlayerPos.getX() + 0.5, otherPlayerPos.getY(), otherPlayerPos.getZ() + 0.5);
}
}
Enter fullscreen mode
Exit fullscreen mode
After implementing this, you’ll need two Minecraft clients to be able to test it. You can do it by opening one with ./gradlew runClient
and then using the official Minecraft client with your Mojang account. Then, open the test world in LAN and join it with the other client.
Swapping their inventories
And now we’ll add the last feature of the mod: swapping the inventory of the players.
To swap the inventories of two players, we have to clone each inventory and after that, we can just replace and swap them. The inventory of a player can be accessed using the inventory
property.
The class PlayerInventory
has two methods that we’ll use, serialize
and deserialize
. The first one allows us to clone the content of the inventory by putting it inside of a ListTag
and then, we can use the second one to replace the content of an inventory with the content that is inside of a ListTag
.
Here’s the code:
// ... teleports ...
// Only swap inventories if the players are alive
if (self.getHealth() > 0.0f && otherPlayer.getHealth() > 0.0f) {
// Get the inventories of both players
ListTag selfInventory = self.inventory.serialize(new ListTag());
ListTag otherPlayerInventory = otherPlayer.inventory.serialize(new ListTag());
// Swap them
self.inventory.deserialize(otherPlayerInventory);
otherPlayer.inventory.deserialize(selfInventory);
}
Enter fullscreen mode
Exit fullscreen mode
As you may have noticed, we’re only swapping the inventories if both players are alive because if we don’t include this check, one of the inventories would get lost whenever a player died.
The final code
If you reached up until this point, congrats! You’ve built your first Minecraft mod, we should now remove the files that aren’t needed, for example, StarterMixin.java
and StarterMod.java
. Don’t forget to remove the references to these files inside fabric.mod.json
and starters.mixins.json
.
I also recommend you to rename the package from starter
to whatever you want, just remember to change every occurrence in the project.
You can find the latest version of the code in the branch final-code
of the starter repo. Click here see the final version of the mod.
Packaging the mod
If you’re familiar with Minecraft Mods, you may already know that mods usually come packaged inside .zip
or .jar
files which later you drop inside the mods
folder in the server or client of Minecraft.
To create a bundle of your mod, you only need to run the following command:
$ ./gradlew build
Enter fullscreen mode
Exit fullscreen mode
If everything compiles correctly, you’ll be able to find the .jar
inside the ./build/libs
folder of your project. In most cases, you’ll want to pick the production version without sources, but there may be cases where also shipping a development version along with the sources is better.
That’s it, you can now drop that .jar
inside your mods
folder, just don’t forget to install the Fabric API first, and for that, you can read the Installing Fabric section in their wiki if you want to learn how to do it.
Learning resources
Here are some resources that may come in handy if you want to learn more about Minecraft Modding:
-
The Fabric Wiki. This one has already been mentioned in the article, but seriously, go check it out because there’s a lot of content that I haven’t covered!
-
The Forge API. The Forge API is the most famous Minecraft API, you may want to check it out because some of the best mods out there have been built with it!
-
ScriptCraft. There seems to be a way to build mods using JavaScript, so if you have a background in web dev, you may want to try this one.
-
MCreator. As their site says, MCreator is a software used to make Minecraft mods and data packs using an intuitive easy-to-learn interface or with an integrated code editor. If you know about someone that wants to get started with programming, this may be a good way to introduce them!
Conclusion
Building my first Minecraft Mod was interesting because I got to know a bit more about how my favorite game works, and I even managed to craft something really fun to play.
Also, I took this as an opportunity to create this article because I think modding is a great way to get introduced to programming, and there are a lot of Minecraft players that may get interested and end up learning a lot about software development.
I hope you had a great time reading this article. If you decide to continue learning I invite you to share your progress with us, who knows, maybe a Minecraft Modding community can be born inside of dev.to 🔥
Назад к списку уроков
Скорее всего многие хотели бы увидеть тут что-то вроде:
Привет! Хочешь делать моды на Minecraft? Без проблем!
Но на деле первый шаг вас может разочаровать…
Шаг 1 — Выучить Java
Есть сомнения? А по мне звучит достаточно логично. Желая создавать моды — выучить язык, на котором эти моды и создают. Да и знание Java неизмеримо все упростит. Изучить его можно при помощи самых разнообразных источников. Это книги, курсы, обучающие видео на YouTube, официальная документация от Oracle или множество тематических сайтов.
Предсказывая будущее — далеко не все будут тратить время на такую мелочь. Ну да, всего-то язык, на котором и будет все написано. Так что я постараюсь объяснить все как можно понятнее и подробнее.
Но учтите! «Легкий путь» не даёт никаких гарантий понимания происходящего!
И да, некоторые пояснения по ходу написания кода не делают из этих статей хоть какую-то замену полноценному изучению Java.
Шаг 2 — Устанавливаем последнюю версию Java 8 JDK
Для начала нам нужно установить Java 8 JDK (Java Development Kit — Дословно: Комплект разработки Java).
Ссылки для загрузки: тут или тут.
Особо любопытные могут найти Java JDK других версий (11 там, или 13). Но все будет работать только с 8 JDK, потому устанавливаем именно её!
И еще, загружайте вашу версию!! Для 64 битных систем — 64 битные, не 32 (86)!
JDK содержит всё, что нам необходимо для работы, а именно: компилятор Java, стандартные библиотеки классов Java, документация, исполнительная система и т.д.
Шаг 3 — Установка IDE (Интегрированной среды разработки)
Для облегчения работы и удобства нужно установить IDE. Можно, конечно, и блокнотом обойтись. Но зачем?
IDE это интегрированная среда разработки. На рынке сейчас два главных представителя: IntelliJ Community Edition и Eclipse. Для загрузки просто ткните в ту, что понравилась 🙂
Свою симпатию я однозначно отдаю первой, так как думаю, что IntelliJ значительно превосходит Eclipse. Да и Android Studio, которую я знаю и люблю, создана на базе этой IDE.
Для тех, кому не принципиально — в IntelliJ все настроить куда проще. Да, и в уроках я буду использовать именно её.
Уделять особое внимание установке не буду. Там все предельно просто. Но если вдруг чего не выйдет — пишите в комменты, допишу.
Хух, с легкой частью закончили. Не думал же ты сразу вот так мод сварганить? Пффф. Это. Только. Начало.
П.С. Да не, шучу, через пару уроков уже начнём, не пугайтесь 😀
Несколько недель назад YouTube порекомендовал мне видео Minecraft с канала Dream , в котором он пытался победить в игре, в то время как его друг Джордж пытался остановить его. Это видео было действительно забавным и заставило меня изучить больше их контента.
Прямо сейчас есть куча людей, которые записывают и загружают видео Minecraft на YouTube, но эти двое нашли способ сделать свой контент другим. В принципе, они создают свои собственные плагины для изменения правил игры а затем они записывают себя, пытаясь победить в измененной игре. Все, что я могу сказать, это то, что мне нравится их контент, и это потрясающе – видеть, чего вы можете достичь с помощью кода.
Несколько дней спустя у меня появилась идея разработать мод для Майнкрафта и я подумал Почему нет? Это будет весело!
Выбор инструментов
Как и в Minecraft, нам нужны некоторые инструменты, но в этом случае они помогут нам в процессе создания нашего первого мода.
Несколько инструментов помогут вам создавать моды Minecraft, и я выбрал Fabric , потому что один из модов, с которыми я обычно играю, был создан с его помощью.
Minecraft использует Java, как и Fabric, что означает, что нам также необходимо установить набор для разработки Java или JDK. Чтобы быть более конкретным, нам нужен JDK 8, чтобы иметь возможность скомпилировать наш мод. Вы можете скачать его на этой странице .
И последнее, но не менее важное: нам нужно выбрать редактор кода, в данном случае я выбрал Код Visual Studio , потому что это мой любимый редактор. Тем не менее, вы можете использовать для этого руководства любой редактор, который вам нравится, так как большинство шагов будет выполнено в командной строке.
Настройка проекта
В этом руководстве мы будем использовать стартер, чтобы быстро перейти к созданию нашего первого мода. Давайте пройдем через следующие шаги:
1. Клонирование/Загрузка репозитория
Если вы используете Git, просто клонируйте репозиторий:
$ git clone https://github.com/HorusGoul/fabric-mod-starter.git
В противном случае нажмите на эту ссылку, чтобы загрузить его .
Хорусгул/ткань-мод-стартер
2. Откройте папку проекта с помощью редактора кода
Использование кода Visual Studio:
$ code fabric-mod-starter
3. Откройте терминал внутри папки проекта и запустите клиент
$ cd fabric-mod-starter $ ./gradlew runClient
записка: В Windows вам нужно будет запустить .gradlew.летучая мышь вместо этого запустите Клиент
.
4. Проверьте, все ли работает
Экземпляр Minecraft должен быть запущен сейчас на вашем компьютере, и консоль должна была напечатать эти две строки рядом с другими:
... [main/INFO]: [STDOUT]: Hello Fabric world! ... [main/INFO]: [STDOUT]: This line is printed by an example mod mixin! ...
Если это не так для вас, перепроверьте все, и если ничего не работает, оставьте комментарий или отправьте мне сообщение в личку, и я постараюсь вам помочь.
Знакомство с проектом
На данный момент мы уже можем испачкать руки, начав кодировать, но давайте ознакомимся с некоторыми файлами.
На данный момент мы уже можем испачкать руки, начав кодировать, но давайте ознакомимся с некоторыми файлами.
В этом файле мы можем настроить некоторые значения, которые будут использоваться при создании нашего мода. Например, мы можем изменить версию Minecraft, версию загрузчика ткани, версию мода и другие свойства, которые нам может потребоваться изменить, если мы хотим использовать новые функции Fabric или Minecraft.
# Done to increase the memory available to gradle. org.gradle.jvmargs=-Xmx1G # Fabric Properties # check these on https://fabricmc.net/use minecraft_version=1.15.1 yarn_mappings=1.15.1+build.1 loader_version=0.7.3+build.176 # Mod Properties mod_version = 1.0.0 maven_group = starter archives_base_name = starter # Dependencies # currently not on the main fabric site, check on the maven: https://maven.fabricmc.net/net/fabricmc/fabric-api/fabric-api fabric_version=0.4.25+build.282-1.15
src/главный
Внутри папки src
мы сможем найти другую папку с именем main
. Именно там находятся код и ресурсы нашего мода.
src/основной/java
Весь код Java находится внутри этой папки. Там мы можем найти пакет под названием starter
, который содержит два элемента: StarterMod.java
файл и смешивание
пакет.
Мы можем найти файл Мы можем найти файл
|/внутри
смешивания
совет: Если вы используете код Visual Studio, я рекомендую вам установить Пакет расширений Java . Это позволит вашему редактору использовать множество утилит, чтобы упростить разработку Java.
Это позволит вашему редактору использовать множество утилит, чтобы упростить разработку Java.
Это основная точка входа в наш мод, как мы видим, он относится к пакету starter
и реализует метод OnInitialize()
, который просто печатает Привет, мир ткани!
к консоли.
package starter; import net.fabricmc.api.ModInitializer; public class StarterMod implements ModInitializer { @Override public void onInitialize() { // This code runs as soon as Minecraft is in a mod-load-ready state. // However, some things (like resources) may still be uninitialized. // Proceed with mild caution. System.out.println("Hello Fabric world!"); } }
|| к консоли.
Этот класс относится к пакету starter.mixing
. Наш мод будет действительно маленьким поэтому нам не стоит сильно беспокоиться о файловой структуре нашего проекта. Давайте просто предположим, что все миксины будут расположены внутри пакета starter.mixing
.
А что такое смешивание?
Миксины отвечают за внедрение кода в существующие классы игры. Например, в StarterMixin.java
, мы вводим метод в начале (НАЧАЛО) метода init()
, который реализован в классе TitleScreen
из Minecraft.
Теперь, если мы загрузим это смешивание, как только Minecraft вызовет init()
метод |/TitleScreen , наш метод, который включает в себя
System.out.println (“Эта строка напечатана с помощью примера модного миксина!”); также будет вызван!
Это часть магии миксинов, и это только верхушка айсберга, на данный момент это все, что нам нужно для создания нашего мода. Если вам нужны более глубокие знания, вам следует проверить Документы по смешиванию .
package starter.mixin; import net.minecraft.client.gui.screen.TitleScreen; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(TitleScreen.class) public class StarterMixin { @Inject(at = @At("HEAD"), method = "init()V") private void init(CallbackInfo info) { System.out.println("This line is printed by an example mod mixin!"); } }
src/основные/ресурсы
Внутри папки ресурсы
мы можем найти папку активы
, в которой сейчас находится только значок нашего мода. Помимо этой папки, есть два файла JSON:
Помимо этой папки, есть два файла JSON:
Для этого файла я рекомендую вам перейти в Документы Fabric о fabric.mod.json и прочитайте о каждом поле, определенном в файле нашего проекта.
Если вы предпочитаете прочитать документы позже, просто взгляните на свойства точки входа
и миксины
.
Держу пари, вы уже видите здесь связь. В точках входа
мы указываем Fabric, какой из наших классов Java должен выступать в качестве основной точки входа нашего мода.
И затем, есть свойство mixins
, где мы просто сообщаем Fabric местоположение любого файла конфигурации микширования, который мы хотим включить в наш мод. В этом случае у нас есть только один, starter.mixins.json
.
{ "schemaVersion": 1, "id": "starter", "version": "${version}", "name": "Starter Mod", "description": "Describe your mod!", "authors": ["Your Name"], "contact": { "homepage": "https://horuslugo.com", "sources": "https://github.com/HorusGoul/fabric-mod-starter" }, "license": "MIT", "icon": "assets/starter/icon.png", "environment": "*", "entrypoints": { "main": ["starter.StarterMod"] }, "mixins": ["starter.mixins.json"], "depends": { "fabricloader": ">=0.7.3", "minecraft": "1.15.x" }, "suggests": { "flamingo": "*" } }
В этом случае у нас есть только один, ||starter.mixins.json||.
Помнишь наш Стартовый микс в классе? Вот как мы можем сообщить цепочке инструментов миксины, которые мы хотим включить в наш мод. Свойство package
– это место, где мы определяем пакет Java, в котором расположены миксины, а внутри массива mixins
мы можем разместить все классы миксина, которые мы хотим включить в игру.
Наряду с mixins
есть два других свойства, которые позволяют нам указать среду, в которую мы хотим загрузить некоторые миксины. Этими свойствами являются сервер
и клиент
, но в данном случае мы их не используем.
Этот файл соответствует спецификации, определенной в разделе Файлы конфигурации микширования Документации по микшированию. Точно так же, как и раньше, я рекомендую вам обратиться к документам и узнать больше об этом файле 😄
{ "required": true, "package": "starter.mixin", "compatibilityLevel": "JAVA_8", "mixins": ["StarterMixin"], "injectors": { "defaultRequire": 1 } }
Давайте создадим наш мод!
Теперь, когда мы знакомы с проектом, давайте испачкаем руки и создадим наш мод!
В этом случае мод просто изменит одну механику игры: получение урона . Мы сделаем так, чтобы всякий раз, когда игрок получает урон, он менял свою позицию и инвентарь с другим игроком на сервере.
Для этого нам понадобится микширование, которое вводит код в сущность игрока
класса, чтобы быть более конкретным, непосредственно перед окончанием метода damage()
.
Определение того, когда игроки получают урон
Давайте создадим это новое смешивание в starter.mix в
пакете с именем SwitchPlayerEntityMixin.java
:
package starter.mixin; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import net.minecraft.entity.damage.DamageSource; import net.minecraft.entity.player.PlayerEntity; @Mixin(PlayerEntity.class) public class SwitchPlayerEntityMixin { @Inject(at = @At("RETURN"), method = "damage") private void onDamage(DamageSource source, float amount, CallbackInfoReturnable info) { System.out.println("The player received damage!"); } }
Не забудьте добавить его в файл starter.mixins.json
:
{ "required": true, "package": "starter.mixin", "compatibilityLevel": "JAVA_8", "mixins": ["StarterMixin", "SwitchPlayerEntityMixin"], "injectors": { "defaultRequire": 1 } }
Теперь выполните команду ./gradlew запустите клиент
в консоли, запустите мир Minecraft в творческом режиме, возьмите несколько зелий мгновенного урона и попытайтесь получить травму.
Как и в GIF, вы должны видеть, как новая строка появляется в консоли каждый раз, когда игрок получает травму, и это означает, что мы можем продолжить объяснение происходящего.
Взгляните на код микширования, нашей целью было получить метод при повреждении
выполняется в конце метода повреждение
, вот почему мы используем строку вернуть
вместо ГОЛОВЫ
. Кроме того, нам понадобится ущерб источник
и сумма
нанесенного ущерба. Последний параметр, info
, требуется для структуры микширования.
Оба источника
и сумма
– это параметры, которые получает исходный метод ущерб
, и именно по этой причине мы можем просто использовать их в нашем методе.
Доступ к текущему игроку
Прямо сейчас мод просто печатает строку каждый раз, когда игрок получает травму, наша следующая цель – доступ к экземпляру игрока.
Сначала мы должны помнить, что метод onDamage
находится внутри Сущность игрока
экземпляр. Мы можем воспользоваться этим и просто использовать это
для доступа к свойствам и методам экземпляра. Проблема возникает, когда компилятор кричит на нас, потому что он думает, что мы являемся экземпляром Переключить Игрока На Миксин Сущности
.
У нас нет способа сообщить компилятору, что этот метод выполняется внутри класса другого типа, поэтому мы можем использовать этот трюк:
PlayerEntity self = (PlayerEntity) (Object) this;
При этом мы сообщаем компилятору, что это
является объектом
а затем мы разыгрываем объект как Сущность игрока
. И виллы! У нас есть доступ к игроку, который получает урон, теперь мы можем обновить нашу печатную строку, чтобы отобразить имя игрока.
... @Mixin(PlayerEntity.class) public class SwitchPlayerEntityMixin { @Inject(at = @At("RETURN"), method = "damage") private void onDamage(DamageSource source, float amount, CallbackInfoReturnable info) { PlayerEntity self = (PlayerEntity) (Object) this; System.out.println("The player " + self.getGameProfile().getName() + " received damage"); } }
Переключение позиций с другим игроком
Теперь, когда мы можем получить доступ к свойствам и методам игрока, мы можем использовать один из них для доступа ко всему миру
.
Свойство world
ссылается на текущий мир Minecraft, в который играют, и одна из вещей, которые мы можем с ним сделать, – это получить список онлайн-игроков.
С помощью этого списка мы можем выбрать одного из этих игроков, а затем поменять их местами, как вы можете видеть в следующем коде:
package starter.mixin; import java.util.List; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import net.minecraft.entity.damage.DamageSource; import net.minecraft.entity.player.PlayerEntity; import net.minecraft.util.math.BlockPos; @Mixin(PlayerEntity.class) public class SwitchPlayerEntityMixin { @Inject(at = @At("RETURN"), method = "damage") private void onDamage(DamageSource source, float amount, CallbackInfoReturnable info) { PlayerEntity self = (PlayerEntity) (Object) this; // Get all the players in the current minecraft world List players = (List) self.world.getPlayers(); // The player we'll switch positions with. PlayerEntity otherPlayer; // Stop the execution if the player is playing alone. if (players.size() <= 1) { return; } // Get a random player from the players list. // Repeat this process until we have a player that is // not the player who got hurt. do { int index = (int) Math.floor(Math.random() * players.size()); otherPlayer = players.get(index); } while (otherPlayer == self); // Get the block position of both players BlockPos selfPos = self.getBlockPos(); BlockPos otherPlayerPos = otherPlayer.getBlockPos(); // Teleport damaged player to the other player's coordinates // We set the Y to 300 in order to avoid a collision with the other player. // // We add 0.5 to both X and Z because that's the center point of a block // and the players could suffocate under certain circumstances if we didn't self.teleport(otherPlayerPos.getX() + 0.5, 300, otherPlayerPos.getZ() + 0.5); // Teleport the other player to the position of the damaged player. otherPlayer.teleport(selfPos.getX() + 0.5, selfPos.getY(), selfPos.getZ() + 0.5); // Finally change the Y to the real value and complete the teleport of both // players. self.teleport(otherPlayerPos.getX() + 0.5, otherPlayerPos.getY(), otherPlayerPos.getZ() + 0.5); } }
После реализации этого вам понадобятся два клиента Minecraft, чтобы иметь возможность протестировать его. Вы можете сделать это, открыв его с помощью ./gradlew runКлиент
а затем используйте официальный клиент Minecraft с вашей учетной записью Mojang. Затем откройте тестовый мир в локальной сети и присоединитесь к нему с другим клиентом.
Обмениваются своими запасами
А теперь мы добавим последнюю функцию мода: замена инвентаря игроков.
Чтобы поменять местами инвентарь двух игроков, мы должны клонировать каждый инвентарь, а после этого мы можем просто заменить и поменять их местами. Доступ к инвентарю игрока можно получить с помощью свойства инвентарь
.
Класс Инвентарь игрока
имеет два метода, которые мы будем использовать, сериализация
и десериализовать
. Первый из них позволяет нам клонировать содержимое инвентаря, поместив его внутрь тега List
и затем мы можем использовать второй, чтобы заменить содержимое инвентаря содержимым, которое находится внутри тега списка
.
Вот код:
// ... teleports ... // Only swap inventories if the players are alive if (self.getHealth() > 0.0f && otherPlayer.getHealth() > 0.0f) { // Get the inventories of both players ListTag selfInventory = self.inventory.serialize(new ListTag()); ListTag otherPlayerInventory = otherPlayer.inventory.serialize(new ListTag()); // Swap them self.inventory.deserialize(otherPlayerInventory); otherPlayer.inventory.deserialize(selfInventory); }
Как вы, возможно, заметили, мы меняем запасы только в том случае, если оба игрока живы, потому что, если мы не включим эту проверку, один из запасов будет потерян всякий раз, когда игрок умрет.
Окончательный код
Если вы дошли до этого момента, поздравляю! Вы создали свой первый мод для Minecraft, теперь мы должны удалить ненужные файлы, например, StarterMixin.java
и StarterMod.java
. Не забудьте удалить ссылки на эти файлы внутри fabric.mod.json
и стартеры.mixins.json
.
Я также рекомендую вам переименовать пакет из starter
в то, что вы хотите, просто не забудьте изменить каждое вхождение в проекте.
Вы можете найти последнюю версию кода в ветке окончательный код
начального репозитория. Нажмите здесь, чтобы увидеть окончательную версию мода .
Упаковка мода
Если вы знакомы с модами Minecraft, вы, возможно, уже знаете, что моды обычно поставляются упакованными внутри .zip
или .jar
файлы, которые позже вы поместите в папку mods
на сервере или клиенте Minecraft.
Чтобы создать комплект вашего мода, вам нужно всего лишь выполнить следующую команду:
Если все будет скомпилировано правильно, вы сможете найти файл .jar
внутри папки ./build/libs
вашего проекта. В большинстве случаев вы захотите выбрать производственную версию без источников, но могут быть случаи, когда также лучше отправить версию разработки вместе с источниками.
Вот и все, теперь вы можете отбросить эту .банку
внутри вашей папки mods
просто не забудьте сначала установить API Fabric, и для этого вы можете прочитать раздел Установка Fabric в их вики, если хотите узнать, как это сделать.
Учебные ресурсы
Вот некоторые ресурсы, которые могут пригодиться, если вы хотите узнать больше о моддинге Minecraft:
-
Ткань Вики . Об этом уже упоминалось в статье, но серьезно, пойдите и проверьте это, потому что есть много контента, который я не охватил!
-
API-интерфейс Forge . API Forge – это самый известный API Minecraft, вы можете проверить его, потому что с его помощью были созданы некоторые из лучших модов!
-
Сценарное мастерство . Похоже, есть способ создавать моды с использованием JavaScript, поэтому, если у вас есть опыт работы в веб-разработке, вы можете попробовать этот способ.
-
Создатель . Как говорится на их сайте, MCreator – это программное обеспечение, используемое для создания модов и пакетов данных Minecraft с использованием интуитивно понятного простого в освоении интерфейса или встроенного редактора кода. Если вы знаете о ком-то, кто хочет начать заниматься программированием, это может быть хорошим способом познакомить их!
Вывод
Создание моего первого мода для Minecraft было интересно, потому что я узнал немного больше о том, как работает моя любимая игра, и мне даже удалось создать что-то действительно интересное для игры.
Кроме того, я воспользовался этой возможностью для создания этой статьи, потому что я думаю, что моддинг – отличный способ познакомиться с программированием, и есть много игроков в Minecraft, которые могут заинтересоваться и в конечном итоге узнать много нового о разработке программного обеспечения.
Я надеюсь, что вы отлично провели время, читая эту статью. Если вы решите продолжить обучение, я предлагаю вам поделиться своим прогрессом с нами, кто знает, может быть в Minecraft моддинг сообщество может родиться внутри Дев.к 🔥
Оригинал: “https://dev.to/horusgoul/creating-a-minecraft-mod-using-java-and-fabric-3bmo”