Эдмунд МакМиллен вместе с Тайлером Глейеном запили игру про котиком, где надо их снашать, рожать и потом мутузить ими других котиков.
Так вот за три дня количество проданных копий превысили 500 тысяч экземпляров, что в итоге полностью позволило окупить затраченный на игру бюджет.
Классическая дилогия Dino Crisis вышла в Стим
Год тому назад нежданно негаданно в ГОГ вышли порты классической дилогии. Все удивились, кто-то порадовался, ну да и ладно. И тут вдруг эти же порты упали на полки парового магазина. Там куча технических фишек, типа поддержки современных мониторов, вшитой локализации на шесть языков, просто картинка поправлена. В общем, как будто стоит брать.
Новый хоррор от создателей «Дожить до рассвета»
Directive 8020 – именно так называется игра от Supermassive Games, анонсировали её давно уже, так что факт существования игры не новость. А вот новость то, что стала известна дата релиза оной.
Изначально я хотел добавить поддержку геймпада только на финальном этапе разработки, но получилось так, что это делается достаточно просто, поэтому засунул сразу.
Пойдём по порядку
function handleKeyDown(e) {
keys[e.code] = true; // аккумулируем все нажатия, чтоб потом обработать
// Предотвращаем прокрутку страницы стрелками и пробелом
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Space'].includes(e.code)) {
e.preventDefault();
}
}
function handleKeyUp(e) {
keys[e.code] = false; // "забываем" отпущенную кнопку
}
function handleGamepadConnected(e) {
console.log(`Gamepad connected: ${e.gamepad.id}`);
gamepadIndex = e.gamepad.index;
}
function handleGamepadDisconnected(e) {
console.log(`Gamepad disconnected: ${e.gamepad.id}`);
if (gamepadIndex === e.gamepad.index) {
gamepadIndex = null;
}
}
В отличии от клавиатуры, которая отдаёт браузеру события о нажатии на кнопка, с геймпадом такой финт ушами не прокатывает и надо опрашивать его состояние самостоятельно (прям как на привычных движках в старые добрые времена).
Дальше заспавним наш "танк" на игровой сцене
const spawnX = 4 * CELL_SIZE; // 4 клетки от левого края = 64 px
const spawnY = 24 * 8 - 32; // Чуть выше базы = 160 px
player = new Tank(spawnX, spawnY, TANK_PLAYER);
Класс танка достаточно большой, он ко конструкторе принимает значения координат спавна и типа танка (танк игрока или тип врага). Ему устанавливается "здоровье" (сколько раз в него надо попасть, чтобы убить), сколько активных патронов есть и т.д.
constructor(x, y, type = TANK_PLAYER) {
this.x = x;
this.y = y;
// Направление (0=вверх, 1=вправо, 2=вниз, 3=влево)
this.direction = DIR_UP;
this.speed = TANK_SPEED_NORMAL; // в пекселях за кадр
// Тип танка
this.type = type;
this.health = 1;
this.destroyed = false;
// Флаг движения в текущем кадре
this.moving = false; // Пока только задаётся, но нигде не используется. Есть мысли на будущее, но если не понадобится, удалю
// Апгрейды (для игрока)
this.bulletLevel = 0; // 0=обычная, 1=быстрая, 2=усиленная
this.bulletCount = 1; // Макс. пуль на экране
this.hasShield = false; // Временная защита
this.hasBoat = false; // Движение по воде
// Активные пули (для подсчёта лимита)
this.activeBullets = 0;
}
Непосредственно движение выгляди таким образом:
move(direction) {
// Сначала поворачиваем, если нужно
if (this.direction !== direction) {
this.turn(direction);
return; // В оригинале, если повернуть, танк не двигается в этом кадре, может уберу, если плейтесты покажут необходимость
}
// Вычисляем смещение по направлению
let dx = 0;
let dy = 0;
switch (direction) {
case DIR_UP: dy = -this.speed; break;
case DIR_DOWN: dy = this.speed; break;
case DIR_LEFT: dx = -this.speed; break;
case DIR_RIGHT: dx = this.speed; break;
}
// Новая позиция
let newX = this.x + dx;
let newY = this.y + dy;
// Ограничение границами игрового поля
newX = Math.max(0, Math.min(newX, LOGICAL_FIELD_SIZE - TANK_SIZE));
newY = Math.max(0, Math.min(newY, LOGICAL_FIELD_SIZE - TANK_SIZE));
// TODO: Проверка коллизий с тайлами (Этап 4)
// TODO: Проверка коллизий с другими танками (Этап 7)
// Применяем новую позицию
this.x = newX;
this.y = newY;
this.moving = true;
}
Ну и рисование танка на игровом поле
render() {
if (this.destroyed) return;
const ctx = foregroundCtx;
// Конвертация логических координат в физические
const px = GAME_FIELD_X + this.x * GAME_SCALE;
const py = GAME_FIELD_Y + this.y * GAME_SCALE;
const size = TANK_SIZE * GAME_SCALE;
// Цвет танка в зависимости от типа, пока всего два, потом будут новые типы и новые цвета
// Основной квадрат танка
ctx.fillStyle = this.type === TANK_PLAYER ? COLOR_TANK_PLAYER : COLOR_TANK_ENEMY;
ctx.fillRect(px, py, size, size);
// Индикатор направления (треугольник)
this.renderDirectionIndicator(ctx, px, py, size); // понял, что просто квадрато недостаточно, поэтому добавил "морду"
}
Рендер танка вызывается в методе основного рендера после очистки всего динамического слоя
// Foreground canvas — очищаем и рисуем сущности каждый кадр
clearForeground();
// Рендер танка игрока
if (player) {
player.render();
}
// TODO: Здесь будет рендер врагов, пуль, эффектов
Результат всего этого безобразия
Танк ездит сквозь стены, потому что "физического" движка ещё нет. Проверка коллизий как раз на очереди.
Следующим этапом сделаем систему отображения тайлов на игровом поле. Мы предполагаем, что у нас есть тайлы разных типов: кирпичи там, бетон, вода, лёд, кусты.
И чтобы эти самые блоки рисовать на игровом поле, нужно ввести сущность карты и самих блоков.
Сущность карты:
class GameMap {
constructor() {
// Сетка 26x26 субтайлов
this.tileGrid = [];
for (let y = 0; y < GRID_SIZE; y++) {
this.tileGrid[y] = new Array(GRID_SIZE).fill(TILE_EMPTY);
}
// Карта для хранения состояния повреждений разрушаемых тайлов
this.damageMask = new Map();
// Флаг необходимости перерисовки фона
this.dirty = true;
}
}
Ну и в нашем основном классе надо карту создать:
/** @type {GameMap} */
let gameMap = null;
...
// в методе init
// Создаём и загружаем карту
gameMap = new GameMap();
gameMap.loadLevel(TEST_LEVEL_1);
// Рисуем фон (карта + рамка)
renderBackground(gameMap);
gameMap.dirty = false;
Не знаю, пока, на сколько такой формат хранения данных корректен и будет удобен в использовании в дальнейшем, но сейчас он по крайне мере очень нагляден и удобен для ручного левел дизайна.
Рисование блоков достаточно просто:
export function renderBackground(gameMap) {
const ctx = backgroundCtx;
// Очищаем весь фон
clearBackground();
// Рамка игрового поля
renderGameFieldBorder();
// Рисуем все непустые тайлы
for (let y = 0; y < GRID_SIZE; y++) {
for (let x = 0; x < GRID_SIZE; x++) {
const tileId = gameMap.getTile(x, y);
if (tileId === TILE_EMPTY) continue;
const tileDef = TILE_DEFS[tileId];
if (!tileDef) continue;
// Логические координаты → физические координаты на canvas
const px = GAME_FIELD_X + x * TILE_SIZE * GAME_SCALE;
const py = GAME_FIELD_Y + y * TILE_SIZE * GAME_SCALE;
const size = TILE_SIZE * GAME_SCALE;
ctx.fillStyle = tileDef.color; // цвет в самих тайлах пока храню
ctx.fillRect(px, py, size, size); // потом буду спрайты рисовать
}
}
}
Хранятся тайлы у меня вот таким образом:
TILE_DEFS = {
[TILE_EMPTY]: {
id: TILE_EMPTY,
name: 'empty',
blocksTank: false,
blocksBullet: false,
destructible: false,
overlay: false,
color: COLOR_EMPTY // эта вся фигня в константах забита
},
...
У сущности карты есть метод для загрузки из вот того текстового бреда, который чуть выше скинут
loadLevel(levelData) {
for (let y = 0; y < GRID_SIZE; y++) {
for (let x = 0; x < GRID_SIZE; x++) {
const char = levelData[y]?.[x] || '.';
this.tileGrid[y][x] = charToTile(char);
}
}
// Инициализируем damageMask для всех разрушаемых тайлов
this.damageMask.clear();
for (let y = 0; y < GRID_SIZE; y++) {
for (let x = 0; x < GRID_SIZE; x++) {
const tileDef = TILE_DEFS[this.tileGrid[y][x]];
if (tileDef && tileDef.destructible) {
this.damageMask.set(`${x},${y}`, DAMAGE_FULL);
}
}
}
this.dirty = true;
console.log(`Level loaded. Destructible tiles: ${this.damageMask.size}`);
}
function charToTile(char) {
switch (char) {
case 'B': return TILE_BRICK;
case 'S': return TILE_STEEL;
case 'W': return TILE_WATER;
case 'F': return TILE_FOREST;
case 'I': return TILE_ICE;
case 'E': return TILE_BASE;
default: return TILE_EMPTY;
}
}
И вот такой вот получается итог
В целом, уже похоже на правду и с этим можно работать.
После прошлого поста я что-то так проникся происходящим, что решил воссоздать ещё одну любовь детства. Но как говорил Маркус Персон: «Если вы не можете написать свой движок, то гавно вы, а не разработчики». Там, скорее всего, было как-о иначе, но суть такая. И да, с этой мыслью я в корне не согласен, но написать свой движок – задача, как минимум, интересная. Я решил, что аркадная игра для этого подходит, как ничто другое. По сути, некий аналог движка уже был реализован в тетрисе, но здесь нужна будет и какая-никакая физическая модель, и интеллект врагов и рендер всего этого безобразия в несколько слоёв и с ФПС побольше, чем 2 :)
Так что, приступим. Что нам нужно:
Просчёт физики
Работа ИИ агентов
Считывание действий игрока
Отрисовка результата работы предыдущих пунктов.
Для одного поста такая задачка звучит жирновато, поэтому, видимо, будет серия.
Я решил разделить рисования окружения (статичных объектов), врагов и игроков (динамических объектов) и UI на три разных канваса, которые просто повещены один поверх другого. Там можно будет проще и меньше перерисовывать.
Начинается наш код с проверки, готова ли страница к явлению миру нашего движка.
Если готова, то давай же скорее всё проинициализируем
Тут всё просто:
function init() {
console.log('Battle City Remake - Initializing...');
// Инициализация рендерера
initRenderer();
// Очищаем все слои
clearBackground();
clearForeground();
clearUI();
// Рисуем границу игрового поля и отладочную сетку
renderGameFieldBorder();
renderDebugGrid();
console.log('Initialization complete. Starting game loop...');
// Запускаем game loop
requestAnimationFrame(gameLoop);
}
Но понятное дело, что кода тут мало, потому что всё вынесено в отдельные методы.
Есть ли среди них хоть что-то интересное?
Ну, renderDebugGrid и renderGameFieldBorder одним названием уже говорят, что там происходит.
Логично, что самое интересное происходит где-то тут requestAnimationFrame(gameLoop);
Но давайте сначала заглянем в инициализацию рендера
Метод update содержит (ну, будет содержать) всю нашу игровую логику. Пока он только считает фпс и выводит его на экран для отладки.
function update(dt) {
// Пока пусто - здесь будет логика игры
// Подсчёт FPS для отладки
frameCount++;
fpsTimer += dt;
if (fpsTimer >= 1000) {
fps = frameCount;
frameCount = 0;
fpsTimer = 0;
// Обновляем FPS на странице
const fpsElement = document.getElementById('fps');
if (fpsElement) {
fpsElement.textContent = fps;
}
}
}
Ну и когда всё закончено, отрисовываем
function render() {
// Background canvas - перерисовывается только при изменении карты
// (пока только отладочная сетка, нарисованная в init)
// Foreground canvas - очищаем и рисуем сущности каждый кадр
clearForeground();
// TODO: Здесь будет рендер танков, пуль, эффектов
// UI canvas - очищаем и рисуем UI каждый кадр
clearUI();
renderUI();
}
То есть, по сути, ничего интересного, просто рисую полосочки и квадратики для UI
Результат выглядит вот так
Следующий пост будет посвящён второму этапу
И да, я упоролся и расписал себе полноценный план реализации на 13 этапов. Без ДизДока последнее время вообще не представляю как работать. А раз уж я в команде один, то сам себе и ТЗ пишу... Но лучше так, чем лепить полную отсебятину.
Вопросы замечания предложения в комментариях жду с нетерпением)
Вчера я остро ощутил, что очень давно не писал ничего простого, но функционального и интересного. А ещё я тут всё про игры да про игры, поэтому решил: пора!
Последнее, что я писал подобного – это змейку, которую можно было наблюдать во время обновлений сайта раньше (помните её?).
Да и ту на половину, а то и на две трети написал за меня чатГПТ. Поэтому я решил, что возьму нового подопытного и реализую всё сам. Мозг не должен забывать, как делать штуки...
В качестве основы решил взять HTML5 и ванильный JS, как технологию с самым дешёвым вариантом рендера, да и повторить, при желании, этот же алгоритм можно на любом другом движке.
Начнём с простого: нам понадобится текстовый редактор любой. Хорошо, если с подсветкой синтаксиса. Для самого простого можно взять Notepad++, для случаев чуть серьёзнее Атом или ВС код.
Создаём html страничку, она будет нашей точкой входа, рядом с ней создаём файлик стилей и основной наш скриптец. Забегая вперёд: стили нам не то, чтобы прям сильно нужны.
Дожидаемся, пока вся страница загрузится, потом инициируем игру
Пока что к самому алгоритму игры мы не приступили, это всё предварительные ласки.
В первую очередь давайте посмотрим на класс GameField.
class GameField {
constructor(width, height) {
this.width = width;
this.height = height;
this.blocks = [];
for (let i = 0; i < this.height; i++) {
let line = [];
for (let j = 0; j < this.width; j++) {
line.push(0);
}
this.blocks.push(line); //Заполняем массив ячеек поля нулями. поле по умолчанию пустое
}
}
checkLines = async () => { // тут мы проверяем, а есть ли линии, которые надо сбросить
for (let y = this.height - 1; y >= 0; y--) {
if (this.currentLineIsFill(this.blocks[y])) {
currentScore++; //плюсуем очки
for (let x = 0; x < this.width; x++) {
this.blocks[y][x] = 0;
redraw();
await sleep(LINE_CLEAR_ANIMATION_DELAY); // это просто для красивой анимации исчезновения
}
}
}
currentScore = getComboPoints(currentScore); // этот метод отдельно, там считаем прирос за комбо
}
currentLineIsEmpty = (line) => {
for (let x = 0; x < line.length; x++) {
if (line[x] === 1) { // если хоть одна ячейка заполнена, то идёт нахер
return false;
}
}
return true;
};
currentLineIsFill = (line) => {
for (let x = 0; x < line.length; x++) {
if (line[x] === 0) { // если хоть одна ячейка пустая, строка идёт нахер
return false;
}
}
return true;
};
moveLines = () => { // сдвигаем после удаления
let notEmptyLines = [];
for (let y = 0; y < this.blocks.length; y++) {
if (!this.currentLineIsEmpty(this.blocks[y])) {
notEmptyLines.push([...this.blocks[y]]); // сначала заполняем массив непустымы строками
}
}
let emptyLine = [];
for (let x = 0; x < this.width; x++) {
emptyLine.push(0); // потом досоздаём массив пустых строк
}
let newLines = []; // запиххиваем их в новый массив
for (let x = 0; x < this.height - notEmptyLines.length; x++) {
newLines.push([...emptyLine]);
}
for (let x = 0; x < notEmptyLines.length; x++) {
newLines.push([...notEmptyLines[x]]);
}
this.blocks = newLines;
}
}
Когда же мы будем всё это дёргать? И почему?
В методе, который описывает игровой цикл. Я постарался программировать так, чтоб даже при современном подходе можно было бы реализовать близкий по сути алгоритм на реальном камне, где, как известно многопоточность лишь выдуманная.
Раз в какое-то количество миллисекунд мы вызываем геймЛуп. «Почему же не сетинтервал», – спросит неокрепших неофит. А всё по тому, что это чревато как раз внезапной неконтролируемой асинхронностью, которая приведёт к гонке за ресурсы и будут беды. Поэтому метод будет вызывать сам себя только после того, как точно закончит.
const gameLoop = async () => {
if (gameMode !== GameMode.PLAYING || isLoopRunning) {
return;
}
isLoopRunning = true;
if (currentFigure === null || typeof currentFigure === 'undefined') { // у нас нет фигуры? Так давай её сделаем из того, что стоит в очереди
currentFigure = new Figure();
currentFigure.cells = nextFigure.cells.map(row => [...row]);
currentFigure.type = nextFigure.type;
currentFigure.states = nextFigure.states.map(state => state.map(row => [...row]));
currentFigure.rotationState = nextFigure.rotationState;
currentFigure.setStartPosition();
nextFigure.fillRandom(); // следующую фигуру херакнули в какую-нибудь новую рандомную
redrawNextFigure();
}
currentFigure.moveDown(); //уронили я одну клеточку
if (currentFigure.checkCollision()) {//пересеклисьс чем-нибудь?
currentFigure.moveUp(); //подняли обратно и зафиксировали с мировом пространстве
const overflow = currentFigure.placeToField();
currentFigure = null;
if (overflow) {
finishGame('top_out');
redraw();
isLoopRunning = false;
return;
}
await field.checkLines(); // все фигуры на своих местах, можно проверить, как там у нас дела
score += currentScore;
scoreEl.innerText = score;
currentScore = 0;
field.moveLines(); // сдвинем, если надо сдвинуть
}
redraw();
isLoopRunning = false;
if (gameMode === GameMode.PLAYING) {
tickTimeout = setTimeout(gameLoop, getCurrentTickDelay()); // снова запускаем всё сначала
}
};
В целом, осталось только понять, что же такое фигура.
Сама геморная часть. Там больше всего буковок.
Фигура - это сущность, в которой хранится информация о том, какой она формы и как её крутить.
class Figure {
constructor() {
this.cells = [
[0],
];
this.x = 2;
this.y = -5;
this.rotationState = 0;
}
moveDown = () => {
this.y++;
};
moveUp = () => {
this.y--;
};
moveLeft = () => {
this.x--;
if (this.checkCollision()) {
this.x++;
}
}
moveRight = () => {
this.x++;
if (this.checkCollision()) {
this.x--;
}
}
fall = () => {
while (!this.checkCollision()) {
this.y++;
}
this.moveUp();
};
checkCollision = () => {
for (let i = 0; i < this.cells.length; i++) {
for (let j = 0; j < this.cells[i].length; j++) {
let cellX = j + this.x;
let cellY = i + this.y;
if (this.cells[i][j] === 0) {
continue;
}
// Проверяем границы по X всегда (независимо от Y)
if (cellX < 0 || cellX >= field.width) {
return true;
}
// Для клеток выше видимой области не проверяем коллизии с полем
if (cellY < 0) {
continue;
}
// Проверяем нижнюю границу и коллизии с заполненными клетками
if (cellY >= field.height) {
return true;
}
if (field.blocks[cellY][cellX] === 1) {
return true;
}
}
}
return false;
};
rotate(withCollisions = true) {
const from = this.rotationState;
const to = (from + 1) % this.states.length;
const key = `${from}>${to}`;
const kickSet = (this.type === "I")
? SRS_KICKS.I[key]
: (this.type === "O" ? SRS_KICKS.O[key] : SRS_KICKS.JLSTZ[key]);
const originalX = this.x;
const originalY = this.y;
let rotated = this.states[to];
if (!withCollisions) {
this.cells = rotated;
this.x = originalX;
this.y = originalY;
this.rotationState = to;
return;
}
for (const [dx, dy] of kickSet) {
this.cells = rotated;
this.x = originalX + dx;
this.y = originalY - dy;
if (!this.checkCollision()) {
this.rotationState = to;
return;
}
}
this.cells = this.states[from];
this.x = originalX;
this.y = originalY;
}
placeToField = () => {
let overflow = false;
for (let i = 0; i < this.cells.length; i++) {
for (let j = 0; j < this.cells[i].length; j++) {
let cellX = j + this.x;
let cellY = i + this.y;
if (this.cells[i][j] === 0) {
continue;
}
if (cellY < 0) {
overflow = true;
continue;
}
if (cellY >= field.height || cellX < 0 || cellX >= field.width) {
overflow = true;
continue;
}
field.blocks[cellY][cellX] = this.cells[i][j];
}
}
return overflow;
};
setStartPosition = () => {
this.x = Math.floor(field.width / 2) - Math.floor(this.cells[0].length / 2);
if (this.type === "I" && this.rotationState === 1) {
this.y = -2;
} else if (this.type === "I") {
this.y = -4;
} else if (this.type === "O") {
this.y = -2;
} else if (this.rotationState === 3) {
this.y = -2;
} else {
this.y = -3;
}
};
fillRandom = () => {
let figure = FIGURES[Math.floor(Math.random() * FIGURES.length)];
this.cells = figure.figure.states[0].map(row => [...row]);
this.states = figure.figure.states.map(state => state.map(row => [...row]));
this.type = figure.type;
this.rotationState = 0;
let rotateSteps = Math.floor(Math.random() * 4);
for (let i = 0; i < rotateSteps; i++) {
this.rotate(false);
}
};
}
Тут, конечно, кода дофига и надо объяснить, что тут происходит. Суть в том, что поворот в тетрисе – это прям отдельная задачка. Я её для себя упростил максимально, создав «спрайты» фигур во всех положениях заранее.
А ещё есть такая штука как SRS – это прям общепринятый стандарт вращения фигур. Специальные таблицы описывают как необходимо проверять смещение фигур в пространстве игрового поля при переходе из одного состояния в другое, на случай столкновения со стенами или существующими блоками в момент вращения. Я, опять-таки, эту часть упростил максимально, вырезав очень много из стандарта, так как у меня, как минимум, нет вращение против часовой стрелки.
Логика такая: после поворота к фигуре применяются смещения по иксу и игрику по очереди из массива, сначала 0-0 (не смещается), потом, к примеру 0-1 и так далее. За идеальное состояние, которое в данный момент всех устраивает, применяется то, после которого проверка коллизии фигуры показывает, что никто ни с кем не столкнулся. Если ни один из вариантов не подошёл, значит поворот не случился.
Классические таблицы подразумевают матрицы фигур одинакового размера и квадратные по своей сути. Я тут тоже отошёл от стандарта,
Как это в итоге играется
По коду там есть у меня есть усложнение с увеличением скорости падения фигур за каждые 30 полученных очков, сохранение рекорда и прочая мишура, которая к основному алгоритму отношения уже не имеет.
Как грицца: понятно, что нифига не понятно, так что спрашивайте, господа и дамы, отвечу на недостающие вопросы)
Прошлую неделю я с новостями профилонил, потому что писал фоксфрейм, сейчас снова возвращаюсь с подборкой того, что мне показалось самым интересным в индустрии.
Один модер против всей системы
Спойлер: он не победил.
Есть дяденька по имени Люк (Luke Ross), он делает уже несколько лет VR моды к разным популярным играм. В целом весьма неплохо делает, все довольны. Все, кроме авторов самих игр. Они говорят: «А чё это ты за свою работу деньги берёшь? Только мы имеем право получать деньги за то, что касается наших игр! Удоли!». Словил он страйк сначала за мод к киберпанку, потом за гостранер. В итоге он обиделся на весь этот корпоративный сброд и удалил все свои 40 модификаций со страницы на Патреоне.
Ситуация двоякая и интернет не встал единым фронтом на защиту модера и как история будет развиваться, пока не понятно (бьюсь об заклад, что никак).
Авторы «Диабло» делают «Диабло дома» за пределами близард
Moon Beast Productions – контора, которую основали дяди и тёти, которые когда-то раньше работали в метелице над диаблами. Теперь они делают новую игру Darkhaven, конечно же, в том же самом жанре
Пока это не более, чем анонс. Авторы планируют на днях или раньше начать сбор средств на кикстартере.
Новый героев помогает делать автор вселенной
HoMM olden era выглядит супер многообещающе (по крайней мере для меня). Но визуальный стиль – это далеко не всё, что нужно игре. Поэтому разработчики пригласили в качестве творческого консультанта Джона Ван Канегема, автора Might and Magic
Ремейк песков времени больше не ремейк
Юбисофт окончательно отчаялась и отменила долгострой, который многие фанаты с нетерпением ждали.
У Принца Персии не лучшие времена...
Stop Killing Games, возможно, даже выстрелит
Инициатива, если кто-то не в курсе, призывает разработчиков всех мастей обеспечить возможность играть в игры даже после того, как они перестали быть коммерчески успешными для издателей.
На сегодняшний день петиция с инициативой собрала порядка 1.3 миллионов подтверждённых пользователей, а в ЕС принято такие петиции официально рассматривать в еропарламенте.
Не далее как вчера товарищ @ZenitTTLMir1B писал о сложности загрузке видео при нестабильном соединении.
Я ночью всобачил на хостинг поддержку дозагрузки и отслеживания наличия сети.
Вот это видео было залито с принудительным разрывом соединения, переключением между сетями в процессе. Эксперимент считаю удавшимся, но если у вас возникнуть проблемы, дайте знать, будем чинить!
Студия Kinetic Games открыла издательское подразделение
Ребятки, разработавшие кооперативный ужастик Phasmophobia подумали и решили, что помощь начинающим разработчикам – это благое дело.
Даниель Найт, тот самый дяденька, который в одно лицо в своё время трудился над той самой фазмофобией, пришёл к выводу, что ему бы в тот момент такая поддержка ну вот вообще не помешала.
От себя лично неистово плюсую мужику, так как мой ход мыслей ровно точно такой же.
Лариан сказали, что решили отказаться от ИИ
Я ранее уже писал о скандалах, навалившихся на разработчиков из-за заявлений, что они периодически пользуются нейросетями для ускорения процесса. Интернет взбурлил говнами, и студия решила «ну его нахер». Заявили, что от греха подальше не будут больше так делать.
А мне лично кажется, они просто сказали разъярённой толпе то, что та хочет услышать. Отказываться от удобного инструмента просто потому что кто-то считает его «фу» – выстрел себе в колено.
Ну и кстати, о том, как толпа любит кого-нибудь в чём-нибудь обвинить.
Авторов Lords of the Fallen 2 обвинили в плагиате
Они показали картинку, как будет выглядеть один из персонажей.
вот так
И интернет такой: «О, я знаю откуда это спижжено!»
от сюда
Но глава студии сказал, что любители заговоров могут идти топтать газон в другом месте, а дизайн был разработал за долго до анонса НайтРейна (именно оттуда второй персонаж), а что до сходства, то... Ну... Они оба в броне и у них есть меч...
Вышла в релиз Мор 3 (Pathologic)
Не то, чтобы это прям полноценная третья часть, это сюжетная кампания за Бакалавра. То есть теперь у игроков есть возможность оценить весь контент ремейков Мора Утопии.
Детализация – это процесс, которым занимается уже не столько левел дизайнер, сколько левел артист. Напомню, что в англоязычной терминологии слово дизайн не про «красиво», а про «спроектировано». Соответственно красотой занимается художник по окружению, опираясь на результаты тестов, проведённые в результате построения грейбоксов и позднее вайтбоксов.
По итогу у нес есть вот такая невзрачная, но уже функциональная локация
Начинаем наваливать на неё детальки
И расставлять свет.
Свету будет посвящён отдельный пост, так что тут не буду заострять на нём внимание. Но основная мысль в украшении локации заключается в том, что нужно наваливать на неё деталей, до тех пор, пока влезает и при этом не нарушает общую художественную задумку и не портит геймплей.
Так выглядит локация в той игре, откуда я её срисовывал.
Чем отличается хорошая локация от плохой? Чет отличаются хорошие локации в старых играх от хороших локация в новых?
Количеством деталей. Чем больше каких-то мелкий элементиков, текстурок, деталей будет в поле зрения игрока, тем с большей охотой он готов будет поверить в увиденное.
При этом нужно понимать, что в некоторых случаях излишняя детализация может пойти во вред игровому процессу.
Mirror’s Edge. Тут всё ещё есть детали, позволяющие поверить в этот мир, но материалы и текстуры сделаны максимально стирильно.
Думаю, нет 100% верного совета, как нужно делать, так как это сильно зависит и от художественного стиля вашей игры и от механик и от отзывов конечных игроков.
Наверное мне просто хочется вывалить всё то, что сохранилось у меня в закромах. Я отчётливо понимаю, что есть те вещи, которые ну никак не получится реализовать здесь и сейчас, а потом... А потом, когда, скорее всего, появятся ресурсы на реализацию, я с большим удовольствием потрачу их на что-то другое.
Что же такое Dissidentity? Вообще, это игра слов Диссидент и Идентичность.
Как родилась идея и что это вообще всё значит?
Ещё на заре моих попыток делать игры, в одно прекрасно утро (может ли оно быть прекрасным, если я тогда работал в офисе?) я поделился с коллегой своей страстью. Коллега зацепился за мысль и вместе спустя небольшой мозговой штурм было принято решение, что нужно делать хоррор?
Почему? Да потому что страшилка – это жанр, который требует меньше всего ресурсов и его вполне реально поднять в 1-3 человека в не слишком длинные сроки. Я должен был заниматься непосредственно производством, коллега ведением проекта, тестированием и небольшой помощью в производстве, его жена должна была стать сценаристом (я в тот момент времени ещё не писал книги, а если и писал, то об этом никто кроме жены и пары друзей не знали).
Звучало реально и правдоподобно, но тут на сцену вышли амбиции (а моя безудержная фантазия). Я не могу позволить себе делать что-то заурядное, мне скучно, поэтому я подумал: «А что если во главе сюжета будет стоять раздвоение личности?». Это та самая идентичность в названии (Диссоциативное расстройство идентичности – настоящее название заболевания (Dis - в названии в том числе отсюда, а не только из слова диссидент)).
Интересно? Допустим. Мне показалось этого мало. Мы играем за обычного преподавателя в университете. Игровой процесс начинается именно с того, что мы буквально должны провести лекцию студентам, но потом, покинув аудиторию, персонаж бы оказывался в какой-то искажённой версии универа. Как после апокалипсиса, вызванного открытием врат в Ад. И задачей игрока с этого момента будет поиск выхода из опасного для жизни места. Какие-то фантасмагоричные монстры, ядовитый газ, меняющееся пространство становятся препятствием на пути к выходу, но вот, наконец, выход найден, персонаж открывает дверь и обнаруживает себя на крыше здания. Вокруг собралась толпа зевак, полицейские пытаются арестовать героя, а он, дабы не быть арестованным решает сбросится с крыши. Конечно же ни игрок ни герой ничерта не понимают. Падение с крыши происходит случайно, оступился дяденька от растерянности. Но он не умирает, а падает в какое-то липкое тёмное болото, которое расопложилось рядом с домом, где прошло детство главного героя.
И там его ждут новые испытания. Суть в том, что мы, как игрок, всегда играем за одну и ту же личность. Просто игра пытается фантазировать на тему, что происходит с той личностью, которая в данный момент неактивна. И действие, соответственно, происходит внутри больного сознания героя, где он должен победить монстров и боссов, отражения реальных проблем, которые и стали причиной того, что он поехал кукушечкой.
Что же пошло не так?
Жена коллеги, будучи неопытной писательницей, да к тому же, не особенно проникшаяся идеей, достаточно быстро выгорела. Коллега только и делал что думал о том, как найти издателя/инвестора/спонсора. Ведением проекта не занимался даже чуть-чуть. Ну и я очень быстро пришёл к выводу, что каши тут не сварится, и проект забросил, однако, для него было написано несколько заготовок суандтреков. Как своими силами, так и с помощью товарища. И я сделал самую первую локацию. Дальше дело не дошло...
думаю, что да
предыдущие зашли, особенно четвёртая.
и вторая за свой сюжетный твист в конце тож хороша прям))
::blush::
Красивое)))
Можно было поступить как в кино: передвинуть труп на 10 метров за границу соседнего района достать чемодан на коридор и дальше это проблема проводника