Гра: "Дух садівник: Нірвана"
ШІ в багатьох сферах дає реалізацію творчого потенціалу. Якщо раніше на програмування та створення якоїсь гри потрібно було пройти цілий курс, хоча б початковий, то зараз обійтись лише запитом із потрібною ідеєєю до потрібного генеративного інструменту. В Google AI Studio можна безоплатно користуватись потужностями та різними можливостями. Все надається на правах тестування, тому й безоплатно, бо ніяких гарантій у майбутньому. Короче це цікаве місце, де можна отримати досвід в деяких штуках. Може не найкращий, у порівнянні із платними підписками вже перевірених і заточених сервісів під подтібну задачу, але для проби саме те.
Отже один із інструментів, це p5playground для створення всяких приколів, артів, ігор чи ще чогось. Може напишу детальніше.
Отже пробуючи створити в ньому гру, подібну до Кобзи по вгадуванню слів, але зі своїм набором слів – словником. І вискочила ідея про те щоб, щось літало й пуляло насінням в землю, яке б проростало й звеселяло. Така собі пісочниця й невелика розрада. Насіння двох видів квіти й дерева. Бо багатьсоко ігор показують шкідництво різного роду, а тут хороше діло.
Гра про духа садівника (так назвала ШІ). Це сфера, яка літає, переливається кольорами й десантує насіння в землю. Є режим одиночних скидів та кулеметних чи килимових. Окрім цього, є деякі події, якщо зібрати певну кількість квітів Папороті, які ростуть на деревах. Нічого шукати логіку так захотілось, хоча на вид це блимаючі кульки – найбільші.
Отак потрібно бомбардувати землю, щоб там росли дерева й квіти! ))
Реалізація теж проста, по суті можна обійтись одни html файлом, щоб було простіше, але це для онлайну. Щоб обійтись без інтернету, потрібно заванртажити ще p5.min.js
бібліотеку, що все працювало. Вона складається із одного файлу десь 1 МБ. І вуаля. Ще була думка, щоб на фоні грала якась мелодія заспокійлива, але до того діло повноцінно не дійшло чи скрпипт не правильно зроблено. Тож гру можна легко запускати в браузері. Щоб грати на смартфоні, найкраще вигружати на веб сервер як веб сторінку. Принаймні просто так воно не хоче працювати в мобільному браузері, мабуть захист. Локальін рішення є, але це вже танці із бубном.
В процесі проб і редагувань ШІ на мої запити, було додано ще окремий скрипт із рівнями, так може на майбутнє, щоб урізноманітнити процес якимись цікавими подіями. Бо там вигулькує сонечко, Бог садить великого дуба, й перехід у Нірвану, якщо зібрати багацько квіточок папороті)).
Вийшло смикнути для себе код. Архів розпакувати, в папці запустити html файл (це для себе). Проте всі бажаючі теж можуть бавитись.
- натиснення на лінк запропонує відразу завантажити або зробить це автоматично.
Шо тут ще сказать. Щоб зробити таку гру, треба пройти хочаб базовий кур чогось там. Наприклад, тут на сайті за 5 тижнів ми писали просту гру вгадування чисел (в мене із ШІ на пару). Для цього ще більше. Натомість технології дають трішки більше реалізіції своїм фантазіям. ))
Або створити порожній файл html і вставити туди код, що нижче, там для онлайн запуску. Тільки тоді все разом і одиночні й чергою може сернуть.
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Дух Садівника: Медитація</title>
<style>
body { margin: 0; overflow: hidden; background-color: #000; font-family: sans-serif; }
canvas { display: block; }
#start-screen { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.7); color: white; display: flex; justify-content: center; align-items: center; text-align: center; font-size: 2em; cursor: pointer; z-index: 100; }
#ui-container { position: absolute; top: 10px; left: 10px; z-index: 10; color: white; text-shadow: 2px 2px 4px #000; max-width: 250px; }
.ui-box { background-color: rgba(0, 0, 0, 0.5); padding: 8px; border-radius: 5px; margin-bottom: 10px; }
#music-upload { display: none; }
#music-upload-label { cursor: pointer; padding: 5px 10px; background-color: #5a9; border-radius: 5px; }
#music-upload-label:hover { background-color: #6ba; }
</style>
</head>
<body>
<div id="start-screen">
<div><h1>Дух Садівника</h1><p>Натисніть, щоб почати</p></div>
</div>
<div id="ui-container">
<div id="game-stats" class="ui-box"></div>
<div id="music-upload-wrapper" class="ui-box">
<label for="music-upload" id="music-upload-label">Обрати мелодію</label>
<input type="file" id="music-upload" accept="audio/*">
<p id="music-status" style="font-size: 0.8em; margin-top: 5px;">Музика не грає</p>
</div>
</div>
(html comment removed: ========================================================== )
(html comment removed: ▼▼▼ ТУТ РЕДАГУВАТИ ТЕКСТИ ДЛЯ ОБ'ЄКТІВ ▼▼▼ )
(html comment removed: ========================================================== )
<div id="story-data" style="display: none;">
<div id="sun-description">
Це Усміхнене Сонце. Його промені несуть тепло та радість у кожен куточок саду. Воно з'являється тоді, коли гармонія починає панувати, нагадуючи про світло всередині кожного з нас.
</div>
<div id="oak-description">
Великий Дуб - це символ сили, мудрості та зв'язку між небом і землею. Його коріння глибоко в землі, а гілки тягнуться до зірок. <br><br>Посидь під ним, відчуй його спокій.
</div>
</div>
(html comment removed: ========================================================== )
(html comment removed: ▲▲▲ КІНЕЦЬ БЛОКУ РЕДАГУВАННЯ ▲▲▲ )
(html comment removed: ========================================================== )
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/2.0.3/p5.min.js"></script>
<script src="p5.sound.js"></script>
<script>
let spirit;
let seeds = [], saplings = [], flowers = [], trees = [], fernFlowers = [], collectibles = [];
let seedAmmo = 50, saplingAmmo = 10, fernFlowersCollected = 0, treeCounter = 0;
let groundLevel;
let gameState = 'start';
let bgMusic;
let firingMode = 'single', lastShotTime = 0;
const burstFireCooldown = 100;
let nirvanaState = { achieved: false, startTime: 0, alpha: 0 };
let nirvanaParticles = [];
// Об'єкти для подій тепер завантажують опис при старті
let smilingSun = { active: false, description: "", isInfoVisible: false };
let godEvent = {
triggered: false, active: false, godY: -100, godTargetY: 0,
oakGrowth: 0, finalOakHeight: 0, oakCanopyLeaves: [],
description: "", isInfoVisible: false
};
function setup() {
createCanvas(windowWidth, windowHeight);
colorMode(HSB, 360, 100, 100, 1);
// Завантажуємо описи з прихованих HTML-елементів
smilingSun.description = select('#sun-description').html();
godEvent.description = select('#oak-description').html();
select('#start-screen').mousePressed(() => {
if (gameState === 'start') {
gameState = 'playing';
select('#start-screen').hide();
initializeGame();
}
});
select('#music-upload').changed(handleMusicFile);
}
function draw() {
if (gameState === 'playing') runGameLogic();
else if (gameState === 'nirvana') runNirvanaSequence();
else background(10, 80, 20);
}
function runGameLogic() {
drawBackgroundAndGround();
handleShooting();
drawPlants();
if (smilingSun.active) drawSmilingSun();
if (godEvent.active) runGodEvent();
spirit.handleInput(); spirit.update(); spirit.display();
handleProjectiles(seeds, 'flower'); handleProjectiles(saplings, 'tree');
handleCollectibles();
handleEvents();
drawUI();
}
function drawBackgroundAndGround() { background(195, 50, 95); noStroke(); fill(100, 80, 50); rect(0, groundLevel, width, height - groundLevel); }
function drawPlants() { for (let t of trees) t.display(); for (let f of flowers) f.display(); for (let ff of fernFlowers) ff.display(); }
function drawUI() {
let statsDiv = select('#game-stats');
statsDiv.html(`<p>Насіння (Z): ${seedAmmo}</p><p>Саджанці (X): ${saplingAmmo}</p><p>Квітки Папороті: ${fernFlowersCollected} / 50</p><p>Режим (M): ${firingMode === 'single' ? 'Одиночний' : 'Чергою'}</p>`);
}
function handleEvents() {
// Події тепер просто "вмикають" об'єкти, опис вже завантажено
if (fernFlowersCollected >= 3 && !smilingSun.active) {
smilingSun.active = true;
}
if (fernFlowersCollected >= 5 && !godEvent.triggered) {
godEvent.triggered = true; godEvent.active = true;
}
if (fernFlowersCollected >= 50 && !nirvanaState.achieved) {
nirvanaState.achieved = true; nirvanaState.startTime = millis(); gameState = 'nirvana';
if (bgMusic && bgMusic.isPlaying()) bgMusic.fade(0, 3);
}
}
function drawSmilingSun() {
let sunX = width - 100, sunY = 100;
push();
translate(sunX, sunY);
let sunSize=120;let rayLength=sunSize*0.8+sin(frameCount*0.05)*10;strokeWeight(4);stroke(55,80,100,0.7);for(let i=0;i<12;i++){let angle=TWO_PI/12*i;line(cos(angle)*(sunSize/2),sin(angle)*(sunSize/2),cos(angle)*rayLength,sin(angle)*rayLength);}noStroke();fill(55,90,100);ellipse(0,0,sunSize,sunSize);fill(0);ellipse(-25,-15,10,15);ellipse(25,-15,10,15);noFill();stroke(0);strokeWeight(4);arc(0,0,60,60,0.1*PI,0.9*PI);
pop();
drawInfoButtonAndText(sunX + 60, sunY - 60, smilingSun);
}
function runGodEvent() {
fill(0,0,100,0.8);noStroke();ellipse(width/2,-20,300,100);if(godEvent.godY<godEvent.godTargetY){godEvent.godY=lerp(godEvent.godY,godEvent.godTargetY+20,0.05);}else if(godEvent.oakGrowth<godEvent.finalOakHeight){godEvent.oakGrowth=lerp(godEvent.oakGrowth,godEvent.finalOakHeight+20,0.02);if(godEvent.oakCanopyLeaves.length===0&&godEvent.oakGrowth>godEvent.finalOakHeight*0.9)generateGreatOakCanopy();}fill(60,80,100,0.9);ellipse(width/2,godEvent.godY,40,80);
if (godEvent.oakGrowth > 1) drawGreatOak();
}
function drawGreatOak() {
let oakX=width/2;let oakBaseY=groundLevel;let trunkH=godEvent.oakGrowth*0.6;let trunkW=40;let canopyBaseY=oakBaseY-trunkH;let canopySize=godEvent.oakGrowth*0.8;push();noStroke();fill(25,80,30);arc(oakX,oakBaseY+5,trunkW*2,trunkW,PI,TWO_PI);for(let i=0;i<10;i++){fill(30,80,40-i*2);rect(oakX-trunkW/2+i,oakBaseY-trunkH,trunkW-i*2,trunkH);}for(const leaf of godEvent.oakCanopyLeaves){fill(leaf.hue,70,leaf.brightness);ellipse(oakX+leaf.xOff,canopyBaseY+leaf.yOff,canopySize*leaf.sizeMod,canopySize*leaf.sizeMod);}pop();
if (godEvent.oakGrowth > godEvent.finalOakHeight * 0.5) {
drawInfoButtonAndText(oakX + 40, oakBaseY - 60, godEvent);
}
}
function drawInfoButtonAndText(buttonX, buttonY, targetObject) {
push();
let d=dist(mouseX,mouseY,buttonX,buttonY);strokeWeight(2);stroke(0,0,0,0.5);if(d<12){fill(200,80,100);}else{fill(0,0,100);}ellipse(buttonX,buttonY,24,24);fill(0);noStroke();textAlign(CENTER,CENTER);textSize(16);textStyle(BOLD);text('i',buttonX,buttonY-1);pop();
if (targetObject.isInfoVisible) {
push();let boxWidth=300;let boxHeight=120;let boxX=constrain(buttonX-boxWidth/2,10,width-boxWidth-10);let boxY=constrain(buttonY+20,10,height-boxHeight-10);fill(0,0,10,0.8);stroke(0,0,100,0.8);strokeWeight(2);rect(boxX,boxY,boxWidth,boxHeight,10);noStroke();fill(0,0,100);textSize(16);textAlign(LEFT,TOP);textStyle(NORMAL);text(targetObject.description,boxX+10,boxY+10,boxWidth-20,boxHeight-20);pop();
}
}
function mousePressed() {
if (gameState !== 'playing') return;
if (smilingSun.active && dist(mouseX, mouseY, width - 100 + 60, 100 - 60) < 12) smilingSun.isInfoVisible = !smilingSun.isInfoVisible;
if (godEvent.oakGrowth > godEvent.finalOakHeight * 0.5 && dist(mouseX, mouseY, width / 2 + 40, groundLevel - 60) < 12) godEvent.isInfoVisible = !godEvent.isInfoVisible;
}
function runNirvanaSequence(){let elapsed=millis()-nirvanaState.startTime;nirvanaState.alpha=min(1,elapsed/5000);background((frameCount/2)%360,50,100,nirvanaState.alpha*0.6);if(frameCount%5===0){for(let i=0;i<3;i++)nirvanaParticles.push(new NirvanaParticle());}for(let i=nirvanaParticles.length-1;i>=0;i--){let p=nirvanaParticles[i];p.update();p.display(nirvanaState.alpha);if(p.isDead())nirvanaParticles.splice(i,1);}push();translate(width/2,height/2);let baseSize=min(width,height)*0.4;noFill();strokeWeight(2);for(let i=0;i<6;i++){let rotation=frameCount*(i%2===0?0.005:-0.007)*(i+1)*0.3;let size=baseSize*(1-i*0.15);let hue=(frameCount+i*40)%360;stroke(hue,80,100,nirvanaState.alpha*0.8);push();rotate(rotation);for(let j=0;j<8;j++){rotate(TWO_PI/8);line(0,size*0.2,0,size);ellipse(0,size*0.15,size/10);}pop();}pop();if(elapsed>4000){let textAlpha=min(1,(elapsed-4000)/2000);textAlign(CENTER,CENTER);textSize(48);fill(60,100,100,textAlpha*0.5);text("Мир у всьому світі досягнуто",width/2+2,height/2+2);fill(0,0,100,textAlpha);text("Мир у всьому світі досягнуто",width/2,height/2);}}
function initializeGame(){groundLevel=height*0.85;spirit=new GardenerSpirit(width/2,groundLevel-100);godEvent.godTargetY=groundLevel-80;godEvent.finalOakHeight=height*0.6;}
function handleMusicFile(file){if(bgMusic)bgMusic.stop();bgMusic=loadSound(file,()=>{bgMusic.loop();select('#music-status').html('Музика грає...');});}
function handleShooting(){if(firingMode==='burst'){if(keyIsDown(90)){if(millis()>lastShotTime+burstFireCooldown&&seedAmmo>0){seeds.push(new Projectile(spirit.x,spirit.y,'seed'));seedAmmo--;lastShotTime=millis();}}if(keyIsDown(88)){if(millis()>lastShotTime+burstFireCooldown&&saplingAmmo>0){saplings.push(new Projectile(spirit.x,spirit.y,'sapling'));saplingAmmo--;lastShotTime=millis();}}}}
function keyPressed(){if(gameState!=='playing')return;if(firingMode==='single'){if(key==='z'||key==='Z')if(seedAmmo>0){seeds.push(new Projectile(spirit.x,spirit.y,'seed'));seedAmmo--;}if(key==='x'||key==='X')if(saplingAmmo>0){saplings.push(new Projectile(spirit.x,spirit.y,'sapling'));saplingAmmo--;}}if(key==='m'||key==='M'){firingMode=(firingMode==='single')?'burst':'single';}}
function windowResized(){resizeCanvas(windowWidth,windowHeight);if(gameState!=='start')initializeGame();}
function generateGreatOakCanopy(){godEvent.oakCanopyLeaves=[];let oakX=width/2;let trunkH=godEvent.finalOakHeight*0.6;let canopyBaseY=groundLevel-trunkH;let canopySize=godEvent.finalOakHeight*0.8;for(let i=0;i<25;i++){godEvent.oakCanopyLeaves.push({hue:120+random(-10,10),brightness:60+random(-10,10),xOff:random(-canopySize*0.35,canopySize*0.35),yOff:random(-canopySize*0.25,canopySize*0.25),sizeMod:random(0.4,0.6)});}}
function handleProjectiles(arr,sproutsInto){for(let i=arr.length-1;i>=0;i--){let p=arr[i];p.update();p.display();if(p.y>groundLevel){if(sproutsInto==='flower')flowers.push(new Flower(p.x,groundLevel));else if(sproutsInto==='tree'){let newTree=new Tree(p.x,groundLevel);trees.push(newTree);treeCounter++;if(treeCounter%3===0)fernFlowers.push(new FernFlower(newTree.x,newTree.y-newTree.canopyYOffset-newTree.canopyHeight/2));}arr.splice(i,1);}}}
function handleCollectibles(){handleGrownPlants();for(let i=collectibles.length-1;i>=0;i--){let c=collectibles[i];c.update();c.display();if(dist(spirit.x,spirit.y,c.x,c.y)<spirit.size/2+10){if(c.type==='seed')seedAmmo+=5;if(c.type==='sapling')saplingAmmo+=1;collectibles.splice(i,1);}}}
function handleGrownPlants(){for(let t of trees)t.update();for(let f of flowers)f.update();for(let i=fernFlowers.length-1;i>=0;i--){let ff=fernFlowers[i];if(dist(spirit.x,spirit.y,ff.x,ff.y)<spirit.size/2+15){fernFlowersCollected++;fernFlowers.splice(i,1);}}}
class NirvanaParticle{constructor(){this.pos=createVector(width/2,height/2);let angle=random(TWO_PI);let speed=random(1,4);this.vel=p5.Vector.fromAngle(angle,speed);this.lifespan=255;this.hue=random(360);this.size=random(2,6);}update(){this.pos.add(this.vel);this.vel.mult(0.99);this.lifespan-=1.5;}display(globalAlpha){noStroke();fill(this.hue,90,100,this.lifespan/255*globalAlpha);ellipse(this.pos.x,this.pos.y,this.size);}isDead(){return this.lifespan<0;}}
class GardenerSpirit{constructor(x,y){this.x=x;this.y=y;this.size=40;this.speed=5;this.angle=0;}handleInput(){if(keyIsDown(LEFT_ARROW))this.x-=this.speed;if(keyIsDown(RIGHT_ARROW))this.x+=this.speed;if(keyIsDown(UP_ARROW))this.y-=this.speed;if(keyIsDown(DOWN_ARROW))this.y+=this.speed;this.x=constrain(this.x,this.size/2,width-this.size/2);this.y=constrain(this.y,this.size/2,groundLevel-this.size/2);}update(){this.angle+=0.05;}display(){push();translate(this.x,this.y);let currentHue=map(sin(frameCount*0.02),-1,1,45,315);let pulse=sin(this.angle)*5;fill(currentHue,80,100,0.3);noStroke();ellipse(0,0,this.size+pulse,this.size+pulse);fill(currentHue,90,100);ellipse(0,0,this.size-5,this.size-5);pop();}}
class Projectile{constructor(x,y,type){this.x=x;this.y=y;this.type=type;this.gravity=0.1;if(this.type==='seed'){this.vy=3+random(-1,1);this.vx=random(-2,2);this.size=8;this.color=color(50,100,100);}else{this.vy=6;this.vx=0;this.size=20;this.color=color(30,80,60);}}update(){this.vy+=this.gravity;this.x+=this.vx;this.y+=this.vy;}display(){fill(this.color);noStroke();if(this.type==='seed')ellipse(this.x,this.y,this.size,this.size);else triangle(this.x,this.y-this.size/2,this.x-this.size/2,this.y+this.size/2,this.x+this.size/2,this.y+this.size/2);}}
class Collectible{constructor(x,y,type){this.x=x;this.y=y;this.type=type;this.angle=random(TWO_PI);if(this.type==='seed'){this.size=10;this.color=color(60,100,100);}else{this.size=12;this.color=color(120,80,80);}}update(){this.angle+=0.1;}display(){let bob=sin(this.angle)*2;push();translate(this.x,this.y+bob);noStroke();fill(this.color);ellipse(0,0,this.size,this.size);fill(0,0,100,0.5);ellipse(0,0,this.size+5,this.size+5);pop();}}
class Flower{constructor(x,y){this.x=x;this.y=y;this.hue=random(360);this.size=random(20,35);this.stemHeight=this.size*1.5;this.spawnRate=floor(random(300,600));}update(){if(frameCount%this.spawnRate===0)collectibles.push(new Collectible(this.x,this.y-this.stemHeight,'seed'));}display(){noStroke();fill(120,60,70);rect(this.x-2,this.y-this.stemHeight,4,this.stemHeight);fill(this.hue,80,100);for(let i=0;i<6;i++)ellipse(this.x+cos(i*PI/3)*this.size/2,this.y-this.stemHeight+sin(i*PI/3)*this.size/2,this.size*0.8,this.size*0.8);fill((this.hue+180)%360,90,90);ellipse(this.x,this.y-this.stemHeight,this.size/2,this.size/2);}}
class Tree{constructor(x,y){this.x=x;this.y=y;this.trunkHeight=random(120,180);this.trunkWidth=this.trunkHeight/10;this.canopyHeight=this.trunkHeight*1.0;this.canopyWidth=this.canopyHeight*0.8;this.canopyYOffset=this.trunkHeight*0.9;this.spawnRate=floor(random(600,900));}update(){if(frameCount%this.spawnRate===0){collectibles.push(new Collectible(this.x+random(-this.canopyWidth/3,this.canopyWidth/3),this.y-this.canopyYOffset,'sapling'));}}display(){noStroke();fill(30,80,40);rect(this.x-this.trunkWidth/2,this.y-this.trunkHeight,this.trunkWidth,this.trunkHeight);fill(130,70,60);ellipse(this.x,this.y-this.canopyYOffset,this.canopyWidth,this.canopyHeight);}}
class FernFlower{constructor(x,y){this.x=x;this.y=y;this.size=25;this.angle=0;}display(){this.angle+=0.08;let glow=5+sin(this.angle)*5;push();translate(this.x,this.y);noStroke();fill(90,100,100,0.4);ellipse(0,0,this.size+glow,this.size+glow);fill(90,80,100);ellipse(0,0,this.size,this.size);fill(0,0,100);ellipse(0,0,this.size*0.3,this.size*0.3);pop();}}
</script>
</body>
</html>
Код згенеровано у Google AI Studio
Твій віртуальний помічник ще не зʼїхав з глузду від співпраці з тобою ?
Не вперше помічаю у твоїх дописах зелені квіти! Таких не існує в природі. Це новий тренд? Чи спосіб виділитись? На мій погляд доцільніше використовувати більш підходящв кольори для зображення пелюсток. Можна : рожеві, бузкові, фіолетові, жовті, сині, помаранчеві, нащо робити зелені ??????????