Tworzenie gry w JavaScript
W pierwszej części pisałem o podstawach three.js, które teraz wykorzystamy do stworzenia gry w JavaScript. Dziś przejdziemy do sedna. Postaram się wytłumaczyć wszystko szybko, zgrabnie i przyjemnie. Tworzenie gier powinno być właśnie takie. Tak jak poprzednio, będziemy korzystać z siatki modelu 3D Karima Maloula.
Poświęć chwilę i zagraj w tę grę. Zanotuj wszystko, co dzieje się na scenie, zanim przejdziesz do następnej sekcji.
Aby pobrać wszystkie pliki z tego projektu, odwiedź moje repozytorium Githubie.
Planowanie
Jedną z najważniejszych rzeczy, które powinieneś zrobić przed rozpoczęciem kodowania w takim projekcie, jest zawsze ułożenie planu. W grze dzieje się tak wiele rzeczy i założę się, że niektórych nie zauważyłeś - uszy Królika się poruszają, ogon Potwora również, Potwór ma zęby i ładny cień porusza się pod naszymi postaciami. Jeśli zauważyłeś którąś z tych rzeczy, powinieneś na część etatu pracować jako Sherlock Holmes.
Musimy uporządkować nasze przemyślenia na temat gry. Mamy głównych bohaterów: Królik/Bohatera i Potwora. Musimy mieć również inne ważne cechy w grze - żywopłoty i marchewki. Wyjaśnimy to sobie w prosty sposób.
Świat
To jest środowisko, w którym działamy. Składają się na niego drzewa, ziemia i mglista pogoda.
Drzewa
Dodają trochę realności do naszej gry.
Potwór
Ściga i łapie królika. Jeśli uda mu się go dogonić, jest to koniec gry.
Królik
Jest przerażony i musi biegać, biegać i uciekać. Potrzebuje jedzenia/marchewki, aby uzyskać energię oraz musi unikać jeży. To sprawia, że gra jest przyjemna i trudna, a gracz jest ciągle skupiony.
Marchewka
Zapewnia siłę działa naszemu Królikowi.
Jeże
Utrzymują czujność naszego gracza. Po zderzeniu z Królikiem zmniejszają prędkość jego biegania.
Przyznawanie punktów
Obliczymy odległość, którą pokonuje nasz królik i która będzie punktami dla naszych graczy.
To wszystko. Jest to prostsze w kodowaniu niż w teorii. Wykonamy powyższe funkcje jedną po drugiej, a następnie scalimy w jedność i stworzymy grę.
Najpierw stwórzmy tło
Zanim zrobimy cokolwiek, musimy mieć miejsce, w którym wyświetlimy grę. Użyjemy HTML i CSS, aby zrobić pustą przestrzeń z zakodowaną początkową odległością na poziomie zero na górze strony i instrukcjami na dole.
Utwórz trzy pliki: game.html, main.js i main.css w tym samym katalogu i otwórz plik main.html w przeglądarce. Poniżej znajduje się mój katalog oraz struktura plików.
Zapisz następujący kod HTML w game.html.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>The Monster and the Rabbit</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" media="screen" href="main.css" />
</head>
<body>
<div id="world" />
<div id="gameoverInstructions">
Game Over
</div>
<div id="dist">
<div class="label">distance</div>
<div id="distValue">000</div>
</div>
<div id="instructions">Click to jump<span class="lightInstructions"> — Grab the carrots / avoid the hedgehogs</span></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r80/three.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.19.0/TweenMax.min.js"></script>
<script src="main.js"></script>
</body>
</html>
Zauważ, że dodaliśmy dwie zewnętrzne biblioteki JavaScript na dole naszego pliku - Three.js i TweenMax. Możesz pobrać pliki i zapisać je lokalnie do użytku w trybie offline.
Dodamy styl do tła, licznika odległości i instrukcji. Zapisz poniższy kod w pliku main.css.
@import url('https://fonts.googleapis.com/css?family=Voltaire');
html, body {
height: 100%;
padding: 0;
margin: 0;
}
*, *:before, *:after {
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
blockquote, dd, div, dl, dt, form, h1, h2, h3, h4, h5, h6, li, ol, p, pre, td, th, ul {
margin: 0;
padding: 0;
}
#world{
position: absolute;
width:100%;
height: 100%;
background-color: #dbe6e6;
overflow: hidden;
}
#gameoverInstructions{
position:absolute;
font-family:'Voltaire', sans-serif;
font-weight:bold;
text-transform: uppercase;
font-size:120px;
text-align:center;
color:#ffc5a2;
opacity:0;
left:50%;
top:50%;
width:100%;
transform : translate(-50%,-100%);
user-select: none;
transition: all 500ms ease-in-out;
}
#gameoverInstructions.show {
opacity:1;
transform : translate(-50%,-50%);
transition: all 500ms ease-in-out;
}
#dist{
position:absolute;
left:50%;
top:50px;
transform:translate(-50%,0%);
user-select: none;
}
.label{
position:relative;
font-family:'Voltaire', sans-serif;
text-transform:uppercase;
color:#ffa873;
font-size:12px;
letter-spacing:2px;
text-align:center;
margin-bottom:5px;
}
#distValue{
position:relative;
text-transform:uppercase;
color:#dc5f45;
font-size:40px;
font-family:'Voltaire';
text-align:center;
}
#instructions{
position:absolute;
width:100%;
bottom:0;
margin: auto;
margin-bottom:50px;
font-family:'Voltaire', sans-serif;
color:#dc5f45;
font-size:16px;
letter-spacing:1px;
text-transform: uppercase;
text-align : center;
user-select: none;
}
.lightInstructions {
color:#5f9042;
}
Użyjemy “third party google font”, żeby gra wyglądała na jeszcze bardziej dopracowaną. Gdy to zrobisz, będziesz miał na ekranie coś takiego:
To wszystko. Cały poniższy kod ląduje w pustym pliku main.js.
Tworzenie sceny
Jak wyjaśniłem w poprzednim artykule, musimy być w stanie wyświetlić naszą grę. Aby to zrobić, potrzebujemy sceny, kamery i renderera. Zanim przejdziemy do szczegółów, skopiuj poniższy kod do pliku main.js i kontynuuj.
var scene,
camera, fieldOfView, aspectRatio, nearPlane, farPlane,
gobalLight, shadowLight, backLight,
renderer,
container;
var delta = 0;
var floorRadius = 200;
var cameraPosGame = 160;
var cameraPosGameOver = 260;
var monsterAcceleration = 0.004;
var malusClearColor = 0xb44b39;
var malusClearAlpha = 0;
var audio = new Audio('https://s3-us-west-2.amazonaws.com/s.cdpn.io/264161/Antonio-Vivaldi-Summer_01.mp3');
var fieldGameOver, fieldDistance;
//INIT THREE JS, SCREEN AND MOUSE EVENTS
function initScreenAnd3D() {
HEIGHT = window.innerHeight;
WIDTH = window.innerWidth;
windowHalfX = WIDTH / 2;
windowHalfY = HEIGHT / 2;
scene = new THREE.Scene();
scene.fog = new THREE.Fog(0xd6eae6, 160, 350);
aspectRatio = WIDTH / HEIGHT;
fieldOfView = 50;
nearPlane = 1;
farPlane = 2000;
camera = new THREE.PerspectiveCamera(
fieldOfView,
aspectRatio,
nearPlane,
farPlane
);
camera.position.x = 0;
camera.position.z = cameraPosGame;
camera.position.y = 30;
camera.lookAt(new THREE.Vector3(0, 30, 0));
renderer = new THREE.WebGLRenderer({
alpha: true,
antialias: true
});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setClearColor(malusClearColor, malusClearAlpha);
renderer.setSize(WIDTH, HEIGHT);
renderer.shadowMap.enabled = true;
container = document.getElementById('world');
container.appendChild(renderer.domElement);
}
function initUI() {
fieldDistance = document.getElementById("distValue");
fieldGameOver = document.getElementById("gameoverInstructions");
}
window.addEventListener('load', init, false);
function render() {
renderer.render(scene, camera);
}
function init(event) {
initScreenAnd3D();
initUI();
render();
}
Co tu zaszło, wyjaśniam dokładnie w pierwszej części. Zaczynamy od stworzenia sceny. Scena jest następnie przekazywana do naszej kamery, a renderer wyświetla scenę. Podobnie jak w filmie.
scene.fog = new THREE.Fog(0xd6eae6, 160, 350);
Nadajemy mgle (Fog) trzy argumenty: pierwszy jest kolorem, drugi to najbliższy dystans, a na końcu daleki zasięg efektu mgły. Zazwyczaj będziemy ustawiać kolor mgły taki sam, jak kolor tła.
0xd6eae6
to po prostu CSS-owy/photoshopowy kod koloru, ale z 0x zamiast #.
Wreszcie dodajemy element renderera do naszego dokumentu HTML.
alpha
- canvas
zawiera bufor alpha (przezroczysty) lub nie. Narysowałem dwa kwadraty z 50% alpha - jeden zielony i jeden czerwony. Możesz zauważyć, że kolejność jest ważna, a ostateczny kolor daje ważną wskazówkę dla oczu dla prawidłowej percepcji głębi. Rozwiązujemy tym kwestię przejrzystości.
antialias
- do wygładzania obrazu lub dźwiękowej szorstkości w grze. Potrzebujemy płynnych przejść. Domyślna wartość to false. Musimy ją zmienić na true..setPixelRatio
ustawia stosunek pikseli. Zwykle jest to używane w przypadku urządzenia HiDPI, aby zapobiec rozmazania obrazu na wyjściowym canvasie..setClearColor
ustawia przezroczysty kolor i krycie. Ustawiamy białawe i szarawe tło z przezroczystością, aby było mniej widoczne.renderer.shadowMap.enabled
pozwala wyświetlać cienie postaci.
Światło
Światło jest bardzo ważne w grach. W poprzednim artykule stworzyliśmy scenę bez oświetlenia. Tak wygląda ona bez niego.
W tej grze wykorzystamy dwa rodzaje oświetlenia.
AmbientLight
To światło globalnie oświetla wszystkie obiekty sceny w równym stopniu. Nie można używać go do rzucania cieni, ponieważ nie ma kierunku.
DirectionalLight
Światło emitowane w określonym kierunku. Będzie zachowywać się tak, jakby było nieskończenie daleko, a emitowane z niego promienie będą równoległe. Powszechnym zastosowaniem tego rozwiązania jest symulowanie światła dziennego - Słońce jest wystarczająco daleko, aby jego położenie mogło być uważane za nieskończone, a wszystkie promienie światła pochodzące od niego są równoległe. Takie światło może rzucać cienie.
function createLights() {
globalLight = new THREE.AmbientLight(0xffffff, .9);
shadowLight = new THREE.DirectionalLight(0xffffff, 1);
shadowLight.position.set(-30, 40, 20);
shadowLight.castShadow = true;
shadowLight.shadow.camera.left = -400;
shadowLight.shadow.camera.right = 400;
shadowLight.shadow.camera.top = 400;
shadowLight.shadow.camera.bottom = -400;
shadowLight.shadow.camera.near = 1;
shadowLight.shadow.camera.far = 2000;
shadowLight.shadow.mapSize.width = shadowLight.shadow.mapSize.height = 2048;
scene.add(globalLight);
scene.add(shadowLight);
}
dodaj createLights()
po initScreenAnd3D()
w funkcji init
w pliku main.js. Nie zobaczysz żadnego efektu, ale gdy stworzymy inne funkcje, zobaczysz różnicę.
Kiedy patrzysz na promienie słoneczne, jak na zdjęciu poniżej, zachowują się jak źródło światła bardzo daleko od nas.
Aby włączyć rzucanie cienia na światło, po prostu użyj tej linii.
light.castShadow = true;
Dodatkowo podczas rzucania cienia przy pomocy THREE.DirectionalLight, musimy ustawić położenie źródła światła. Mamy różne odcienie i natężenie światła w zależności od jego położenia.
Materiały
Opisują wygląd przedmiotów - szkła, drewna, metalu, ceramiki i wielu innych. W naszej grze wykorzystujemy materiały do środowiska naszych postaci. Ale w przeciwieństwie do rzeczywistego świata, będziemy używać różnych kolorów do reprezentowania różnych materiałów. Zieleń dla trawy i podłogi, czerń dla potwora, drzew i tak dalej.
Tworzenie materiałów w Three.js jest dość proste.
// Materials
var blackMat = new THREE.MeshPhongMaterial({
color: 0x100707,
shading: THREE.FlatShading,
});
var brownMat = new THREE.MeshPhongMaterial({
color: 0xb44b39,
shininess: 0,
shading: THREE.FlatShading,
});
var greenMat = new THREE.MeshPhongMaterial({
color: 0x7abf8e,
shininess: 0,
shading: THREE.FlatShading,
});
var pinkMat = new THREE.MeshPhongMaterial({
color: 0xdc5f45,//0xb43b29,//0xff5b49,
shininess: 0,
shading: THREE.FlatShading,
});
var lightBrownMat = new THREE.MeshPhongMaterial({
color: 0xe07a57,
shading: THREE.FlatShading,
});
var whiteMat = new THREE.MeshPhongMaterial({
color: 0xa49789,
shading: THREE.FlatShading,
});
var skinMat = new THREE.MeshPhongMaterial({
color: 0xff9ea5,
shading: THREE.FlatShading
});
MeshPhongMaterial
- tworzy błyszczące powierzchnie z połyskującymi refleksami. Może to symulować powierzchnie takie, jak np. drewno lakierowane.color
- kolor materiału, domyślnie ustawiony na jest biały (0xffffff).shininess
- ustawia jak bardzo błyszczący ma być połysk. Wyższa wartość daje ostrzejszy efekt. Domyślna wartość to 30.shading
- Istnieją dwa rodzaje cieniowania - płaskie (Flat) i gładkie (Smooth). Płaskiego używamy, gdy chcemy nadać szorstką fakturę obiektom takim jak teren, kora drzew, kamienie itp.. Gładkie cieniowanie dotyczy materiałów takich jak szkło i reszta.
Podłoga
function createFloor() {
floorShadow = new THREE.Mesh(new THREE.SphereGeometry(floorRadius, 50, 50), new THREE.MeshPhongMaterial({
color: 0x7abf8e,
specular: 0x000000,
shininess: 1,
transparent: true,
opacity: .5
}));
//floorShadow.rotation.x = -Math.PI / 2;
floorShadow.receiveShadow = true;
floorGrass = new THREE.Mesh(new THREE.SphereGeometry(floorRadius - .5, 50, 50), new THREE.MeshBasicMaterial({
color: 0x7abf8e
}));
//floor.rotation.x = -Math.PI / 2;
floorGrass.receiveShadow = false;
floor = new THREE.Group();
floor.position.y = -floorRadius;
floor.add(floorShadow);
floor.add(floorGrass);
scene.add(floor);
}
Three.Mesh
to klasa reprezentująca siatkę 3D. Siatka wieloboków to zbiór wierzchołków, krawędzi i powierzchni, które definiują obiekty oparte na kształcie. Poniżej znajduje się przykład siatki wielokątów.
Na powyższej zauważysz dwie rzeczy - kształt przedmiotu i materiał. Aby utworzyć siatkę, nadajemy kształt, który tworzymy, oraz materiał, który do niego zastosujemy.
Aby stworzyć prostą podłogę taką, jaką mamy w naszej grze, tworzymy kulisty kształt THREE.SphereGeometry
, który jest pokryty zielonym materiałem THREE.MeshPhongMaterial
.
Na naszej podłodze mają być cienie. Aby to osiągnąć, najpierw dodajemy floorShadow
, który nadaje przejrzystość, a receiveShadow
zmieniamy na true.
Jednak to wciąż nie jest to, czego chcemy. Wykonajmy duplikat floorShadow
i nazwijmy go floorGrass
, ale bez parametrów specular, shininess, transparent i opacity. Ustawimy również wartość receiveShadow
na false. To ustawienie daje nam zieloną podłogę bez cieni.
Po wykonaniu tej czynności w końcu łączymy siatki, tworząc grupę floor = new THREE.Group()
oraz dodając floorGrass
i floorShadow
do tej grupy. Efekt będzie taki:
Za każdym razem, gdy tworzę nowy Mesh, po prostu dodaję go do instancji grupy, a nie Sceny, dzięki temu pochodzenie każdej siatki jest względne w odniesieniu do instancji grupy, a nie sceny. Gdy to zrobisz i zmienisz pozycję lub rotację grupy, zmieni ona pozycję i obrót całej grupy. Jeśli znasz Adobe lub Sketch, łatwiej to zrozumiesz.
Podsumowanie
Three.js to rodzaj biblioteki, w której naprawdę trzeba poświęcić co najmniej solidny miesiąc lub więcej, aby zacząć należycie z niej korzystać. Ja nadal się uczę, ale uważam, że są pewne dodatkowe aspekty tej biblioteki, które są bardzo ważne, podczas gdy inne są opcjonalne, w zależności od rodzaju wykonywanych projektów.
Mam nadzieję, że dobrze się bawiłeś i będziesz zainteresowany kolejną częścią serii tworzenia gry w JavaScripcie.