init repo

This commit is contained in:
Renge 2022-05-23 06:00:37 -04:00
commit 86548b6057
591 changed files with 80113 additions and 0 deletions

17
hw1/README.md Normal file
View File

@ -0,0 +1,17 @@
# CSE380_HW1
---
## GAME concentration_multi(temp)
### prepare state
all lights are off
single click anyone to set the number of players
double click to start the game
### play state
at each player's tern, single click to select a single blink
nothing happened -- earn 1 pt
turn white for a second -- earn no pt
### end state
all lights turn white to validate
then all blink will turn to the color of the player who chose that blink
double click to reset to prepare state
## GAME 2
## GAME 3

224
hw1/concentration_multi.cpp Normal file
View File

@ -0,0 +1,224 @@
enum signalStates { PREPARE, INERT, GO, RESOLVE, CHECK, END };
byte signalState = PREPARE;
enum player {PLAYER1, PLAYER2, PLAYER3, PLAYER4, PLAYER5, PLAYER6};//these modes will simply be different colors
byte player = PLAYER1;//the default mode when the game begins
byte player_num = 0;
byte chosen_player = 6;
Timer warn_timer;
Timer check_timer;
void loop() {
switch (signalState) {
case PREPARE:
prepareLoop();
break;
case INERT:
case GO:
case RESOLVE:
playLoop();
break;
case CHECK:
checkLoop();
break;
case END:
endLoop();
break;
}
}
void prepareLoop() {
FOREACH_FACE(f) {
if (getSignalState(getLastValueReceivedOnFace(f)) == INERT) {
signalState = INERT;
player = getPlayer(getLastValueReceivedOnFace(f));
player_num = player + 1;
byte sendData = (signalState << 3) + (player);
setValueSentOnAllFaces(sendData);
}
else {
if (player_num == 0) {
setColor(OFF);
}
else {
for ( byte i = 0; i < player_num; i ++) {
setColorOnFace(getColor(i+1), i);
}
}
if (buttonSingleClicked()) {
player_num = (player_num + 1) % 7;
}
if (buttonDoubleClicked() && player_num != 0) {
player = player_num - 1;
signalState = INERT;
byte sendData = (signalState << 3) + (player);
setValueSentOnAllFaces(sendData);
}
else {
setValueSentOnAllFaces(0);
}
}
}
}
void playLoop() {
switch (signalState) {
case INERT:
inertLoop();
break;
case GO:
goLoop();
break;
case RESOLVE:
resolveLoop();
break;
}
displaySignalState();
byte sendData = (signalState << 3) + (player);
setValueSentOnAllFaces(sendData);
}
void inertLoop() {
//set myself to GO
if (buttonSingleClicked()) {
if (chosen_player == 6) {
chosen_player = player;
}
else {
warn_timer.set(1000);
}
signalState = GO;
player = (player + 1) % (player_num);//adds one to game mode, but 3+1 becomes 0
}
//listen for neighbors in GO
FOREACH_FACE(f) {
if (!isValueReceivedOnFaceExpired(f)) { //a neighbor!
if (getSignalState(getLastValueReceivedOnFace(f)) == GO) { //a neighbor saying GO!
signalState = GO;
player = getPlayer(getLastValueReceivedOnFace(f));
}
}
}
}
void goLoop() {
signalState = RESOLVE; //I default to this at the start of the loop. Only if I see a problem does this not happen
//look for neighbors who have not heard the GO news
FOREACH_FACE(f) {
if (!isValueReceivedOnFaceExpired(f)) { //a neighbor!
if (getSignalState(getLastValueReceivedOnFace(f)) == INERT) {//This neighbor doesn't know it's GO time. Stay in GO
signalState = GO;
}
}
}
}
void resolveLoop() {
if (chosen_player == 6) {
signalState = INERT; //I default to this at the start of the loop. Only if I see a problem does this not happen
}
else {
check_timer.set(2000);
signalState = CHECK;
}
//look for neighbors who have not moved to RESOLVE
FOREACH_FACE(f) {
if (!isValueReceivedOnFaceExpired(f)) { //a neighbor!
if (getSignalState(getLastValueReceivedOnFace(f)) == GO) {//This neighbor isn't in RESOLVE. Stay in RESOLVE
signalState = RESOLVE;
}
}
}
}
void checkLoop() {
if (check_timer.isExpired()) {
setColor(OFF);
signalState = END;
}
else {
setColor(WHITE);
FOREACH_FACE(f) {
if (!isValueReceivedOnFaceExpired(f)) { //a neighbor!
if (getSignalState(getLastValueReceivedOnFace(f)) == INERT) {//This neighbor isn't in RESOLVE. Stay in RESOLVE
signalState = INERT;
}
}
}
setValueSentOnAllFaces(6 << 3);
}
}
void endLoop() {
setColor(getColor(getRealPlayer(chosen_player)))
FOREACH_FACE(f) {
if (!isValueReceivedOnFaceExpired(f)) { //a neighbor!
if (getSignalState(getLastValueReceivedOnFace(f)) == PREPARE) {//This neighbor isn't in RESOLVE. Stay in RESOLVE
signalState = PREPARE;
player = PLAYER1;
player_num = 0;
chosen_player = 6;
setColor(OFF);
}
}
}
if (buttonDoubleClicked()) {
signalState = PREPARE;
player = PLAYER1;
player_num = 0;
chosen_player = 6;
setValueSentOnAllFaces(0);
}
}
void displaySignalState() {
if (warn_timer.isExpired()) {
switch (signalState) {
case INERT:
setColor(getColor(getRealPlayer(player)));
break;
case GO:
case RESOLVE:
setColor(WHITE);
break;
}
}
else {
setColor(WHITE);
}
}
Color getColor(data) {
switch (data) {
case 0:
return (makeColorRGB(0,0,0));
break;
case 1:
return (makeColorRGB(255,0,0));
break;
case 2:
return (makeColorRGB(0,255,0));
break;
case 3:
return (makeColorRGB(0,0,255));
break;
case 4:
return (makeColorRGB(255,0,255));
break;
case 5:
return (makeColorRGB(255,255,0));
break;
case 6:
return (makeColorRGB(0,255,255));
break;
}
}
byte getRealPlayer(byte data) {
return (data + 1) % player_num + 1;
}
byte getPlayer(byte data) {
return (data & 7);//returns bits E and F
}
byte getSignalState(byte data) {
return ((data >> 3) & 7);//returns bits C and D
}

18
hw3/.gitignore vendored Normal file
View File

@ -0,0 +1,18 @@
# Exclude node modules
node_modules
# Exclude the compiled project
dist/*
# Include the demo_assets folder
!dist/demo_assets/
# Include the built-in asset folder
!dist/builtin/
# Include the hw1 assets
!dist/hw3_assets/
### IF YOU ARE MAKING A PROJECT, YOU MAY WANT TO UNCOMMENT THIS LINE ###
# !dist/assets/

77
hw3/dist/builtin/shaders/label.fshader vendored Normal file
View File

@ -0,0 +1,77 @@
precision mediump float;
uniform vec4 u_BackgroundColor;
uniform vec4 u_BorderColor;
uniform float u_BorderWidth;
uniform float u_BorderRadius;
uniform vec2 u_MaxSize;
varying vec4 v_Position;
void main(){
vec2 adj_MaxSize = u_MaxSize - u_BorderWidth;
vec2 rad_MaxSize = u_MaxSize - u_BorderRadius;
vec2 rad2_MaxSize = u_MaxSize - 2.0*u_BorderRadius;
bool inX = (v_Position.x < adj_MaxSize.x) && (v_Position.x > -adj_MaxSize.x);
bool inY = (v_Position.y < adj_MaxSize.y) && (v_Position.y > -adj_MaxSize.y);
bool inRadiusRangeX = (v_Position.x < rad_MaxSize.x) && (v_Position.x > -rad_MaxSize.x);
bool inRadiusRangeY = (v_Position.y < rad_MaxSize.y) && (v_Position.y > -rad_MaxSize.y);
bool inRadius2RangeX = (v_Position.x < rad2_MaxSize.x) && (v_Position.x > -rad2_MaxSize.x);
bool inRadius2RangeY = (v_Position.y < rad2_MaxSize.y) && (v_Position.y > -rad2_MaxSize.y);
if(inX && inY){
// Inside bounds, draw background color
gl_FragColor = u_BackgroundColor;
} else {
// In boundary, draw border color
gl_FragColor = u_BorderColor;
}
// This isn't working well right now
/*
if(inRadius2RangeX || inRadius2RangeY){
// Draw normally
if(inX && inY){
// Inside bounds, draw background color
gl_FragColor = u_BackgroundColor;
} else {
// In boundary, draw border color
gl_FragColor = u_BorderColor;
}
} else if(inRadiusRangeX || inRadiusRangeY){
// Draw a rounded boundary for the inner part
float x = v_Position.x - sign(v_Position.x)*rad2_MaxSize.x;
float y = v_Position.y - sign(v_Position.y)*rad2_MaxSize.y;
float radSq = x*x + y*y;
float bRadSq = u_BorderRadius*u_BorderRadius;
if(radSq > bRadSq){
// Outside of radius - draw as transparent
gl_FragColor = u_BorderColor;
} else {
gl_FragColor = u_BackgroundColor;
}
} else {
// Both coordinates are in the circular section
float x = v_Position.x - sign(v_Position.x)*rad_MaxSize.x;
float y = v_Position.y - sign(v_Position.y)*rad_MaxSize.y;
float radSq = x*x + y*y;
float bRadSq = u_BorderRadius*u_BorderRadius;
if(radSq > bRadSq){
// Outside of radius - draw as transparent
gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0);
} else if(sqrt(bRadSq) - sqrt(radSq) < u_BorderWidth) {
// In border
gl_FragColor = u_BorderColor;
} else {
gl_FragColor = u_BackgroundColor;
}
}
*/
}

12
hw3/dist/builtin/shaders/label.vshader vendored Normal file
View File

@ -0,0 +1,12 @@
attribute vec4 a_Position;
uniform mat4 u_Transform;
varying vec4 v_Position;
void main(){
gl_Position = u_Transform * a_Position;
// Pass position to the fragment shader
v_Position = a_Position;
}

View File

@ -0,0 +1,7 @@
precision mediump float;
uniform vec4 u_Color;
void main(){
gl_FragColor = u_Color;
}

View File

@ -0,0 +1,8 @@
attribute vec4 a_Position;
uniform float u_PointSize;
void main(){
gl_Position = a_Position;
gl_PointSize = u_PointSize;
}

7
hw3/dist/builtin/shaders/rect.fshader vendored Normal file
View File

@ -0,0 +1,7 @@
precision mediump float;
uniform vec4 u_Color;
void main(){
gl_FragColor = u_Color;
}

7
hw3/dist/builtin/shaders/rect.vshader vendored Normal file
View File

@ -0,0 +1,7 @@
attribute vec4 a_Position;
uniform mat4 u_Transform;
void main(){
gl_Position = u_Transform * a_Position;
}

View File

@ -0,0 +1,9 @@
precision mediump float;
uniform sampler2D u_Sampler;
varying vec2 v_TexCoord;
void main(){
gl_FragColor = texture2D(u_Sampler, v_TexCoord);
}

13
hw3/dist/builtin/shaders/sprite.vshader vendored Normal file
View File

@ -0,0 +1,13 @@
attribute vec4 a_Position;
attribute vec2 a_TexCoord;
uniform mat4 u_Transform;
uniform vec2 u_texShift;
uniform vec2 u_texScale;
varying vec2 v_TexCoord;
void main(){
gl_Position = u_Transform * a_Position;
v_TexCoord = a_TexCoord*u_texScale + u_texShift;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 B

BIN
hw3/dist/demo_assets/sounds/jump.wav vendored Normal file

Binary file not shown.

BIN
hw3/dist/demo_assets/sounds/title.mp3 vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,27 @@
{
"name": "PlatformerPlayer",
"spriteSheetImage": "player.png",
"spriteWidth": 16,
"spriteHeight": 16,
"columns": 5,
"rows": 1,
"durationType": "time",
"animations": [
{
"name": "IDLE",
"frames": [ {"index": 0, "duration": 1} ]
},
{
"name": "WALK",
"frames": [ {"index": 0, "duration": 16}, {"index": 1, "duration": 16}, {"index": 2, "duration": 16}, {"index": 3, "duration": 16} ]
},
{
"name": "JUMP",
"frames":[ {"index": 4, "duration": 32}]
},
{
"name": "FALL",
"frames":[ {"index": 4, "duration": 32}]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 B

View File

@ -0,0 +1,453 @@
{ "compressionlevel":-1,
"editorsettings":
{
"export":
{
"format":"json",
"target":"platformer.json"
}
},
"height":20,
"infinite":false,
"layers":[
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 44, 44, 44, 44, 44, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 0, 44, 44, 44, 44, 44, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 44, 45, 0, 43, 0, 44, 44, 44, 44, 44, 44, 0, 44, 44, 44, 44, 44, 44, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 35, 36, 0, 36, 36, 36, 36, 36, 36, 36, 36, 36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 44, 45, 0, 43, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 54, 55, 44, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 44, 45, 0, 43, 0, 0, 0, 0, 0, 0, 0, 0, 0, 44, 44, 62, 63, 44, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":20,
"id":2,
"name":"Background",
"opacity":1,
"properties":[
{
"name":"Collidable",
"type":"bool",
"value":false
},
{
"name":"Depth",
"type":"int",
"value":0
}],
"type":"tilelayer",
"visible":true,
"width":64,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 20, 20, 20, 8, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 11, 12, 10, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 10, 12, 12, 12, 12, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 12, 12, 12, 12, 12, 12, 12, 12, 9, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 16, 12, 12, 12, 12, 10, 12, 12, 12, 12, 12, 12, 9, 12, 12, 12, 12, 12, 12, 12, 12, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 12, 12, 12, 12, 7, 20, 20, 20, 20, 20, 20, 8, 12, 12, 12, 12, 10, 12, 12, 12, 12, 12, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, 28, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 9, 16, 12, 12, 13, 0, 0, 0, 0, 0, 0, 11, 12, 12, 12, 12, 12, 12, 9, 12, 12, 12, 13, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 19, 20, 20, 20, 20, 13, 0, 0, 0, 0, 0, 0, 11, 20, 20, 20, 20, 20, 20, 20, 8, 12, 9, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 0, 0, 0, 0, 22, 0, 0, 0, 0, 0, 0, 0, 11, 12, 12, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 4, 4, 4, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 12, 12, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 2, 2, 2, 2, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 10, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 4, 4, 4, 4, 4, 4, 4, 5, 0, 0, 0, 0, 0, 0, 11, 12, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 11, 2, 2, 2, 2, 13, 1, 1, 1, 1, 1, 1, 1, 1, 1, 11, 12, 13, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 11, 10, 12, 12, 12, 12, 12, 12, 10, 15, 4, 4, 4, 4, 4, 4, 4, 4],
"height":20,
"id":1,
"name":"Main",
"opacity":1,
"properties":[
{
"name":"Collidable",
"type":"bool",
"value":true
},
{
"name":"Depth",
"type":"int",
"value":1
}],
"type":"tilelayer",
"visible":true,
"width":64,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":4,
"name":"Coins",
"objects":[
{
"gid":25,
"height":16,
"id":2,
"name":"",
"properties":[
{
"name":"Group",
"type":"string",
"value":"Coins"
},
{
"name":"HasPhysics",
"type":"bool",
"value":true
},
{
"name":"IsCollidable",
"type":"bool",
"value":false
},
{
"name":"IsTrigger",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":16,
"x":256,
"y":272
},
{
"gid":25,
"height":16,
"id":3,
"name":"",
"properties":[
{
"name":"Group",
"type":"string",
"value":"Coins"
},
{
"name":"HasPhysics",
"type":"bool",
"value":true
},
{
"name":"IsCollidable",
"type":"bool",
"value":false
},
{
"name":"IsTrigger",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":16,
"x":272,
"y":272
},
{
"gid":25,
"height":16,
"id":4,
"name":"",
"properties":[
{
"name":"Group",
"type":"string",
"value":"Coins"
},
{
"name":"HasPhysics",
"type":"bool",
"value":true
},
{
"name":"IsCollidable",
"type":"bool",
"value":false
},
{
"name":"IsTrigger",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":16,
"x":368,
"y":288
},
{
"gid":25,
"height":16,
"id":5,
"name":"",
"properties":[
{
"name":"Group",
"type":"string",
"value":"Coins"
},
{
"name":"HasPhysics",
"type":"bool",
"value":true
},
{
"name":"IsCollidable",
"type":"bool",
"value":false
},
{
"name":"IsTrigger",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":16,
"x":384,
"y":288
},
{
"gid":25,
"height":16,
"id":6,
"name":"",
"properties":[
{
"name":"Group",
"type":"string",
"value":"Coins"
},
{
"name":"HasPhysics",
"type":"bool",
"value":true
},
{
"name":"IsCollidable",
"type":"bool",
"value":false
},
{
"name":"IsTrigger",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":16,
"x":400,
"y":288
},
{
"gid":25,
"height":16,
"id":7,
"name":"",
"properties":[
{
"name":"Group",
"type":"string",
"value":"Coins"
},
{
"name":"HasPhysics",
"type":"bool",
"value":true
},
{
"name":"IsCollidable",
"type":"bool",
"value":false
},
{
"name":"IsTrigger",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":16,
"x":688,
"y":272
},
{
"gid":25,
"height":16,
"id":8,
"name":"",
"properties":[
{
"name":"Group",
"type":"string",
"value":"Coins"
},
{
"name":"HasPhysics",
"type":"bool",
"value":true
},
{
"name":"IsCollidable",
"type":"bool",
"value":false
},
{
"name":"IsTrigger",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":16,
"x":688,
"y":288
},
{
"gid":25,
"height":16,
"id":9,
"name":"",
"properties":[
{
"name":"Group",
"type":"string",
"value":"Coins"
},
{
"name":"HasPhysics",
"type":"bool",
"value":true
},
{
"name":"IsCollidable",
"type":"bool",
"value":false
},
{
"name":"IsTrigger",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":16,
"x":688,
"y":304
},
{
"gid":25,
"height":16,
"id":10,
"name":"",
"properties":[
{
"name":"Group",
"type":"string",
"value":"Coins"
},
{
"name":"HasPhysics",
"type":"bool",
"value":true
},
{
"name":"IsCollidable",
"type":"bool",
"value":false
},
{
"name":"IsTrigger",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":16,
"x":784,
"y":256
},
{
"gid":25,
"height":16,
"id":11,
"name":"",
"properties":[
{
"name":"Group",
"type":"string",
"value":"Coins"
},
{
"name":"HasPhysics",
"type":"bool",
"value":true
},
{
"name":"IsCollidable",
"type":"bool",
"value":false
},
{
"name":"IsTrigger",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":16,
"x":832,
"y":256
}],
"opacity":1,
"properties":[
{
"name":"Depth",
"type":"int",
"value":1
}],
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":20,
"id":3,
"name":"Foreground",
"opacity":1,
"properties":[
{
"name":"Collidable",
"type":"bool",
"value":false
},
{
"name":"Depth",
"type":"int",
"value":2
}],
"type":"tilelayer",
"visible":true,
"width":64,
"x":0,
"y":0
}],
"nextlayerid":5,
"nextobjectid":14,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.3.4",
"tileheight":16,
"tilesets":[
{
"columns":8,
"firstgid":1,
"image":"platformer.png",
"imageheight":128,
"imagewidth":128,
"margin":0,
"name":"platformer_tileset",
"spacing":0,
"tilecount":64,
"tileheight":16,
"tilewidth":16
}],
"tilewidth":16,
"type":"map",
"version":1.2,
"width":64
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,25 @@
precision mediump float;
varying vec4 v_Position;
uniform vec4 circle_Color;
void main(){
// Default alpha is 0
float alpha = 0.0;
// Radius is 0.5, since the diameter of our quad is 1
float radius = 0.5;
// Get the distance squared of from (0, 0)
float dist_sq = v_Position.x*v_Position.x + v_Position.y*v_Position.y;
if(dist_sq < radius*radius){
// Multiply by 4, since distance squared is at most 0.25
alpha = 4.0*dist_sq;
}
// Use the alpha value in our color
gl_FragColor = vec4(circle_Color);
gl_FragColor.a = alpha;
}

View File

@ -0,0 +1,11 @@
attribute vec4 a_Position;
uniform mat4 u_Transform;
varying vec4 v_Position;
void main(){
gl_Position = u_Transform * a_Position;
v_Position = a_Position;
}

View File

@ -0,0 +1,33 @@
precision mediump float;
varying vec4 v_Position;
uniform vec4 circle_Color;
uniform vec4 default_Color;
// HOMEWORK 3
/*
The fragment shader is where pixel colors are decided.
You'll have to modify this code to make the circle vary between 2 colors.
Currently this will render the exact same thing as the gradient_circle shaders
*/
void main(){
// Default alpha is 0
float alpha = 0.0;
// Radius is 0.5, since the diameter of our quad is 1
float radius = 0.5;
float ratio = (1.0-(v_Position.x+0.25+v_Position.y+0.25));
// Get the distance squared of from (0, 0)
float dist_sq = v_Position.x*v_Position.x + v_Position.y*v_Position.y;
if(dist_sq < radius*radius){
// Multiply by 4, since distance squared is at most 0.25
alpha = 1.0;
}
// Use the alpha value in our color
gl_FragColor = vec4(default_Color[0]*ratio+circle_Color[0]*(1.0-ratio), default_Color[1]*ratio+circle_Color[1]*(1.0-ratio), default_Color[2]*ratio+circle_Color[2]*(1.0-ratio), default_Color[3]*ratio+circle_Color[3]*(1.0-ratio));
gl_FragColor.a = alpha;
}

View File

@ -0,0 +1,11 @@
attribute vec4 a_Position;
uniform mat4 u_Transform;
varying vec4 v_Position;
void main(){
gl_Position = u_Transform * a_Position;
v_Position = a_Position;
}

BIN
hw3/dist/hw3_assets/sprites/road.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
hw3/dist/hw3_assets/sprites/stone.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -0,0 +1,139 @@
{
"name": "cars",
"spriteSheetImage": "cars.png",
"spriteWidth": 256,
"spriteHeight": 256,
"leftBuffer": 0,
"rightBuffer": 0,
"topBuffer": 0,
"bottomBuffer": 0,
"columns": 3,
"rows": 4,
"animations": [
{
"name": "driving",
"repeat": true,
"frames": [
{
"index": 0,
"duration": 10
},
{
"index": 1,
"duration": 10
}
]
},
{
"name": "firing",
"repeat": true,
"frames": [
{
"index": 0,
"duration": 10
},
{
"index": 2,
"duration": 10
},
{
"index": 3,
"duration": 10
}
]
},
{
"name": "damage",
"repeat": true,
"frames": [
{
"index": 4,
"duration": 6
},
{
"index": 5,
"duration": 6
},
{
"index": 4,
"duration": 6
},
{
"index": 5,
"duration": 6
},
{
"index": 4,
"duration": 6
}, {
"index": 5,
"duration": 6
},
{
"index": 4,
"duration": 6
},
{
"index": 5,
"duration": 6
},
{
"index": 4,
"duration": 6
},
{
"index": 5,
"duration": 6
},
{
"index": 4,
"duration": 6
}, {
"index": 5,
"duration": 6
}
]
},
{
"name": "dying",
"repeat": false,
"next": "dead",
"frames": [
{
"index": 6,
"duration": 10
},
{
"index": 7,
"duration": 10
},
{
"index": 8,
"duration": 10
},
{
"index": 9,
"duration": 10
},
{
"index": 10,
"duration": 10
},
{
"index": 11,
"duration": 10
}
]
},
{
"name": "dead",
"repeat": true,
"frames": [
{
"index": 11,
"duration": 20
}
]
}
]
}

View File

@ -0,0 +1,139 @@
{
"name": "cars",
"spriteSheetImage": "cars.png",
"spriteWidth": 256,
"spriteHeight": 256,
"leftBuffer": 0,
"rightBuffer": 0,
"topBuffer": 0,
"bottomBuffer": 0,
"columns": 3,
"rows": 4,
"animations": [
{
"name": "driving",
"repeat": true,
"frames": [
{
"index": 0,
"duration": 10
},
{
"index": 1,
"duration": 10
}
]
},
{
"name": "firing",
"repeat": true,
"frames": [
{
"index": 0,
"duration": 10
},
{
"index": 2,
"duration": 10
},
{
"index": 3,
"duration": 10
}
]
},
{
"name": "damage",
"repeat": true,
"frames": [
{
"index": 4,
"duration": 6
},
{
"index": 5,
"duration": 6
},
{
"index": 4,
"duration": 6
},
{
"index": 5,
"duration": 6
},
{
"index": 4,
"duration": 6
}, {
"index": 5,
"duration": 6
},
{
"index": 4,
"duration": 6
},
{
"index": 5,
"duration": 6
},
{
"index": 4,
"duration": 6
},
{
"index": 5,
"duration": 6
},
{
"index": 4,
"duration": 6
}, {
"index": 5,
"duration": 6
}
]
},
{
"name": "dying",
"repeat": false,
"next": "dead",
"frames": [
{
"index": 6,
"duration": 10
},
{
"index": 7,
"duration": 10
},
{
"index": 8,
"duration": 10
},
{
"index": 9,
"duration": 10
},
{
"index": 10,
"duration": 10
},
{
"index": 11,
"duration": 10
}
]
},
{
"name": "dead",
"repeat": true,
"frames": [
{
"index": 11,
"duration": 20
}
]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

34
hw3/gulpfile.js Normal file
View File

@ -0,0 +1,34 @@
var gulp = require('gulp');
var browserify = require('browserify');
var source = require('vinyl-source-stream');
var watchify = require('watchify');
var tsify = require('tsify');
var fancy_log = require('fancy-log');
var paths = {
pages: ['src/*.html']
};
var watchedBrowserify = watchify(browserify({
basedir: '.',
debug: true,
entries: ['src/main.ts'],
cache: {},
packageCache: {}
}).plugin(tsify));
gulp.task('copy-html', function () {
return gulp.src(paths.pages)
.pipe(gulp.dest('dist'));
});
function bundle() {
return watchedBrowserify
.bundle()
.on('error', fancy_log)
.pipe(source('bundle.js'))
.pipe(gulp.dest('dist'));
}
gulp.task('default', gulp.series(gulp.parallel('copy-html'), bundle));
watchedBrowserify.on('update', bundle);
watchedBrowserify.on('log', fancy_log);

9236
hw3/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

21
hw3/package.json Normal file
View File

@ -0,0 +1,21 @@
{
"name": "wolfie2d",
"version": "1.0.0",
"description": "A game engine written in TypeScript",
"main": "./dist/main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Joe Weaver",
"license": "ISC",
"devDependencies": {
"browserify": "^16.5.1",
"fancy-log": "^1.3.3",
"gulp": "^4.0.0",
"gulp-typescript": "^6.0.0-alpha.1",
"tsify": "^5.0.0",
"typescript": "^3.9.7",
"vinyl-source-stream": "^2.0.0",
"watchify": "^3.11.1"
}
}

6
hw3/readme.md Normal file
View File

@ -0,0 +1,6 @@
# Game Engine
## How to transpile and run
Start gulp by just running `gulp` in the console. Start the code by running `dist/main.js` with Web Server for Chrome or a similar product. Anytime you save, gulp should recompile the code automatically.
Setup follows [this helpful guide from TypeScript] (https://www.typescriptlang.org/docs/handbook/gulp.html) (Up through Watchify).

View File

@ -0,0 +1,65 @@
import Actor from "../DataTypes/Interfaces/Actor";
import Updateable from "../DataTypes/Interfaces/Updateable";
import AI from "../DataTypes/Interfaces/AI";
import Map from "../DataTypes/Map";
/**
* A manager class for all of the AI in a scene.
* Keeps a list of registered actors and handles AI generation for actors.
*/
export default class AIManager implements Updateable {
/** The array of registered actors */
actors: Array<Actor>;
/** Maps AI names to their constructors */
registeredAI: Map<AIConstructor>;
constructor(){
this.actors = new Array();
this.registeredAI = new Map();
}
/**
* Registers an actor with the AIManager
* @param actor The actor to register
*/
registerActor(actor: Actor): void {
this.actors.push(actor);
}
removeActor(actor: Actor): void {
let index = this.actors.indexOf(actor);
if(index !== -1){
this.actors.splice(index, 1);
}
}
/**
* Registers an AI with the AIManager for use later on
* @param name The name of the AI to register
* @param constr The constructor for the AI
*/
registerAI(name: string, constr: new <T extends AI>() => T ): void {
this.registeredAI.add(name, constr);
}
/**
* Generates an AI instance from its name
* @param name The name of the AI to add
* @returns A new AI instance
*/
generateAI(name: string): AI {
if(this.registeredAI.has(name)){
return new (this.registeredAI.get(name))();
} else {
throw `Cannot create AI with name ${name}, no AI with that name is registered`;
}
}
update(deltaT: number): void {
// Run the ai for every active actor
this.actors.forEach(actor => { if(actor.aiActive) actor.ai.update(deltaT) });
}
}
type AIConstructor = new <T extends AI>() => T;

View File

@ -0,0 +1,21 @@
import AI from "../DataTypes/Interfaces/AI";
import GameEvent from "../Events/GameEvent";
import GameNode from "../Nodes/GameNode";
export default abstract class ControllerAI implements AI {
/** The owner of this controller */
owner: GameNode;
/** Removes the instance of the owner of this AI */
destroy(): void {
delete this.owner;
}
abstract initializeAI(owner: GameNode, options: Record<string, any>): void;
abstract activate(options: Record<string, any>): void;
abstract handleEvent(event: GameEvent): void;
abstract update(deltaT: number): void;
}

View File

@ -0,0 +1,24 @@
import AI from "../DataTypes/Interfaces/AI";
import StateMachine from "../DataTypes/State/StateMachine";
import GameNode from "../Nodes/GameNode";
/**
* A version of a @reference[StateMachine] that is configured to work as an AI controller for a @reference[GameNode]
*/
export default class StateMachineAI extends StateMachine implements AI {
/** The GameNode that uses this StateMachine for its AI */
protected owner: GameNode;
// @implemented
initializeAI(owner: GameNode, config: Record<string, any>): void {}
// @implemented
destroy(){
// Get rid of our reference to the owner
delete this.owner;
this.receiver.destroy();
}
// @implemented
activate(options: Record<string, any>): void {}
}

View File

@ -0,0 +1,17 @@
// @ignorePage
/**
* An interface for all iterable data custom data structures
*/
export default interface Collection {
/**
* Iterates through all of the items in this data structure.
* @param func The function to evaluate of every item in the collection
*/
forEach(func: Function): void;
/**
* Clears the contents of the data structure
*/
clear(): void;
}

View File

@ -0,0 +1,8 @@
// @ignorePage
/**
* A placeholder function for No Operation. Does nothing
*/
const NullFunc = () => {};
export default NullFunc;

View File

@ -0,0 +1,22 @@
/**
* A linked-list for the edges in a @reference[Graph].
*/
export default class EdgeNode {
/** The node in the Graph this edge connects to */
y: number;
/** The weight of this EdgeNode */
weight: number;
/** The next EdgeNode in the linked-list */
next: EdgeNode;
/**
* Creates a new EdgeNode
* @param index The index of the node this edge connects to
* @param weight The weight of this edge
*/
constructor(index: number, weight?: number){
this.y = index;
this.next = null;
this.weight = weight ? weight : 1;
}
}

View File

@ -0,0 +1,145 @@
import EdgeNode from "./EdgeNode";
export const MAX_V = 100;
/**
* An implementation of a graph data structure using edge lists. Inspired by The Algorithm Design Manual.
*/
export default class Graph {
/** An array of edges at the node specified by the index */
edges: Array<EdgeNode>;
/** An array representing the degree of the node specified by the index */
degree: Array<number>;
/** The number of vertices in the graph */
numVertices: number;
/** The number of edges in the graph */
numEdges: number;
/** Whether or not the graph is directed */
directed: boolean;
/** Whether or not the graph is weighted */
weighted: boolean;
/**
* Constructs a new graph
* @param directed Whether or not this graph is directed
*/
constructor(directed: boolean = false){
this.directed = directed;
this.weighted = false;
this.numVertices = 0;
this.numEdges = 0;
this.edges = new Array(MAX_V);
this.degree = new Array(MAX_V);
}
/** Adds a node to this graph and returns the index of it
* @returns The index of the new node
*/
addNode(): number {
this.numVertices++;
return this.numVertices;
}
/** Adds an edge between node x and y, with an optional weight
* @param x The index of the start of the edge
* @param y The index of the end of the edge
* @param weight The optional weight of the new edge
*/
addEdge(x: number, y: number, weight?: number): void {
let edge = new EdgeNode(y, weight);
if(this.edges[x]){
edge.next = this.edges[x];
}
this.edges[x] = edge;
if(!this.directed){
edge = new EdgeNode(x, weight);
if(this.edges[y]){
edge.next = this.edges[y];
}
this.edges[y] = edge;
}
this.numEdges += 1;
}
/**
* Checks whether or not an edge exists between two nodes.
* This check is directional if this is a directed graph.
* @param x The first node
* @param y The second node
* @returns true if an edge exists, false otherwise
*/
edgeExists(x: number, y: number): boolean {
let edge = this.edges[x];
while(edge !== null){
if(edge.y === y){
return true;
}
edge = edge.next;
}
}
/**
* Gets the edge list associated with node x
* @param x The index of the node
* @returns The head of a linked-list of edges
*/
getEdges(x: number): EdgeNode {
return this.edges[x];
}
/**
* Gets the degree associated with node x
* @param x The index of the node
*/
getDegree(x: number): number {
return this.degree[x];
}
/**
* Converts the specifed node into a string
* @param index The index of the node to convert to a string
* @returns The string representation of the node: "Node x"
*/
protected nodeToString(index: number): string {
return "Node " + index;
}
/**
* Converts the Graph into a string format
* @returns The graph as a string
*/
toString(): string {
let retval = "";
for(let i = 0; i < this.numVertices; i++){
let edge = this.edges[i];
let edgeStr = "";
while(edge !== null){
edgeStr += edge.y.toString();
if(this.weighted){
edgeStr += " (" + edge.weight + ")";
}
if(edge.next !== null){
edgeStr += ", ";
}
edge = edge.next;
}
retval += this.nodeToString(i) + ": " + edgeStr + "\n";
}
return retval;
}
}

View File

@ -0,0 +1,94 @@
import Graph, { MAX_V } from "./Graph";
import Vec2 from "../Vec2";
import DebugRenderable from "../Interfaces/DebugRenderable";
/**
* An extension of Graph that has nodes with positions in 2D space.
* This is a weighted graph (though not inherently directd)
*/
export default class PositionGraph extends Graph implements DebugRenderable {
/** An array of the positions of the nodes in this graph */
positions: Array<Vec2>;
/**
* Createes a new PositionGraph
* @param directed Whether or not this graph is directed
*/
constructor(directed: boolean = false){
super(directed);
this.positions = new Array(MAX_V);
}
/**
* Adds a positioned node to this graph
* @param position The position of the node to add
* @returns The index of the added node
*/
addPositionedNode(position: Vec2): number {
this.positions[this.numVertices] = position;
return this.addNode();
}
/**
* Changes the position of a node.
* Automatically adjusts the weights of the graph tied to this node.
* As such, be warned that this function has an O(n + m) running time, and use it sparingly.
* @param index The index of the node
* @param position The new position of the node
*/
setNodePosition(index: number, position: Vec2): void {
this.positions[index] = position;
// Recalculate all weights associated with this index
for(let i = 0; i < this.numEdges; i++){
let edge = this.edges[i];
while(edge !== null){
// If this node is on either side of the edge, recalculate weight
if(i === index || edge.y === index){
edge.weight = this.positions[i].distanceTo(this.positions[edge.y]);
}
edge = edge.next;
}
}
}
/**
* Gets the position of a node
* @param index The index of the node
* @returns The position of the node
*/
getNodePosition(index: number): Vec2 {
return this.positions[index];
}
/**
* Adds an edge to this graph between node x and y.
* Automatically calculates the weight of the edge as the distance between the nodes.
* @param x The beginning of the edge
* @param y The end of the edge
*/
addEdge(x: number, y: number): void {
if(!this.positions[x] || !this.positions[y]){
throw "Can't add edge to un-positioned node!";
}
// Weight is the distance between the nodes
let weight = this.positions[x].distanceTo(this.positions[y]);
super.addEdge(x, y, weight);
}
// @override
protected nodeToString(index: number): string {
return "Node " + index + " - " + this.positions[index].toString();
}
debugRender = (): void => {
// for(let point of this.positions){
// ctx.fillRect((point.x - origin.x - 4)*zoom, (point.y - origin.y - 4)*zoom, 8, 8);
// }
}
}

View File

@ -0,0 +1,20 @@
import GameEvent from "../../Events/GameEvent";
import GameNode from "../../Nodes/GameNode";
import Updateable from "./Updateable";
/**
* Defines a controller for a bot or a human. Must be able to update
*/
export default interface AI extends Updateable {
/** Initializes the AI with the actor and any additional config */
initializeAI(owner: GameNode, options: Record<string, any>): void;
/** Clears references from to the owner */
destroy(): void;
/** Activates this AI from a stopped state and allows variables to be passed in */
activate(options: Record<string, any>): void;
/** Handles events from the Actor */
handleEvent(event: GameEvent): void;
}

View File

@ -0,0 +1,40 @@
import NavigationPath from "../../Pathfinding/NavigationPath";
import AI from "./AI";
/**
* A game object that has an AI and can perform its own actions every update cycle
*/
export default interface Actor {
/** The AI of the actor */
ai: AI;
/** The activity status of the actor */
aiActive: boolean;
/** The path that navigation will follow */
path: NavigationPath;
/** A flag representing whether or not the actor is currently pathfinding */
pathfinding: boolean;
/**
* Adds an AI to this Actor.
* @param ai The name of the AI, or the actual AI, to add to the Actor.
* @param options The options to give to the AI for initialization.
*/
addAI<T extends AI>(ai: string | (new () => T), options: Record<string, any>): void;
/**
* Sets the AI to start/stop for this Actor.
* @param active The new active status of the AI.
* @param options An object that allows options to be pased to the activated AI
*/
setAIActive(active: boolean, options: Record<string, any>): void;
/**
* Moves this GameNode along a path
* @param speed The speed to move with
* @param path The path we're moving along
*/
moveOnPath(speed: number, path: NavigationPath): void;
}

View File

@ -0,0 +1,7 @@
/**
* Represents a game object that can be rendered in Debug mode
*/
export default interface DebugRenderable {
/** Renders the debugging information for this object. */
debugRender(): void;
}

View File

@ -0,0 +1,12 @@
import Vec2 from "../Vec2";
import NavigationPath from "../../Pathfinding/NavigationPath";
/** Represents an entity that can be navigated on. */
export default interface Navigable {
/**
* Gets a new navigation path based on this Navigable object.
* @param fromPosition The position to start navigation from.
* @param toPosition The position to navigate to.
*/
getNavigationPath(fromPosition: Vec2, toPosition: Vec2): NavigationPath;
}

View File

@ -0,0 +1,120 @@
import Shape from "../Shapes/Shape";
import AABB from "../Shapes/AABB";
import Vec2 from "../Vec2";
import Map from "../Map";
/**
* Describes an object that can opt into physics.
*/
export default interface Physical {
/** A flag for whether or not this object has initialized game physics. */
hasPhysics: boolean;
/** Represents whether the object is moving or not. */
moving: boolean;
/** Represent whether the object is frozen from moving or not. */
frozen: boolean;
/** Represents whether the object is on the ground or not. */
onGround: boolean;
/** Reprsents whether the object is on the wall or not. */
onWall: boolean;
/** Reprsents whether the object is on the ceiling or not. */
onCeiling: boolean;
/** Represnts whether this object has active physics or not. */
active: boolean;
/** The shape of the collider for this physics object. */
collisionShape: Shape;
/** The offset of the collision shape from the center of the node */
colliderOffset: Vec2;
/** Represents whether this object can move or not. */
isStatic: boolean;
/** Represents whether this object is collidable (solid) or not. */
isCollidable: boolean;
/** Represnts whether this object is a trigger or not. */
isTrigger: boolean;
/** The trigger mask for this node */
triggerMask: number;
/** Events to trigger for collision enters. */
triggerEnters: Array<string>;
/** Events to trigger for collision exits */
triggerExits: Array<string>;
/** A vector that allows velocity to be passed to the physics engine */
_velocity: Vec2;
/** The rectangle swept by the movement of this object, if dynamic */
sweptRect: AABB;
/** A boolean representing whether or not the node just collided with the tilemap */
collidedWithTilemap: boolean;
/** The physics group this node belongs to */
group: number;
isPlayer: boolean;
isColliding: boolean;
/*---------- FUNCTIONS ----------*/
/**
* Tells the physics engine to handle a move by this object.
* @param velocity The velocity with which to move the object.
*/
move(velocity: Vec2): void;
/**
* The move actually done by the physics engine after collision checks are done.
* @param velocity The velocity with which the object will move.
*/
finishMove(): void;
/**
* Adds physics to this object
* @param collisionShape The shape of this collider for this object
* @param isCollidable Whether this object will be able to collide with other objects
* @param isStatic Whether this object will be static or not
*/
addPhysics(collisionShape?: Shape, colliderOffset?: Vec2, isCollidable?: boolean, isStatic?: boolean): void;
/** Removes this object from the physics system */
removePhysics(): void;
/** Prevents this object from participating in all collisions and triggers. It can still move. */
disablePhysics(): void;
/** Enables this object to participate in collisions and triggers. This is only necessary if disablePhysics was called */
enablePhysics(): void;
/**
* Sets this object to be a trigger for a specific group
* @param group The name of the group that activates the trigger
* @param onEnter The name of the event to send when this trigger is activated
* @param onExit The name of the event to send when this trigger stops being activated
*/
setTrigger(group: string, onEnter: string, onExit: string): void;
/**
* Sets the physics group of this node
* @param group The name of the group
*/
setGroup(group: string): void;
/**
* If used before "move()", it will tell you the velocity of the node after its last movement
*/
getLastVelocity(): Vec2;
}

View File

@ -0,0 +1,10 @@
import Vec2 from "../Vec2";
/** An object that has a position */
export default interface Positioned {
/** The center of this object. */
position: Vec2;
/** The center of this object relative to the viewport. */
readonly relativePosition: Vec2;
}

View File

@ -0,0 +1,21 @@
import Vec2 from "../Vec2";
import AABB from "../Shapes/AABB";
/** An object that is a region, with a size, scale, and boundary. */
export default interface Region {
/** The size of this object. */
size: Vec2;
/** The scale of this object. */
scale: Vec2;
/** The size of the object taking into account the zoom and scale */
readonly sizeWithZoom: Vec2;
/** The bounding box of this object. */
boundary: AABB;
}
export function isRegion(arg: any): boolean {
return arg && arg.size && arg.scale && arg.boundary;
}

View File

@ -0,0 +1,7 @@
/**
* Represents an object with a unique id
*/
export default interface Unique {
/** The unique id of this object. */
id: number;
}

View File

@ -0,0 +1,8 @@
/** Represents a game object that can be updated */
export default interface Updateable {
/**
* Updates this object.
* @param deltaT The timestep of the update.
*/
update(deltaT: number): void;
}

View File

@ -0,0 +1,63 @@
import Collection from "./Collection";
/**
* A doubly linked list
*/
export default class List<T> implements Collection {
private head: ListItem<T>;
private tail: ListItem<T>;
private _size: number;
constructor(){
this.head = null;
this.tail = null;
this._size = 0;
}
get size(): number {
return this._size;
}
add(value: T){
if(this._size === 0){
// There were no items in the list previously, so head and tail are the same
this.head = new ListItem(value, null, null);
this.tail = this.head;
} else {
this.tail.next = new ListItem(value, this.tail, null);
this.tail = this.tail.next;
}
// Increment the size
this._size += 1;
}
forEach(func: Function): void {
let p = this.head;
while(p !== null){
func(p.value);
p = p.next;
}
}
clear(): void {
this.head = null
this.tail = null
this._size = 0;
}
}
class ListItem<T> {
value: T;
next: ListItem<T>;
prev: ListItem<T>;
constructor(value: T, next: ListItem<T>, prev: ListItem<T>){
this.value = value;
this.next = next;
this.prev = prev;
}
}

View File

@ -0,0 +1,87 @@
import Collection from "./Collection";
/**
* Associates strings with elements of type T
*/
export default class Map<T> implements Collection {
private map: Record<string, T>;
/** Creates a new map */
constructor(){
this.map = {};
}
/**
* Adds a value T stored at a key.
* @param key The key of the item to be stored
* @param value The item to be stored
*/
add(key: string, value: T): void {
this.map[key] = value;
}
/**
* Get the value associated with a key.
* @param key The key of the item
* @returns The item at the key or undefined
*/
get(key: string): T {
return this.map[key];
}
/**
* An alias of add. Sets the value stored at key to the new specified value
* @param key The key of the item to be stored
* @param value The item to be stored
*/
set(key: string, value: T): void {
this.add(key, value);
}
/**
* Returns true if there is a value stored at the specified key, false otherwise.
* @param key The key to check
* @returns A boolean representing whether or not there is an item at the given key.
*/
has(key: string): boolean {
return this.map[key] !== undefined;
}
/**
* Returns an array of all of the keys in this map.
* @returns An array containing all keys in the map.
*/
keys(): Array<string> {
return Object.keys(this.map);
}
// @implemented
forEach(func: (key: string) => void): void {
Object.keys(this.map).forEach(key => func(key));
}
/**
* Deletes an item associated with a key
* @param key The key at which to delete an item
*/
delete(key: string): void {
delete this.map[key];
}
// @implemented
clear(): void {
this.forEach(key => delete this.map[key]);
}
/**
* Converts this map to a string representation.
* @returns The string representation of this map.
*/
toString(): string {
let str = "";
this.forEach((key) => str += key + " -> " + this.get(key).toString() + "\n");
return str;
}
}

View File

@ -0,0 +1,167 @@
import Vec2 from "./Vec2";
/** A 4x4 matrix0 */
export default class Mat4x4 {
private mat: Float32Array;
constructor(){
this.mat = new Float32Array([
0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0
]);
}
// Static members
static get IDENTITY(): Mat4x4 {
return new Mat4x4().identity();
}
static get ZERO(): Mat4x4 {
return new Mat4x4().zero();
}
// Accessors
set _00(x: number) {
this.mat[0] = x;
}
set(col: number, row: number, value: number): Mat4x4 {
if(col < 0 || col > 3 || row < 0 || row > 3){
throw `Error - index (${col}, ${row}) is out of bounds for Mat4x4`
}
this.mat[row*4 + col] = value;
return this;
}
get(col: number, row: number): number {
return this.mat[row*4 + col];
}
setAll(...items: Array<number>): Mat4x4 {
this.mat.set(items);
return this;
}
identity(): Mat4x4 {
return this.setAll(
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
)
}
zero(): Mat4x4 {
return this.setAll(
0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0
);
}
/**
* Makes this Mat4x4 a rotation matrix of the specified number of radians ccw
* @param zRadians The number of radians to rotate
* @returns this Mat4x4
*/
rotate(zRadians: number): Mat4x4 {
return this.setAll(
Math.cos(zRadians), -Math.sin(zRadians), 0, 0,
Math.sin(zRadians), Math.cos(zRadians), 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
);
}
/**
* Turns this Mat4x4 into a translation matrix of the specified translation
* @param translation The translation in x and y
* @returns this Mat4x4
*/
translate(translation: Vec2 | Float32Array): Mat4x4 {
// If translation is a vec, get its array
if(translation instanceof Vec2){
translation = translation.toArray();
}
return this.setAll(
1, 0, 0, translation[0],
0, 1, 0, translation[1],
0, 0, 1, 0,
0, 0, 0, 1
);
}
scale(scale: Vec2 | Float32Array | number): Mat4x4 {
// Make sure scale is a float32Array
if(scale instanceof Vec2){
scale = scale.toArray();
} else if(!(scale instanceof Float32Array)){
scale = new Float32Array([scale, scale]);
}
return this.setAll(
scale[0], 0, 0, 0,
0, scale[1], 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
);
}
/**
* Returns a new Mat4x4 that represents the right side multiplication THIS x OTHER
* @param other The other Mat4x4 to multiply by
* @returns a new Mat4x4 containing the product of these two Mat4x4s
*/
mult(other: Mat4x4, out?: Mat4x4): Mat4x4 {
let temp = new Float32Array(16);
for(let i = 0; i < 4; i++){
for(let j = 0; j < 4; j++){
let value = 0;
for(let k = 0; k < 4; k++){
value += this.get(k, i) * other.get(j, k);
}
temp[j*4 + i] = value;
}
}
if(out !== undefined){
return out.setAll(...temp);
} else {
return new Mat4x4().setAll(...temp);
}
}
/**
* Multiplies all given matricies in order. e.g. MULT(A, B, C) -> A*B*C
* @param mats A list of Mat4x4s to multiply in order
* @returns A new Mat4x4 holding the result of the operation
*/
static MULT(...mats: Array<Mat4x4>): Mat4x4 {
// Create a new array
let temp = Mat4x4.IDENTITY;
// Multiply by every array in order, in place
for(let i = 0; i < mats.length; i++){
temp.mult(mats[i], temp);
}
return temp;
}
toArray(): Float32Array {
return this.mat;
}
toString(): string {
return `|${this.mat[0].toFixed(2)}, ${this.mat[1].toFixed(2)}, ${this.mat[2].toFixed(2)}, ${this.mat[3].toFixed(2)}|\n` +
`|${this.mat[4].toFixed(2)}, ${this.mat[5].toFixed(2)}, ${this.mat[6].toFixed(2)}, ${this.mat[7].toFixed(2)}|\n` +
`|${this.mat[8].toFixed(2)}, ${this.mat[9].toFixed(2)}, ${this.mat[10].toFixed(2)}, ${this.mat[11].toFixed(2)}|\n` +
`|${this.mat[12].toFixed(2)}, ${this.mat[13].toFixed(2)}, ${this.mat[14].toFixed(2)}, ${this.mat[15].toFixed(2)}|`;
}
}

View File

@ -0,0 +1,40 @@
import Physical from "../Interfaces/Physical";
import AABB from "../Shapes/AABB";
import Vec2 from "../Vec2";
import Hit from "./Hit";
/**
* A class that contains the area of overlap of two colliding objects to allow for sorting by the physics system.
*/
export default class AreaCollision {
/** The area of the overlap for the colliding objects */
area: number;
/** The AABB of the other collider in this collision */
collider: AABB;
/** Type of the collision */
type: string;
/** Ther other object in the collision */
other: Physical;
/** The tile, if this was a tilemap collision */
tile: Vec2;
/** The physics hit for this object */
hit: Hit;
/**
* Creates a new AreaCollision object
* @param area The area of the collision
* @param collider The other collider
*/
constructor(area: number, collider: AABB, other: Physical, type: string, tile: Vec2){
this.area = area;
this.collider = collider;
this.other = other;
this.type = type;
this.tile = tile;
}
}

View File

@ -0,0 +1,18 @@
import Vec2 from "../Vec2";
/**
* An object representing the data collected from a physics hit between two geometric objects.
* Inspired by the helpful collision documentation @link(here)(https://noonat.github.io/intersect/).
*/
export default class Hit {
/** The time of the collision. Only numbers 0 through 1 happen in this frame. */
time: number;
/** The near times of the collision */
nearTimes: Vec2 = Vec2.ZERO;
/** The position of the collision */
pos: Vec2 = Vec2.ZERO;
/** The overlap distance of the hit */
delta: Vec2 = Vec2.ZERO;
/** The normal vector of the hit */
normal: Vec2 = Vec2.ZERO;
}

View File

@ -0,0 +1,156 @@
import Vec2 from "./Vec2";
import Collection from "./Collection";
import AABB from "./Shapes/AABB"
import Positioned from "./Interfaces/Positioned";
// TODO - Make max depth
// @ignorePage
/**
* Primarily used to organize the scene graph
*/
export default class QuadTree<T extends Positioned> implements Collection {
/**
* The center of this quadtree
*/
protected boundary: AABB;
/**
* The number of elements this quadtree root can hold before splitting
*/
protected capacity: number;
/**
* The maximum height of the quadtree from this root
*/
protected maxDepth: number;
/**
* Represents whether the quadtree is a root or a leaf
*/
protected divided: boolean;
/**
* The array of the items in this quadtree
*/
protected items: Array<T>;
// The child quadtrees of this one
protected nw: QuadTree<T>;
protected ne: QuadTree<T>;
protected sw: QuadTree<T>;
protected se: QuadTree<T>;
constructor(center: Vec2, size: Vec2, maxDepth?: number, capacity?: number){
this.boundary = new AABB(center, size);
this.maxDepth = maxDepth !== undefined ? maxDepth : 10
this.capacity = capacity ? capacity : 10;
// If we're at the bottom of the tree, don't set a max size
if(this.maxDepth === 0){
this.capacity = Infinity;
}
this.divided = false;
this.items = new Array();
}
/**
* Inserts a new item into this quadtree. Defers to children if this quadtree is divided
* or divides the quadtree if capacity is exceeded with this add.
* @param item The item to add to the quadtree
*/
insert(item: T){
// If the item is inside of the bounds of this quadtree
if(this.boundary.containsPointSoft(item.position)){
if(this.divided){
// Defer to the children
this.deferInsert(item);
} else if (this.items.length < this.capacity){
// Add to this items list
this.items.push(item);
} else {
// We aren't divided, but are at capacity - divide
this.subdivide();
this.deferInsert(item);
this.divided = true;
}
}
}
/**
* Divides this quadtree up into 4 smaller ones - called through insert.
*/
protected subdivide(): void {
let x = this.boundary.x;
let y = this.boundary.y;
let hw = this.boundary.hw;
let hh = this.boundary.hh;
this.nw = new QuadTree(new Vec2(x-hw/2, y-hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1);
this.ne = new QuadTree(new Vec2(x+hw/2, y-hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1)
this.sw = new QuadTree(new Vec2(x-hw/2, y+hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1)
this.se = new QuadTree(new Vec2(x+hw/2, y+hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1)
this.distributeItems();
}
/**
* Distributes the items of this quadtree into its children.
*/
protected distributeItems(): void {
this.items.forEach(item => this.deferInsert(item));
// Delete the items from this array
this.items.forEach((item, index) => delete this.items[index]);
this.items.length = 0;
}
/**
* Defers this insertion to the children of this quadtree
* @param item
*/
protected deferInsert(item: T): void {
this.nw.insert(item);
this.ne.insert(item);
this.sw.insert(item);
this.se.insert(item);
}
/**
* Renders the quadtree for demo purposes.
* @param ctx
*/
public render_demo(ctx: CanvasRenderingContext2D): void {
ctx.strokeStyle = "#FF0000";
ctx.strokeRect(this.boundary.x - this.boundary.hw, this.boundary.y - this.boundary.hh, 2*this.boundary.hw, 2*this.boundary.hh);
if(this.divided){
this.nw.render_demo(ctx);
this.ne.render_demo(ctx);
this.sw.render_demo(ctx);
this.se.render_demo(ctx);
}
}
forEach(func: Function): void {
// If divided, send the call down
if(this.divided){
this.nw.forEach(func);
this.ne.forEach(func);
this.sw.forEach(func);
this.se.forEach(func);
} else {
// Otherwise, iterate over items
for(let i = 0; i < this.items.length; i++){
func(this.items[i]);
}
}
}
clear(): void {
throw new Error("Method not implemented.");
}
}

View File

@ -0,0 +1,130 @@
import Collection from "./Collection";
/**
* A FIFO queue with elements of type T
*/
export default class Queue<T> implements Collection {
/** The maximum number of elements in the Queue */
private readonly MAX_ELEMENTS: number;
/** The internal representation of the queue */
private q: Array<T>;
/** The head of the queue */
private head: number;
/** The tail of the queue */
private tail: number;
/** The current number of items in the queue */
private size: number;
/**
* Constructs a new queue
* @param maxElements The maximum size of the stack
*/
constructor(maxElements: number = 100){
this.MAX_ELEMENTS = maxElements;
this.q = new Array(this.MAX_ELEMENTS);
this.head = 0;
this.tail = 0;
this.size = 0;
}
/**
* Adds an item to the back of the queue
* @param item The item to add to the back of the queue
*/
enqueue(item: T): void{
if((this.tail + 1) % this.MAX_ELEMENTS === this.head){
throw new Error("Queue full - cannot add element");
}
this.size += 1;
this.q[this.tail] = item;
this.tail = (this.tail + 1) % this.MAX_ELEMENTS;
}
/**
* Retrieves an item from the front of the queue
* @returns The item at the front of the queue
*/
dequeue(): T {
if(this.head === this.tail){
throw new Error("Queue empty - cannot remove element");
}
this.size -= 1;
let item = this.q[this.head];
// Now delete the item
delete this.q[this.head];
this.head = (this.head + 1) % this.MAX_ELEMENTS;
return item;
}
/**
* Returns the item at the front of the queue, but does not remove it
* @returns The item at the front of the queue
*/
peekNext(): T {
if(this.head === this.tail){
throw "Queue empty - cannot get element"
}
let item = this.q[this.head];
return item;
}
/**
* Returns true if the queue has items in it, false otherwise
* @returns A boolean representing whether or not this queue has items
*/
hasItems(): boolean {
return this.head !== this.tail;
}
/**
* Returns the number of elements in the queue.
* @returns The size of the queue
*/
getSize(): number {
return this.size;
}
// @implemented
clear(): void {
this.forEach((item, index) => delete this.q[index]);
this.size = 0;
this.head = this.tail;
}
// @implemented
forEach(func: (item: T, index?: number) => void): void {
let i = this.head;
while(i !== this.tail){
func(this.q[i], i);
i = (i + 1) % this.MAX_ELEMENTS;
}
}
/**
* Converts this queue into a string format
* @returns A string representing this queue
*/
toString(): string {
let retval = "";
this.forEach( (item, index) => {
let str = item.toString()
if(index !== 0){
str += " -> "
}
retval = str + retval;
});
return "Top -> " + retval;
}
}

View File

@ -0,0 +1,271 @@
import Vec2 from "./Vec2";
import Collection from "./Collection";
import AABB from "./Shapes/AABB";
import Region from "./Interfaces/Region";
import Unique from "./Interfaces/Unique";
import Map from "./Map";
/**
* A quadtree data structure implemented to work with regions rather than points.
* Elements in this quadtree have a position and an area, and thus can span multiple
* quadtree branches.
*/
export default class QuadTree<T extends Region & Unique> implements Collection {
/** The center of this quadtree */
protected boundary: AABB;
/** The number of elements this quadtree root can hold before splitting */
protected capacity: number;
/** The maximum height of the quadtree from this root */
protected maxDepth: number;
/** Represents whether the quadtree is a root or a leaf */
protected divided: boolean;
/** The array of the items in this quadtree */
protected items: Array<T>;
// The child quadtrees of this one
/** The top left child */
protected nw: QuadTree<T>;
/** The top right child */
protected ne: QuadTree<T>;
/** The bottom left child */
protected sw: QuadTree<T>;
/** The bottom right child */
protected se: QuadTree<T>;
constructor(center: Vec2, size: Vec2, maxDepth?: number, capacity?: number){
this.boundary = new AABB(center, size);
this.maxDepth = maxDepth !== undefined ? maxDepth : 10
this.capacity = capacity ? capacity : 10;
// If we're at the bottom of the tree, don't set a max size
if(this.maxDepth === 1){
this.capacity = Infinity;
}
this.divided = false;
this.items = new Array();
// Create all of the children for this quadtree if there are any
if(this.maxDepth > 1){
let x = this.boundary.x;
let y = this.boundary.y;
let hw = this.boundary.hw;
let hh = this.boundary.hh;
this.nw = new QuadTree(new Vec2(x-hw/2, y-hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1, this.capacity);
this.ne = new QuadTree(new Vec2(x+hw/2, y-hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1, this.capacity);
this.sw = new QuadTree(new Vec2(x-hw/2, y+hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1, this.capacity);
this.se = new QuadTree(new Vec2(x+hw/2, y+hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1, this.capacity);
}
}
/**
* Inserts a new item into this quadtree. Defers to children if this quadtree is divided
* or divides the quadtree if capacity is exceeded with this add.
* @param item The item to add to the quadtree
*/
insert(item: T): void {
// If the item is inside of the bounds of this quadtree
if(this.boundary.overlaps(item.boundary)){
if(this.divided){
// Defer to the children
this.deferInsert(item);
} else if (this.items.length < this.capacity){
// Add to this items list
this.items.push(item);
} else {
// We aren't divided, but are at capacity - divide
this.subdivide();
this.deferInsert(item);
}
}
}
/**
* Returns all items at this point.
* @param point The point to query at
* @returns A list of all elements in the quadtree that contain the specified point
*/
queryPoint(point: Vec2): Array<T> {
// A matrix to keep track of our results
let results = new Array<T>();
// A map to keep track of the items we've already found
let uniqueMap = new Map<T>();
// Query and return
this._queryPoint(point, results, uniqueMap);
return results;
}
// @ignoreFunction
/**
* A recursive function called by queryPoint
* @param point The point being queried
* @param results The results matrix
* @param uniqueMap A map that stores the unique ids of the results so we know what was already found
*/
protected _queryPoint(point: Vec2, results: Array<T>, uniqueMap: Map<T>): void {
// Does this quadtree even contain the point?
if(!this.boundary.containsPointSoft(point)) return;
// If the matrix is divided, ask its children for results
if(this.divided){
this.nw._queryPoint(point, results, uniqueMap);
this.ne._queryPoint(point, results, uniqueMap);
this.sw._queryPoint(point, results, uniqueMap);
this.se._queryPoint(point, results, uniqueMap);
} else {
// Otherwise, return a set of the items
for(let item of this.items){
let id = item.id.toString();
// If the item hasn't been found yet and it contains the point
if(!uniqueMap.has(id) && item.boundary.containsPoint(point)){
// Add it to our found points
uniqueMap.add(id, item);
results.push(item);
}
}
}
}
/**
* Returns all items in this region
* @param boundary The region to check
* @param inclusionCheck Allows for additional inclusion checks to further refine searches
* @returns A list of all elements in the specified region
*/
queryRegion(boundary: AABB): Array<T> {
// A matrix to keep track of our results
let results = new Array<T>();
// A map to keep track of the items we've already found
let uniqueMap = new Array<boolean>();
// Query and return
this._queryRegion(boundary, results, uniqueMap);
return results;
}
// @ignoreFunction
/**
* A recursive function called by queryPoint
* @param point The point being queried
* @param results The results matrix
* @param uniqueMap A map that stores the unique ids of the results so we know what was already found
*/
protected _queryRegion(boundary: AABB, results: Array<T>, uniqueMap: Array<boolean>): void {
// Does this quadtree even contain the point?
if(!this.boundary.overlaps(boundary)) return;
// If the matrix is divided, ask its children for results
if(this.divided){
this.nw._queryRegion(boundary, results, uniqueMap);
this.ne._queryRegion(boundary, results, uniqueMap);
this.sw._queryRegion(boundary, results, uniqueMap);
this.se._queryRegion(boundary, results, uniqueMap);
} else {
// Otherwise, return a set of the items
for(let item of this.items){
// TODO - This is REALLY slow for some reason when we check for unique keys
// let id = item.getId().toString();
// // If the item hasn't been found yet and it contains the point
// if(!uniqueMap.has(id) && item.getBoundary().overlaps(boundary)){
// // Add it to our found points
// uniqueMap.add(id, item);
// results.push(item);
// }
// Maybe this is better? Just use a boolean array with no string nonsense?
if(item.id >= uniqueMap.length || !uniqueMap[item.id]){
if(item.boundary.overlaps(boundary)){
results.push(item);
uniqueMap[item.id] = true;
}
}
}
}
}
/**
* Divides this quadtree up into 4 smaller ones - called through insert.
*/
protected subdivide(): void {
this.divided = true;
this.distributeItems();
}
/**
* Distributes the items of this quadtree into its children.
*/
protected distributeItems(): void {
this.items.forEach(item => this.deferInsert(item));
// Delete the items from this array
this.items.forEach((item, index) => delete this.items[index]);
this.items.length = 0;
}
/**
* Defers this insertion to the children of this quadtree
* @param item The item to insert
*/
protected deferInsert(item: T): void {
this.nw.insert(item);
this.ne.insert(item);
this.sw.insert(item);
this.se.insert(item);
}
public render_demo(ctx: CanvasRenderingContext2D, origin: Vec2, zoom: number): void {
ctx.strokeStyle = "#0000FF";
ctx.strokeRect((this.boundary.x - this.boundary.hw - origin.x)*zoom, (this.boundary.y - this.boundary.hh - origin.y)*zoom, 2*this.boundary.hw*zoom, 2*this.boundary.hh*zoom);
if(this.divided){
this.nw.render_demo(ctx, origin, zoom);
this.ne.render_demo(ctx, origin, zoom);
this.sw.render_demo(ctx, origin, zoom);
this.se.render_demo(ctx, origin, zoom);
}
}
// @implemented
forEach(func: Function): void {
// If divided, send the call down
if(this.divided){
this.nw.forEach(func);
this.ne.forEach(func);
this.sw.forEach(func);
this.se.forEach(func);
} else {
// Otherwise, iterate over items
for(let i = 0; i < this.items.length; i++){
func(this.items[i]);
}
}
}
// @implemented
clear(): void {
if(this.nw){
this.nw.clear();
this.ne.clear();
this.sw.clear();
this.se.clear();
}
for(let item in this.items){
delete this.items[item];
}
this.items.length = 0;
this.divided = false;
}
}

View File

@ -0,0 +1,5 @@
export default class WebGLGameTexture {
webGLTextureId: number;
webGLTexture: WebGLTexture;
imageKey: string;
}

View File

@ -0,0 +1,29 @@
/** A container for info about a webGL shader program */
export default class WebGLProgramType {
/** A webGL program */
program: WebGLProgram;
/** A vertex shader */
vertexShader: WebGLShader;
/** A fragment shader */
fragmentShader: WebGLShader;
/**
* Deletes this shader program
*/
delete(gl: WebGLRenderingContext): void {
// Clean up all aspects of this program
if(this.program){
gl.deleteProgram(this.program);
}
if(this.vertexShader){
gl.deleteShader(this.vertexShader);
}
if(this.fragmentShader){
gl.deleteShader(this.fragmentShader);
}
}
}

View File

@ -0,0 +1,339 @@
import Shape from "./Shape";
import Vec2 from "../Vec2";
import MathUtils from "../../Utils/MathUtils";
import Circle from "./Circle";
import Hit from "../Physics/Hit";
/**
* An Axis-Aligned Bounding Box. In other words, a rectangle that is always aligned to the x-y grid.
* Inspired by the helpful collision documentation @link(here)(https://noonat.github.io/intersect/).
*/
export default class AABB extends Shape {
center: Vec2;
halfSize: Vec2;
/**
* Creates a new AABB
* @param center The center of the AABB
* @param halfSize The half size of the AABB - The distance from the center to an edge in x and y
*/
constructor(center?: Vec2, halfSize?: Vec2){
super();
this.center = center ? center : new Vec2(0, 0);
this.halfSize = halfSize ? halfSize : new Vec2(0, 0);
}
/** Returns a point representing the top left corner of the AABB */
get topLeft(): Vec2 {
return new Vec2(this.left, this.top)
}
/** Returns a point representing the top right corner of the AABB */
get topRight(): Vec2 {
return new Vec2(this.right, this.top)
}
/** Returns a point representing the bottom left corner of the AABB */
get bottomLeft(): Vec2 {
return new Vec2(this.left, this.bottom)
}
/** Returns a point representing the bottom right corner of the AABB */
get bottomRight(): Vec2 {
return new Vec2(this.right, this.bottom)
}
// @override
getBoundingRect(): AABB {
return this.clone();
}
// @override
getBoundingCircle(): Circle {
let r = Math.max(this.hw, this.hh)
return new Circle(this.center.clone(), r);
}
// @deprecated
getHalfSize(): Vec2 {
return this.halfSize;
}
// @deprecated
setHalfSize(halfSize: Vec2): void {
this.halfSize = halfSize;
}
// TODO - move these all to the Shape class
/**
* A simple boolean check of whether this AABB contains a point
* @param point The point to check
* @returns A boolean representing whether this AABB contains the specified point
*/
containsPoint(point: Vec2): boolean {
return point.x >= this.x - this.hw && point.x <= this.x + this.hw
&& point.y >= this.y - this.hh && point.y <= this.y + this.hh
}
/**
* A simple boolean check of whether this AABB contains a point
* @param point The point to check
* @returns A boolean representing whether this AABB contains the specified point
*/
intersectPoint(point: Vec2): boolean {
let dx = point.x - this.x;
let px = this.hw - Math.abs(dx);
if(px <= 0){
return false;
}
let dy = point.y - this.y;
let py = this.hh - Math.abs(dy);
if(py <= 0){
return false;
}
return true;
}
/**
* A boolean check of whether this AABB contains a point with soft left and top boundaries.
* In other words, if the top left is (0, 0), the point (0, 0) is not in the AABB
* @param point The point to check
* @returns A boolean representing whether this AABB contains the specified point
*/
containsPointSoft(point: Vec2): boolean {
return point.x > this.x - this.hw && point.x <= this.x + this.hw
&& point.y > this.y - this.hh && point.y <= this.y + this.hh
}
/**
* Returns the data from the intersection of this AABB with a line segment from a point in a direction
* @param point The point that the line segment starts from
* @param delta The direction and distance of the segment
* @param padding Pads the AABB to make it wider for the intersection test
* @returns The Hit object representing the intersection, or null if there was no intersection
*/
intersectSegment(point: Vec2, delta: Vec2, padding?: Vec2): Hit {
let paddingX = padding ? padding.x : 0;
let paddingY = padding ? padding.y : 0;
let scaleX = 1/delta.x;
let scaleY = 1/delta.y;
let signX = MathUtils.sign(scaleX);
let signY = MathUtils.sign(scaleY);
let tnearx = scaleX*(this.x - signX*(this.hw + paddingX) - point.x);
let tneary = scaleY*(this.y - signY*(this.hh + paddingY) - point.y);
let tfarx = scaleX*(this.x + signX*(this.hw + paddingX) - point.x);
let tfary = scaleY*(this.y + signY*(this.hh + paddingY) - point.y);
if(tnearx > tfary || tneary > tfarx){
// We aren't colliding - we clear one axis before intersecting another
return null;
}
let tnear = Math.max(tnearx, tneary);
// Double check for NaNs
if(tnearx !== tnearx){
tnear = tneary;
} else if (tneary !== tneary){
tnear = tnearx;
}
let tfar = Math.min(tfarx, tfary);
if(tnear === -Infinity){
return null;
}
if(tnear >= 1 || tfar <= 0){
return null;
}
// We are colliding
let hit = new Hit();
hit.time = MathUtils.clamp01(tnear);
hit.nearTimes.x = tnearx;
hit.nearTimes.y = tneary;
if(tnearx > tneary){
// We hit on the left or right size
hit.normal.x = -signX;
hit.normal.y = 0;
} else if(Math.abs(tnearx - tneary) < 0.0001){
// We hit on the corner
hit.normal.x = -signX;
hit.normal.y = -signY;
hit.normal.normalize();
} else {
// We hit on the top or bottom
hit.normal.x = 0;
hit.normal.y = -signY;
}
hit.delta.x = (1.0 - hit.time) * -delta.x;
hit.delta.y = (1.0 - hit.time) * -delta.y;
hit.pos.x = point.x + delta.x * hit.time;
hit.pos.y = point.y + delta.y * hit.time;
return hit;
}
// @override
overlaps(other: Shape): boolean {
if(other instanceof AABB){
return this.overlapsAABB(other);
}
throw "Overlap not defined between these shapes."
}
/**
* A simple boolean check of whether this AABB overlaps another
* @param other The other AABB to check against
* @returns True if this AABB overlaps the other, false otherwise
*/
protected overlapsAABB(other: AABB): boolean {
let dx = other.x - this.x;
let px = this.hw + other.hw - Math.abs(dx);
if(px <= 0){
return false;
}
let dy = other.y - this.y;
let py = this.hh + other.hh - Math.abs(dy);
if(py <= 0){
return false;
}
return true;
}
/**
* Determines whether these AABBs are JUST touching - not overlapping.
* Vec2.x is -1 if the other is to the left, 1 if to the right.
* Likewise, Vec2.y is -1 if the other is on top, 1 if on bottom.
* @param other The other AABB to check
* @returns The collision sides stored in a Vec2 if the AABBs are touching, null otherwise
*/
touchesAABB(other: AABB): Vec2 {
let dx = other.x - this.x;
let px = this.hw + other.hw - Math.abs(dx);
let dy = other.y - this.y;
let py = this.hh + other.hh - Math.abs(dy);
// If one axis is just touching and the other is overlapping, true
if((px === 0 && py >= 0) || (py === 0 && px >= 0)){
let ret = new Vec2();
if(px === 0){
ret.x = other.x < this.x ? -1 : 1;
}
if(py === 0){
ret.y = other.y < this.y ? -1 : 1;
}
return ret;
} else {
return null;
}
}
/**
* Determines whether these AABBs are JUST touching - not overlapping.
* Also, if they are only touching corners, they are considered not touching.
* Vec2.x is -1 if the other is to the left, 1 if to the right.
* Likewise, Vec2.y is -1 if the other is on top, 1 if on bottom.
* @param other The other AABB to check
* @returns The side of the touch, stored as a Vec2, or null if there is no touch
*/
touchesAABBWithoutCorners(other: AABB): Vec2 {
let dx = other.x - this.x;
let px = this.hw + other.hw - Math.abs(dx);
let dy = other.y - this.y;
let py = this.hh + other.hh - Math.abs(dy);
// If one axis is touching, and the other is strictly overlapping
if((px === 0 && py > 0) || (py === 0 && px > 0)){
let ret = new Vec2();
if(px === 0){
ret.x = other.x < this.x ? -1 : 1;
} else {
ret.y = other.y < this.y ? -1 : 1;
}
return ret;
} else {
return null;
}
}
/**
* Calculates the area of the overlap between this AABB and another
* @param other The other AABB
* @returns The area of the overlap between the AABBs
*/
overlapArea(other: AABB): number {
let leftx = Math.max(this.x - this.hw, other.x - other.hw);
let rightx = Math.min(this.x + this.hw, other.x + other.hw);
let dx = rightx - leftx;
let lefty = Math.max(this.y - this.hh, other.y - other.hh);
let righty = Math.min(this.y + this.hh, other.y + other.hh);
let dy = righty - lefty;
if(dx < 0 || dy < 0) return 0;
return dx*dy;
}
/**
* Moves and resizes this rect from its current position to the position specified
* @param velocity The movement of the rect from its position
* @param fromPosition A position specified to be the starting point of sweeping
* @param halfSize The halfSize of the sweeping rect
*/
sweep(velocity: Vec2, fromPosition?: Vec2, halfSize?: Vec2): void {
if(!fromPosition){
fromPosition = this.center;
}
if(!halfSize){
halfSize = this.halfSize;
}
let centerX = fromPosition.x + velocity.x/2;
let centerY = fromPosition.y + velocity.y/2;
let minX = Math.min(fromPosition.x - halfSize.x, fromPosition.x + velocity.x - halfSize.x);
let minY = Math.min(fromPosition.y - halfSize.y, fromPosition.y + velocity.y - halfSize.y);
this.center.set(centerX, centerY);
this.halfSize.set(centerX - minX, centerY - minY);
}
// @override
clone(): AABB {
return new AABB(this.center.clone(), this.halfSize.clone());
}
/**
* Converts this AABB to a string format
* @returns (center: (x, y), halfSize: (x, y))
*/
toString(): string {
return "(center: " + this.center.toString() + ", half-size: " + this.halfSize.toString() + ")"
}
}

View File

@ -0,0 +1,76 @@
import Vec2 from "../Vec2";
import AABB from "./AABB";
import Shape from "./Shape";
/**
* A Circle
*/
export default class Circle extends Shape {
private _center: Vec2;
radius: number;
/**
* Creates a new Circle
* @param center The center of the circle
* @param radius The radius of the circle
*/
constructor(center: Vec2, radius: number) {
super();
this._center = center ? center : new Vec2(0, 0);
this.radius = radius ? radius : 0;
}
get center(): Vec2 {
return this._center;
}
set center(center: Vec2) {
this._center = center;
}
get halfSize(): Vec2 {
return new Vec2(this.radius, this.radius);
}
get r(): number {
return this.radius;
}
set r(radius: number) {
this.radius = radius;
}
// @override
/**
* A simple boolean check of whether this AABB contains a point
* @param point The point to check
* @returns A boolean representing whether this AABB contains the specified point
*/
containsPoint(point: Vec2): boolean {
return this.center.distanceSqTo(point) <= this.radius*this.radius;
}
// @override
getBoundingRect(): AABB {
return new AABB(this._center.clone(), new Vec2(this.radius, this.radius));
}
// @override
getBoundingCircle(): Circle {
return this.clone();
}
// @override
overlaps(other: Shape): boolean {
throw new Error("Method not implemented.");
}
// @override
clone(): Circle {
return new Circle(this._center.clone(), this.radius);
}
toString(): string {
return "(center: " + this.center.toString() + ", radius: " + this.radius + ")";
}
}

View File

@ -0,0 +1,169 @@
import Vec2 from "../Vec2";
import AABB from "./AABB";
import Circle from "./Circle";
/**
* An abstract Shape class that acts as an interface for better interactions with subclasses.
*/
export default abstract class Shape {
abstract get center(): Vec2;
abstract set center(center: Vec2);
abstract get halfSize(): Vec2;
get x(): number {
return this.center.x;
}
get y(): number {
return this.center.y;
}
get hw(): number {
return this.halfSize.x;
}
get hh(): number {
return this.halfSize.y;
}
get top(): number {
return this.y - this.hh;
}
get bottom(): number {
return this.y + this.hh;
}
get left(): number {
return this.x - this.hw;
}
get right(): number {
return this.x + this.hw;
}
/**
* Gets a bounding rectangle for this shape. Warning - may be the same as this Shape.
* For instance, the bounding circle of an AABB is itself. Use clone() if you need a new shape.
* @returns An AABB that bounds this shape
*/
abstract getBoundingRect(): AABB;
/**
* Gets a bounding circle for this shape. Warning - may be the same as this Shape.
* For instance, the bounding circle of a Circle is itself. Use clone() if you need a new shape.
* @returns A Circle that bounds this shape
*/
abstract getBoundingCircle(): Circle;
/**
* Returns a copy of this Shape
* @returns A new copy of this shape
*/
abstract clone(): Shape;
/**
* Checks if this shape overlaps another
* @param other The other shape to check against
* @returns a boolean that represents whether this Shape overlaps the other one
*/
abstract overlaps(other: Shape): boolean;
/**
* A simple boolean check of whether this Shape contains a point
* @param point The point to check
* @returns A boolean representing whether this Shape contains the specified point
*/
abstract containsPoint(point: Vec2): boolean;
static getTimeOfCollision(A: Shape, velA: Vec2, B: Shape, velB: Vec2): [Vec2, Vec2, boolean, boolean] {
if(A instanceof AABB && B instanceof AABB){
return Shape.getTimeOfCollision_AABB_AABB(A, velA, B, velB);
}
}
private static getTimeOfCollision_AABB_AABB(A: AABB, velA: Vec2, B: Shape, velB: Vec2): [Vec2, Vec2, boolean, boolean] {
let posSmaller = A.center;
let posLarger = B.center;
let sizeSmaller = A.halfSize;
let sizeLarger = B.halfSize;
let firstContact = new Vec2(0, 0);
let lastContact = new Vec2(0, 0);
let collidingX = false;
let collidingY = false;
// Sort by position
if(posLarger.x < posSmaller.x){
// Swap, because smaller is further right than larger
let temp: Vec2;
temp = sizeSmaller;
sizeSmaller = sizeLarger;
sizeLarger = temp;
temp = posSmaller;
posSmaller = posLarger;
posLarger = temp;
temp = velA;
velA = velB;
velB = temp;
}
// A is left, B is right
firstContact.x = Infinity;
lastContact.x = Infinity;
if (posLarger.x - sizeLarger.x >= posSmaller.x + sizeSmaller.x){
// If we aren't currently colliding
let relVel = velA.x - velB.x;
if(relVel > 0){
// If they are moving towards each other
firstContact.x = ((posLarger.x - sizeLarger.x) - (posSmaller.x + sizeSmaller.x))/(relVel);
lastContact.x = ((posLarger.x + sizeLarger.x) - (posSmaller.x - sizeSmaller.x))/(relVel);
}
} else {
collidingX = true;
}
if(posLarger.y < posSmaller.y){
// Swap, because smaller is further up than larger
let temp: Vec2;
temp = sizeSmaller;
sizeSmaller = sizeLarger;
sizeLarger = temp;
temp = posSmaller;
posSmaller = posLarger;
posLarger = temp;
temp = velA;
velA = velB;
velB = temp;
}
// A is top, B is bottom
firstContact.y = Infinity;
lastContact.y = Infinity;
if (posLarger.y - sizeLarger.y >= posSmaller.y + sizeSmaller.y){
// If we aren't currently colliding
let relVel = velA.y - velB.y;
if(relVel > 0){
// If they are moving towards each other
firstContact.y = ((posLarger.y - sizeLarger.y) - (posSmaller.y + sizeSmaller.y))/(relVel);
lastContact.y = ((posLarger.y + sizeLarger.y) - (posSmaller.y - sizeSmaller.y))/(relVel);
}
} else {
collidingY = true;
}
return [firstContact, lastContact, collidingX, collidingY];
}
}

View File

@ -0,0 +1,22 @@
import { AnimationData } from "../Rendering/Animations/AnimationTypes";
/** A class representing data contained in a spritesheet.
* Spritesheets are the images associated with sprites, and contain images indexed in a grid, which
* correspond to animations.
*/
export default class Spritesheet {
/** The name of the spritesheet */
name: string;
/** The image key of the spritesheet */
spriteSheetImage: string;
/** The width of the sprite */
spriteWidth: number;
/** The height of the sprite */
spriteHeight: number;
/** The number of columns in the spritesheet */
columns: number;
/** The number of rows in the spritesheet */
rows: number;
/** An array of the animations associated with this spritesheet */
animations: Array<AnimationData>;
}

View File

@ -0,0 +1,108 @@
import Collection from "./Collection";
/**
* A LIFO stack with items of type T
*/
export default class Stack<T> implements Collection {
/** The maximum number of elements in the Stack */
private MAX_ELEMENTS: number;
/** The internal representation of the stack */
private stack: Array<T>;
/** The head of the stack */
private head: number;
/**
* Constructs a new stack
* @param maxElements The maximum size of the stack
*/
constructor(maxElements: number = 100){
this.MAX_ELEMENTS = maxElements;
this.stack = new Array<T>(this.MAX_ELEMENTS);
this.head = -1;
}
/**
* Adds an item to the top of the stack
* @param item The new item to add to the stack
*/
push(item: T): void {
if(this.head + 1 === this.MAX_ELEMENTS){
throw "Stack full - cannot add element";
}
this.head += 1;
this.stack[this.head] = item;
}
/**
* Removes an item from the top of the stack
* @returns The item at the top of the stack
*/
pop(): T {
if(this.head === -1){
throw "Stack empty - cannot remove element";
}
this.head -= 1;
return this.stack[this.head + 1];
}
/**
* Returns the element currently at the top of the stack
* @returns The item at the top of the stack
*/
peek(): T {
if(this.head === -1){
throw "Stack empty - cannot get element";
}
return this.stack[this.head];
}
/** Returns true if this stack is empty
* @returns A boolean that represents whether or not the stack is empty
*/
isEmpty(): boolean {
return this.head === -1;
}
// @implemented
clear(): void {
this.forEach((item, index) => delete this.stack[index]);
this.head = -1;
}
/**
* Returns the number of items currently in the stack
* @returns The number of items in the stack
*/
size(): number {
return this.head + 1;
}
// @implemented
forEach(func: (item: T, index?: number) => void): void{
let i = 0;
while(i <= this.head){
func(this.stack[i], i);
i += 1;
}
}
/**
* Converts this stack into a string format
* @returns A string representing this stack
*/
toString(): string {
let retval = "";
this.forEach( (item, index) => {
let str = item.toString()
if(index !== 0){
str += " -> "
}
retval = str + retval;
});
return "Top -> " + retval;
}
}

View File

@ -0,0 +1,54 @@
import Emitter from "../../Events/Emitter";
import GameEvent from "../../Events/GameEvent";
import Updateable from "../Interfaces/Updateable";
import StateMachine from "./StateMachine";
/**
* An abstract implementation of a state for a @reference[StateMachine].
* This class should be extended to allow for custom state behaviors.
*/
export default abstract class State implements Updateable {
/** The StateMachine that uses this State */
protected parent: StateMachine;
/** An event emitter */
protected emitter: Emitter;
/**
* Constructs a new State
* @param parent The parent StateMachine of this state
*/
constructor(parent: StateMachine) {
this.parent = parent;
this.emitter = new Emitter();
}
/**
* A method that is called when this state is entered. Use this to initialize any variables before updates occur.
* @param options Information to pass to this state
*/
abstract onEnter(options: Record<string, any>): void;
/**
* A lifecycle method that handles an input event, such as taking damage.
* @param event The GameEvent to process
*/
abstract handleInput(event: GameEvent): void;
// @implemented
abstract update(deltaT: number): void;
/**
* Tells the state machine that this state has ended, and makes it transition to the new state specified
* @param stateName The name of the state to transition to
*/
protected finished(stateName: string): void {
this.parent.changeState(stateName);
}
/**
* A lifecycle method is called when the state is ending.
* @returns info to pass to the next state
*/
abstract onExit(): Record<string, any>;
}

View File

@ -0,0 +1,137 @@
import Stack from "../Stack";
import State from "./State";
import Map from "../Map";
import GameEvent from "../../Events/GameEvent";
import Receiver from "../../Events/Receiver";
import Emitter from "../../Events/Emitter";
import Updateable from "../Interfaces/Updateable";
/**
* An implementation of a Push Down Automata State machine. States can also be hierarchical
* for more flexibility, as described in @link(Game Programming Patterns)(https://gameprogrammingpatterns.com/state.html).
*/
export default class StateMachine implements Updateable {
/** A stack of the current states */
protected stack: Stack<State>;
/** A mape of state keys to actual state instances */
protected stateMap: Map<State>;
/** The current state */
protected currentState: State;
/** An event receiver */
protected receiver: Receiver;
/** An event emitter */
protected emitter: Emitter;
/** A boolean representing whether or not this StateMachine is currently active */
protected active: boolean;
/** A boolean representing whether or not this StateMachine should emit an event on state change */
protected emitEventOnStateChange: boolean;
/** The name of the event to be emitted on state change */
protected stateChangeEventName: string;
/**
* Creates a new StateMachine
*/
constructor(){
this.stack = new Stack();
this.stateMap = new Map();
this.receiver = new Receiver();
this.emitter = new Emitter();
this.emitEventOnStateChange = false;
}
/**
* Sets the activity state of this state machine
* @param flag True if you want to set this machine running, false otherwise
*/
setActive(flag: boolean): void {
this.active = flag;
}
/**
* Makes this state machine emit an event any time its state changes
* @param stateChangeEventName The name of the event to emit
*/
setEmitEventOnStateChange(stateChangeEventName: string): void {
this.emitEventOnStateChange = true;
this.stateChangeEventName = stateChangeEventName;
}
/**
* Stops this state machine from emitting events on state change.
*/
cancelEmitEventOnStateChange(): void {
this.emitEventOnStateChange = false;
}
/**
* Initializes this state machine with an initial state and sets it running
* @param initialState The name of initial state of the state machine
*/
initialize(initialState: string, options: Record<string, any> = {}): void {
this.stack.push(this.stateMap.get(initialState));
this.currentState = this.stack.peek();
this.currentState.onEnter(options);
this.setActive(true);
}
/**
* Adds a state to this state machine
* @param stateName The name of the state to add
* @param state The state to add
*/
addState(stateName: string, state: State): void {
this.stateMap.add(stateName, state);
}
/**
* Changes the state of this state machine to the provided string
* @param state The string name of the state to change to
*/
changeState(state: string): void {
// Exit the current state
let options = this.currentState.onExit();
// Make sure the correct state is at the top of the stack
if(state === "previous"){
// Pop the current state off the stack
this.stack.pop();
} else {
// Retrieve the new state from the statemap and put it at the top of the stack
this.stack.pop();
this.stack.push(this.stateMap.get(state));
}
// Retreive the new state from the stack
this.currentState = this.stack.peek();
// Emit an event if turned on
if(this.emitEventOnStateChange){
this.emitter.fireEvent(this.stateChangeEventName, {state: this.currentState});
}
// Enter the new state
this.currentState.onEnter(options);
}
/**
* Handles input. This happens at the very beginning of this state machine's update cycle.
* @param event The game event to process
*/
handleEvent(event: GameEvent): void {
if(this.active){
this.currentState.handleInput(event);
}
}
// @implemented
update(deltaT: number): void {
// Distribute events
while(this.receiver.hasNextEvent()){
let event = this.receiver.getNextEvent();
this.handleEvent(event);
}
// Delegate the update to the current state
this.currentState.update(deltaT);
}
}

View File

@ -0,0 +1,78 @@
// @ignorePage
/**
* a representation of Tiled's tilemap data
*/
export class TiledTilemapData {
height: number;
width: number;
tileheight: number;
tilewidth: number;
orientation: string;
layers: Array<TiledLayerData>;
tilesets: Array<TiledTilesetData>;
}
/**
* A representation of a custom layer property in a Tiled tilemap
*/
export class TiledLayerProperty {
name: string;
type: string;
value: any;
}
/**
* A representation of a tileset in a Tiled tilemap
*/
export class TiledTilesetData {
columns: number;
tilewidth: number;
tileheight: number;
tilecount: number;
firstgid: number;
imageheight: number;
imagewidth: number;
margin: number;
spacing: number;
name: string;
image: string;
tiles: Array<TiledCollectionTile>
}
/**
* A representation of a layer in a Tiled tilemap
*/
export class TiledLayerData {
data: number[];
x: number;
y: number;
width: number;
height: number;
name: string;
opacity: number;
visible: boolean;
properties: TiledLayerProperty[];
type: string;
objects: Array<TiledObject>;
}
export class TiledObject {
gid: number;
height: number;
width: number;
id: number;
name: string;;
properties: Array<TiledLayerProperty>;
rotation: number;
type: string;
visible: boolean;
x: number;
y: number;
}
export class TiledCollectionTile {
id: number;
image: string;
imageheight: number;
imagewidth: number;
}

View File

@ -0,0 +1,146 @@
import ResourceManager from "../../ResourceManager/ResourceManager";
import Vec2 from "../Vec2";
import { TiledTilesetData } from "./TiledData";
/**
* The data representation of a Tileset for the game engine. This represents one image,
* with a startIndex if required (as it is with Tiled using two images in one tilset).
*/
export default class Tileset {
/** The key of the image used by this tileset */
protected imageKey: string;
/** The size of the tileset image */
protected imageSize: Vec2;
/** The index of 0th image of this tileset */
protected startIndex: number;
/** The index of the last image of this tilset */
protected endIndex: number;
/** The size of the tiles in this tileset */
protected tileSize: Vec2;
/** The number of rows in this tileset */
protected numRows: number;
/** The number of columns in this tileset */
protected numCols: number;
// TODO: Change this to be more general and work with other tileset formats
constructor(tilesetData: TiledTilesetData){
// Defer handling of the data to a helper class
this.initFromTiledData(tilesetData);
}
/**
* Initialize the tileset from the data from a Tiled json file
* @param tiledData The parsed object from a Tiled json file
*/
initFromTiledData(tiledData: TiledTilesetData): void {
this.numRows = tiledData.tilecount/tiledData.columns;
this.numCols = tiledData.columns;
this.startIndex = tiledData.firstgid;
this.endIndex = this.startIndex + tiledData.tilecount - 1;
this.tileSize = new Vec2(tiledData.tilewidth, tiledData.tilewidth);
this.imageKey = tiledData.image;
this.imageSize = new Vec2(tiledData.imagewidth, tiledData.imageheight);
}
/**
* Gets the image key associated with this tilemap
* @returns The image key of this tilemap
*/
getImageKey(): string {
return this.imageKey;
}
/**
* Returns a Vec2 containing the left and top offset from the image origin for this tile.
* @param tileIndex The index of the tile from startIndex to endIndex of this tileset
* @returns A Vec2 containing the offset for the specified tile.
*/
getImageOffsetForTile(tileIndex: number): Vec2 {
// Get the true index
let index = tileIndex - this.startIndex;
let row = Math.floor(index / this.numCols);
let col = index % this.numCols;
let width = this.tileSize.x;
let height = this.tileSize.y;
// Calculate the position to start a crop in the tileset image
let left = col * width;
let top = row * height;
return new Vec2(left, top);
}
/**
* Gets the start index
* @returns The start index
*/
getStartIndex(): number {
return this.startIndex;
}
/**
* Gets the tile set
* @returns A Vec2 containing the tile size
*/
getTileSize(): Vec2 {
return this.tileSize;
}
/**
* Gets the number of rows in the tileset
* @returns The number of rows
*/
getNumRows(): number {
return this.numRows;
}
/**
* Gets the number of columns in the tilset
* @returns The number of columns
*/
getNumCols(): number {
return this.numCols;
}
getTileCount(): number {
return this.endIndex - this.startIndex + 1;
}
/**
* Checks whether or not this tilset contains the specified tile index. This is used for rendering.
* @param tileIndex The index of the tile to check
* @returns A boolean representing whether or not this tilset uses the specified index
*/
hasTile(tileIndex: number): boolean {
return tileIndex >= this.startIndex && tileIndex <= this.endIndex;
}
/**
* Render a singular tile with index tileIndex from the tileset located at position dataIndex
* @param ctx The rendering context
* @param tileIndex The value of the tile to render
* @param dataIndex The index of the tile in the data array
* @param worldSize The size of the world
* @param origin The viewport origin in the current layer
* @param scale The scale of the tilemap
*/
renderTile(ctx: CanvasRenderingContext2D, tileIndex: number, dataIndex: number, maxCols: number, origin: Vec2, scale: Vec2, zoom: number): void {
let image = ResourceManager.getInstance().getImage(this.imageKey);
// Get the true index
let index = tileIndex - this.startIndex;
let row = Math.floor(index / this.numCols);
let col = index % this.numCols;
let width = this.tileSize.x;
let height = this.tileSize.y;
// Calculate the position to start a crop in the tileset image
let left = col * width;
let top = row * height;
// Calculate the position in the world to render the tile
let x = Math.floor((dataIndex % maxCols) * width * scale.x);
let y = Math.floor(Math.floor(dataIndex / maxCols) * height * scale.y);
ctx.drawImage(image, left, top, width, height, Math.floor((x - origin.x)*zoom), Math.floor((y - origin.y)*zoom), Math.ceil(width * scale.x * zoom), Math.ceil(height * scale.y * zoom));
}
}

View File

@ -0,0 +1,432 @@
import MathUtils from "../Utils/MathUtils";
/**
* A two-dimensional vector (x, y)
*/
export default class Vec2 {
// Store x and y in an array
/** The array that stores the actual vector values x and y */
private vec: Float32Array;
/**
* When this vector changes its value, do something
*/
private onChange: Function = () => {};
/**
* Creates a new Vec2
* @param x The x value of the vector
* @param y The y value of the vector
*/
constructor(x: number = 0, y: number = 0) {
this.vec = new Float32Array(2);
this.vec[0] = x;
this.vec[1] = y;
}
// Expose x and y with getters and setters
get x() {
return this.vec[0];
}
set x(x: number) {
this.vec[0] = x;
if(this.onChange){
this.onChange();
}
}
get y() {
return this.vec[1];
}
set y(y: number) {
this.vec[1] = y;
if(this.onChange){
this.onChange();
}
}
static get ZERO() {
return new Vec2(0, 0);
}
static readonly ZERO_STATIC = new Vec2(0, 0);
static get INF() {
return new Vec2(Infinity, Infinity);
}
static get UP() {
return new Vec2(0, -1);
}
static get DOWN() {
return new Vec2(0, 1);
}
static get LEFT() {
return new Vec2(-1, 0);
}
static get RIGHT() {
return new Vec2(1, 0);
}
/**
* The squared magnitude of the vector. This tends to be faster, so use it in situations where taking the
* square root doesn't matter, like for comparing distances.
* @returns The squared magnitude of the vector
*/
magSq(): number {
return this.x*this.x + this.y*this.y;
}
/**
* The magnitude of the vector.
* @returns The magnitude of the vector.
*/
mag(): number {
return Math.sqrt(this.magSq());
}
/**
* Divdes x and y by the magnitude to obtain the unit vector in the direction of this vector.
* @returns This vector as a unit vector.
*/
normalize(): Vec2 {
if(this.x === 0 && this.y === 0) return this;
let mag = this.mag();
this.x /= mag;
this.y /= mag;
return this;
}
/**
* Works like normalize(), but returns a new Vec2
* @returns A new vector that is the unit vector for this one
*/
normalized(): Vec2 {
if(this.isZero()){
return this;
}
let mag = this.mag();
return new Vec2(this.x/mag, this.y/mag);
}
/**
* Sets the x and y elements of this vector to zero.
* @returns This vector, with x and y set to zero.
*/
zero(): Vec2 {
return this.set(0, 0);
}
/**
* Sets the vector's x and y based on the angle provided. Goes counter clockwise.
* @param angle The angle in radians
* @param radius The magnitude of the vector at the specified angle
* @returns This vector.
*/
setToAngle(angle: number, radius: number = 1): Vec2 {
this.x = MathUtils.floorToPlace(Math.cos(angle)*radius, 5);
this.y = MathUtils.floorToPlace(-Math.sin(angle)*radius, 5);
return this;
}
/**
* Returns a vector that point from this vector to another one
* @param other The vector to point to
* @returns A new Vec2 that points from this vector to the one provided
*/
vecTo(other: Vec2): Vec2 {
return new Vec2(other.x - this.x, other.y - this.y);
}
/**
* Returns a vector containing the direction from this vector to another
* @param other The vector to point to
* @returns A new Vec2 that points from this vector to the one provided. This new Vec2 will be a unit vector.
*/
dirTo(other: Vec2): Vec2 {
return this.vecTo(other).normalize();
}
/**
* Keeps the vector's direction, but sets its magnitude to be the provided magnitude
* @param magnitude The magnitude the vector should be
* @returns This vector with its magnitude set to the new magnitude
*/
scaleTo(magnitude: number): Vec2 {
return this.normalize().scale(magnitude);
}
/**
* Scales x and y by the number provided, or if two number are provided, scales them individually.
* @param factor The scaling factor for the vector, or for only the x-component if yFactor is provided
* @param yFactor The scaling factor for the y-component of the vector
* @returns This vector after scaling
*/
scale(factor: number, yFactor: number = null): Vec2 {
if(yFactor !== null){
this.x *= factor;
this.y *= yFactor;
return this;
}
this.x *= factor;
this.y *= factor;
return this;
}
/**
* Returns a scaled version of this vector without modifying it.
* @param factor The scaling factor for the vector, or for only the x-component if yFactor is provided
* @param yFactor The scaling factor for the y-component of the vector
* @returns A new vector that has the values of this vector after scaling
*/
scaled(factor: number, yFactor: number = null): Vec2 {
return this.clone().scale(factor, yFactor);
}
/**
* Rotates the vector counter-clockwise by the angle amount specified
* @param angle The angle to rotate by in radians
* @returns This vector after rotation.
*/
rotateCCW(angle: number): Vec2 {
let cs = Math.cos(angle);
let sn = Math.sin(angle);
let tempX = this.x*cs - this.y*sn;
let tempY = this.x*sn + this.y*cs;
this.x = tempX;
this.y = tempY;
return this;
}
/**
* Sets the vectors coordinates to be the ones provided
* @param x The new x value for this vector
* @param y The new y value for this vector
* @returns This vector
*/
set(x: number, y: number): Vec2 {
this.x = x;
this.y = y;
return this;
}
/**
* Copies the values of the other Vec2 into this one.
* @param other The Vec2 to copy
* @returns This vector with its values set to the vector provided
*/
copy(other: Vec2): Vec2 {
return this.set(other.x, other.y);
}
/**
* Adds this vector the another vector
* @param other The Vec2 to add to this one
* @returns This vector after adding the one provided
*/
add(other: Vec2): Vec2 {
this.x += other.x;
this.y += other.y;
return this;
}
/**
* Increments the fields of this vector. Both are incremented with a, if only a is provided.
* @param a The first number to increment by
* @param b The second number to increment by
* @returnss This vector after incrementing
*/
inc(a: number, b?: number): Vec2 {
if(b === undefined){
this.x += a;
this.y += a;
} else {
this.x += a;
this.y += b;
}
return this;
}
/**
* Subtracts another vector from this vector
* @param other The Vec2 to subtract from this one
* @returns This vector after subtracting the one provided
*/
sub(other: Vec2): Vec2 {
this.x -= other.x;
this.y -= other.y;
return this;
}
/**
* Multiplies this vector with another vector element-wise. In other words, this.x *= other.x and this.y *= other.y
* @param other The Vec2 to multiply this one by
* @returns This vector after multiplying its components by this one
*/
mult(other: Vec2): Vec2 {
this.x *= other.x;
this.y *= other.y;
return this;
}
/**
* Divides this vector with another vector element-wise. In other words, this.x /= other.x and this.y /= other.y
* @param other The vector to divide this one by
* @returns This vector after division
*/
div(other: Vec2): Vec2 {
if(other.x === 0 || other.y === 0) throw "Divide by zero error";
this.x /= other.x;
this.y /= other.y;
return this;
}
/**
* Does an element wise remainder operation on this vector. this.x %= other.x and this.y %= other.y
* @param other The other vector
* @returns this vector
*/
remainder(other: Vec2): Vec2 {
this.x = this.x % other.x;
this.y = this.y % other.y;
return this;
}
/**
* Returns the squared distance between this vector and another vector
* @param other The vector to compute distance squared to
* @returns The squared distance between this vector and the one provided
*/
distanceSqTo(other: Vec2): number {
return (this.x - other.x)*(this.x - other.x) + (this.y - other.y)*(this.y - other.y);
}
/**
* Returns the distance between this vector and another vector
* @param other The vector to compute distance to
* @returns The distance between this vector and the one provided
*/
distanceTo(other: Vec2): number {
return Math.sqrt(this.distanceSqTo(other));
}
/**
* Returns the dot product of this vector and another
* @param other The vector to compute the dot product with
* @returns The dot product of this vector and the one provided.
*/
dot(other: Vec2): number {
return this.x*other.x + this.y*other.y;
}
/**
* Returns the angle counter-clockwise in radians from this vector to another vector
* @param other The vector to compute the angle to
* @returns The angle, rotating CCW, from this vector to the other vector
*/
angleToCCW(other: Vec2): number {
let dot = this.dot(other);
let det = this.x*other.y - this.y*other.x;
let angle = -Math.atan2(det, dot);
if(angle < 0){
angle += 2*Math.PI;
}
return angle;
}
/**
* Returns a string representation of this vector rounded to 1 decimal point
* @returns This vector as a string
*/
toString(): string {
return this.toFixed();
}
/**
* Returns a string representation of this vector rounded to the specified number of decimal points
* @param numDecimalPoints The number of decimal points to create a string to
* @returns This vector as a string
*/
toFixed(numDecimalPoints: number = 1): string {
return "(" + this.x.toFixed(numDecimalPoints) + ", " + this.y.toFixed(numDecimalPoints) + ")";
}
/**
* Returns a new vector with the same coordinates as this one.
* @returns A new Vec2 with the same values as this one
*/
clone(): Vec2 {
return new Vec2(this.x, this.y);
}
/**
* Returns true if this vector and other have the EXACT same x and y (not assured to be safe for floats)
* @param other The vector to check against
* @returns A boolean representing the equality of the two vectors
*/
strictEquals(other: Vec2): boolean {
return this.x === other.x && this.y === other.y;
}
/**
* Returns true if this vector and other have the same x and y
* @param other The vector to check against
* @returns A boolean representing the equality of the two vectors
*/
equals(other: Vec2): boolean {
let xEq = Math.abs(this.x - other.x) < 0.0000001;
let yEq = Math.abs(this.y - other.y) < 0.0000001;
return xEq && yEq;
}
/**
* Returns true if this vector is the zero vector exactly (not assured to be safe for floats).
* @returns A boolean representing the equality of this vector and the zero vector
*/
strictIsZero(): boolean {
return this.x === 0 && this.y === 0;
}
/**
* Returns true if this x and y for this vector are both zero.
* @returns A boolean representing the equality of this vector and the zero vector
*/
isZero(): boolean {
return Math.abs(this.x) < 0.0000001 && Math.abs(this.y) < 0.0000001;
}
/**
* Sets the function that is called whenever this vector is changed.
* @param f The function to be called
*/
setOnChange(f: Function): void {
this.onChange = f;
}
toArray(): Float32Array {
return this.vec;
}
/**
* Performs linear interpolation between two vectors
* @param a The first vector
* @param b The second vector
* @param t The time of the lerp, with 0 being vector A, and 1 being vector B
* @returns A new Vec2 representing the lerp between vector a and b.
*/
static lerp(a: Vec2, b: Vec2, t: number): Vec2 {
return new Vec2(MathUtils.lerp(a.x, b.x, t), MathUtils.lerp(a.y, b.y, t));
}
}

View File

@ -0,0 +1,197 @@
import Map from "../DataTypes/Map";
import Vec2 from "../DataTypes/Vec2";
import GameNode from "../Nodes/GameNode";
import Color from "../Utils/Color";
/**
* A util class for rendering Debug messages to the canvas.
*/
export default class Debug {
/** A map of log messages to display on the screen */
private static logMessages: Map<string> = new Map();
/** An array of game nodes to render debug info for */
private static nodes: Array<GameNode>;
/** The rendering context for any debug messages */
private static debugRenderingContext: CanvasRenderingContext2D;
/** The size of the debug canvas */
private static debugCanvasSize: Vec2;
/** The rendering color for text */
private static defaultTextColor: Color = Color.WHITE;
/**
* Add a message to display on the debug screen
* @param id A unique ID for this message
* @param messages The messages to print to the debug screen
*/
static log(id: string, ...messages: any): void {
// let message = "";
// for(let i = 0; i < messages.length; i++){
// message += messages[i].toString();
// }
// Join all messages with spaces
let message = messages.map((m: any) => m.toString()).join(" ");
this.logMessages.add(id, message);
}
/**
* Deletes a a key from the log and stops it from keeping up space on the screen
* @param id The id of the log item to clear
*/
static clearLogItem(id: string): void {
this.logMessages.delete(id);
}
/**
* Sets the list of nodes to render with the debugger
* @param nodes The new list of nodes
*/
static setNodes(nodes: Array<GameNode>): void {
this.nodes = nodes;
}
/**
* Draws a box at the specified position
* @param center The center of the box
* @param halfSize The dimensions of the box
* @param filled A boolean for whether or not the box is filled
* @param color The color of the box to draw
*/
static drawBox(center: Vec2, halfSize: Vec2, filled: boolean, color: Color): void {
let alpha = this.debugRenderingContext.globalAlpha;
this.debugRenderingContext.globalAlpha = color.a;
if(filled){
this.debugRenderingContext.fillStyle = color.toString();
this.debugRenderingContext.fillRect(center.x - halfSize.x, center.y - halfSize.y, halfSize.x*2, halfSize.y*2);
} else {
let lineWidth = 2;
this.debugRenderingContext.lineWidth = lineWidth;
this.debugRenderingContext.strokeStyle = color.toString();
this.debugRenderingContext.strokeRect(center.x - halfSize.x, center.y - halfSize.y, halfSize.x*2, halfSize.y*2);
}
this.debugRenderingContext.globalAlpha = alpha;
}
/**
* Draws a circle at the specified position
* @param center The center of the circle
* @param radius The dimensions of the box
* @param filled A boolean for whether or not the circle is filled
* @param color The color of the circle
*/
static drawCircle(center: Vec2, radius: number, filled: boolean, color: Color): void {
let alpha = this.debugRenderingContext.globalAlpha;
this.debugRenderingContext.globalAlpha = color.a;
if(filled){
this.debugRenderingContext.fillStyle = color.toString();
this.debugRenderingContext.beginPath();
this.debugRenderingContext.arc(center.x, center.y, radius, 0, 2 * Math.PI);
this.debugRenderingContext.closePath();
this.debugRenderingContext.fill();
} else {
let lineWidth = 2;
this.debugRenderingContext.lineWidth = lineWidth;
this.debugRenderingContext.strokeStyle = color.toString();
this.debugRenderingContext.beginPath();
this.debugRenderingContext.arc(center.x, center.y, radius, 0, 2 * Math.PI);
this.debugRenderingContext.closePath();
this.debugRenderingContext.stroke();
}
this.debugRenderingContext.globalAlpha = alpha;
}
/**
* Draws a ray at the specified position
* @param from The starting position of the ray
* @param to The ending position of the ray
* @param color The color of the ray
*/
static drawRay(from: Vec2, to: Vec2, color: Color): void {
this.debugRenderingContext.lineWidth = 2;
this.debugRenderingContext.strokeStyle = color.toString();
this.debugRenderingContext.beginPath();
this.debugRenderingContext.moveTo(from.x, from.y);
this.debugRenderingContext.lineTo(to.x, to.y);
this.debugRenderingContext.closePath();
this.debugRenderingContext.stroke();
}
/**
* Draws a point at the specified position
* @param pos The position of the point
* @param color The color of the point
*/
static drawPoint(pos: Vec2, color: Color): void {
let pointSize = 6;
this.debugRenderingContext.fillStyle = color.toString();
this.debugRenderingContext.fillRect(pos.x - pointSize/2, pos.y - pointSize/2, pointSize, pointSize);
}
/**
* Sets the default rendering color for text for the debugger
* @param color The color to render the text
*/
static setDefaultTextColor(color: Color): void {
this.defaultTextColor = color;
}
/**
* Performs any necessary setup operations on the Debug canvas
* @param canvas The debug canvas
* @param width The desired width of the canvas
* @param height The desired height of the canvas
* @returns The rendering context extracted from the canvas
*/
static initializeDebugCanvas(canvas: HTMLCanvasElement, width: number, height: number): CanvasRenderingContext2D {
canvas.width = width;
canvas.height = height;
this.debugCanvasSize = new Vec2(width, height);
this.debugRenderingContext = canvas.getContext("2d");
return this.debugRenderingContext;
}
/** Clears the debug canvas */
static clearCanvas(): void {
this.debugRenderingContext.clearRect(0, 0, this.debugCanvasSize.x, this.debugCanvasSize.y);
}
/** Renders the text and nodes sent to the Debug system */
static render(): void {
this.renderText();
this.renderNodes();
}
/** Renders the text sent to the Debug canvas */
static renderText(): void {
let y = 20;
this.debugRenderingContext.font = "20px Arial";
this.debugRenderingContext.fillStyle = this.defaultTextColor.toString();
// Draw all of the text
this.logMessages.forEach((key: string) => {
this.debugRenderingContext.fillText(this.logMessages.get(key), 10, y)
y += 30;
});
}
/** Renders the nodes registered with the debug canvas */
static renderNodes(): void {
if(this.nodes){
this.nodes.forEach(node => {
node.debugRender();
});
}
}
}

View File

@ -0,0 +1,244 @@
import Color from "../Utils/Color";
// @ignorePage
export default class Stats extends Object {
// The fps of the game.
private static prevfps: Array<number>;
private static readonly NUM_POINTS: number = 60;
private static ctx: CanvasRenderingContext2D;
private static CANVAS_WIDTH: number = 300;
private static CANVAS_HEIGHT: number = 300;
private static statsDiv: HTMLDivElement;
private static graphChoices: HTMLSelectElement;
// Quadtree stats
private static prevClearTimes: Array<number>;
private static SGClearTimes: Array<number>;
private static avgSGClearTime: number;
private static prevFillTimes: Array<number>;
private static SGFillTimes: Array<number>;
private static avgSGFillTime: number;
private static prevUpdateTimes: Array<number>;
private static SGUpdateTimes: Array<number>;
private static avgSGUpdateTime: number;
private static prevQueryTimes: Array<number>;
private static SGQueryTimes: Array<number>;
private static avgSGQueryTime: number;
static initStats(): void {
let canvas = <HTMLCanvasElement>document.getElementById("stats-canvas");
canvas.width = this.CANVAS_WIDTH;
canvas.height = this.CANVAS_HEIGHT;
this.ctx = canvas.getContext("2d");
this.statsDiv = <HTMLDivElement>document.getElementById("stats-display");
this.prevfps = new Array();
this.prevClearTimes = new Array();
this.SGClearTimes = new Array();
this.avgSGClearTime = 0;
this.prevFillTimes = new Array();
this.SGFillTimes = new Array();
this.avgSGFillTime = 0;
this.prevUpdateTimes = new Array();
this.SGUpdateTimes = new Array();
this.avgSGUpdateTime = 0;
this.prevQueryTimes = new Array();
this.SGQueryTimes = new Array();
this.avgSGQueryTime = 0;
let clearTime = document.createElement("span");
clearTime.setAttribute("id", "sgclear");
let fillTime = document.createElement("span");
fillTime.setAttribute("id", "sgfill");
let updateTime = document.createElement("span");
updateTime.setAttribute("id", "sgupdate");
let queryTime = document.createElement("span");
queryTime.setAttribute("id", "sgquery");
let br1 = document.createElement("br");
let br2 = document.createElement("br");
let br3 = document.createElement("br");
this.statsDiv.append(clearTime, br1, fillTime, br2, updateTime, br3, queryTime);
this.graphChoices = <HTMLSelectElement>document.getElementById("chart-option");
let option1 = document.createElement("option");
option1.value = "prevfps";
option1.label = "FPS";
let option2 = document.createElement("option");
option2.value = "prevClearTimes";
option2.label = "Clear Time";
let option3 = document.createElement("option");
option3.value = "prevFillTimes";
option3.label = "Fill time";
let option4 = document.createElement("option");
option4.value = "prevUpdateTimes";
option4.label = "Update time";
let option5 = document.createElement("option");
option5.value = "prevQueryTimes";
option5.label = "Query Time";
let optionAll = document.createElement("option");
optionAll.value = "all";
optionAll.label = "All";
this.graphChoices.append(option1, option2, option3, option4, option5, optionAll);
}
static updateFPS(fps: number): void {
this.prevfps.push(fps);
if(this.prevfps.length > Stats.NUM_POINTS){
this.prevfps.shift();
}
if(this.SGClearTimes.length > 0){
this.prevClearTimes.push(this.avgSGClearTime);
if(this.prevClearTimes.length > this.NUM_POINTS){
this.prevClearTimes.shift();
}
}
if(this.SGFillTimes.length > 0){
this.prevFillTimes.push(this.avgSGFillTime);
if(this.prevFillTimes.length > this.NUM_POINTS){
this.prevFillTimes.shift();
}
}
if(this.SGUpdateTimes.length > 0){
this.prevUpdateTimes.push(this.avgSGUpdateTime);
if(this.prevUpdateTimes.length > this.NUM_POINTS){
this.prevUpdateTimes.shift();
}
}
if(this.SGQueryTimes.length > 0){
this.prevQueryTimes.push(this.avgSGQueryTime);
if(this.prevQueryTimes.length > this.NUM_POINTS){
this.prevQueryTimes.shift();
}
}
this.updateSGStats();
}
static log(key: string, data: any): void {
if(key === "sgclear"){
this.SGClearTimes.push(data);
if(this.SGClearTimes.length > 100){
this.SGClearTimes.shift();
}
} else if(key === "sgfill"){
this.SGFillTimes.push(data);
if(this.SGFillTimes.length > 100){
this.SGFillTimes.shift();
}
} else if(key === "sgupdate"){
this.SGUpdateTimes.push(data);
if(this.SGUpdateTimes.length > 100){
this.SGUpdateTimes.shift();
}
} else if(key === "sgquery"){
this.SGQueryTimes.push(data);
if(this.SGQueryTimes.length > 1000){
this.SGQueryTimes.shift();
}
}
}
static render(): void {
// Display stats
this.drawCharts();
}
static drawCharts(){
this.ctx.clearRect(0, 0, this.CANVAS_WIDTH, this.CANVAS_HEIGHT);
let paramString = this.graphChoices.value;
if(paramString === "prevfps" || paramString === "all"){
let param = this.prevfps;
let color = Color.BLUE.toString();
this.drawChart(param, color);
}
if(paramString === "prevClearTimes" || paramString === "all"){
let param = this.prevClearTimes;
let color = Color.RED.toString();
this.drawChart(param, color);
}
if(paramString === "prevFillTimes" || paramString === "all"){
let param = this.prevFillTimes;
let color = Color.GREEN.toString();
this.drawChart(param, color);
}
if(paramString === "prevUpdateTimes" || paramString === "all"){
let param = this.prevUpdateTimes;
let color = Color.CYAN.toString();
this.drawChart(param, color);
}
if(paramString === "prevQueryTimes" || paramString === "all"){
let param = this.prevQueryTimes;
let color = Color.ORANGE.toString();
this.drawChart(param, color);
}
}
static drawChart(param: Array<number>, color: string){
this.ctx.strokeStyle = Color.BLACK.toString();
this.ctx.beginPath();
this.ctx.moveTo(10, 10);
this.ctx.lineTo(10, this.CANVAS_HEIGHT - 10);
this.ctx.closePath();
this.ctx.stroke();
this.ctx.beginPath();
this.ctx.moveTo(10, this.CANVAS_HEIGHT - 10);
this.ctx.lineTo(this.CANVAS_WIDTH - 10, this.CANVAS_HEIGHT - 10);
this.ctx.closePath();
this.ctx.stroke();
let max = Math.max(...param);
let prevX = 10;
let prevY = this.CANVAS_HEIGHT - 10 - param[0]/max*(this.CANVAS_HEIGHT-20);
this.ctx.strokeStyle = color;
for(let i = 1; i < param.length; i++){
let fps = param[i];
let x = 10 + i*(this.CANVAS_WIDTH - 20)/this.NUM_POINTS;
let y = this.CANVAS_HEIGHT - 10 - fps/max*(this.CANVAS_HEIGHT-20)
this.ctx.beginPath();
this.ctx.moveTo(prevX, prevY);
this.ctx.lineTo(x, y);
this.ctx.closePath();
this.ctx.stroke();
prevX = x;
prevY = y;
}
}
static updateSGStats(){
if(this.SGClearTimes.length > 0){
this.avgSGClearTime = this.SGClearTimes.reduce((acc, val) => acc + val)/this.SGClearTimes.length;
}
if(this.SGFillTimes.length > 0){
this.avgSGFillTime = this.SGFillTimes.reduce((acc, val) => acc + val)/this.SGFillTimes.length;
}
if(this.SGUpdateTimes.length > 0){
this.avgSGUpdateTime = this.SGUpdateTimes.reduce((acc, val) => acc + val)/this.SGUpdateTimes.length;
}
if(this.SGQueryTimes.length > 0){
this.avgSGQueryTime = this.SGQueryTimes.reduce((acc, val) => acc + val)/this.SGQueryTimes.length;
}
document.getElementById("sgclear").innerHTML = "Avg SG clear time: " + this.avgSGClearTime;
document.getElementById("sgfill").innerHTML = "Avg SG fill time: " + this.avgSGFillTime;
document.getElementById("sgupdate").innerHTML = "Avg SG update time: " + this.avgSGUpdateTime;
document.getElementById("sgquery").innerHTML = "Avg SG query time: " + this.avgSGQueryTime;
}
}

View File

@ -0,0 +1,26 @@
import Map from "../DataTypes/Map";
import EventQueue from "./EventQueue";
import GameEvent from "./GameEvent";
/**
* An event emitter object other systems can use to hook into the EventQueue.
* Provides an easy interface for firing off events.
*/
export default class Emitter {
/** A reference to the EventQueue */
private eventQueue: EventQueue;
/** Creates a new Emitter */
constructor(){
this.eventQueue = EventQueue.getInstance();
}
/**
* Emit and event of type eventType with the data packet data
* @param eventType The name of the event to fire off
* @param data A @reference[Map] or record containing any data about the event
*/
fireEvent(eventType: string, data: Map<any> | Record<string, any> = null): void {
this.eventQueue.addEvent(new GameEvent(eventType, data));
}
}

View File

@ -0,0 +1,125 @@
import Queue from "../DataTypes/Queue";
import Map from "../DataTypes/Map";
import GameEvent from "./GameEvent";
import Receiver from "./Receiver";
import { GameEventType } from "./GameEventType";
/**
* The main event system of the game engine.
* Events are sent to the EventQueue, which handles distribution to any systems that are listening for those events.
* This allows for handling of input without having classes directly hook into javascript event handles,
* and allows otherwise separate classes to communicate with each other cleanly, such as a Player object
* requesting a sound be played by the audio system.
*
* The distribution of @reference[GameEvent]s happens as follows:
*
* Events are recieved throughout a frame and are queued up by the EventQueue.
* At the beginning of the next frame, events are sent out to any receivers that are hooked into the event type.
* @reference[Receiver]s are then free to process events as they see fit.
*
* Overall, the EventQueue can be considered as something similar to an email server,
* and the @reference[Receiver]s can be considered as the client inboxes.
*
* See @link(Game Programming Patterns)(https://gameprogrammingpatterns.com/event-queue.html) for more discussion on EventQueues
*/
export default class EventQueue {
private static instance: EventQueue = null;
/** The maximum number of events visible */
private readonly MAX_SIZE: number;
/** The actual queue of events */
private q: Queue<GameEvent>;
/** The map of receivers registered for an event name */
private receivers: Map<Array<Receiver>>;
private constructor(){
this.MAX_SIZE = 100;
this.q = new Queue<GameEvent>(this.MAX_SIZE);
this.receivers = new Map<Array<Receiver>>();
}
/** Retrieves the instance of the Singleton EventQueue */
static getInstance(): EventQueue {
if(this.instance === null){
this.instance = new EventQueue();
}
return this.instance;
}
/** Adds an event to the EventQueue.
* This is exposed to the rest of the game engine through the @reference[Emitter] class */
addEvent(event: GameEvent): void {
this.q.enqueue(event);
}
/**
* Associates a receiver with a type of event. Every time this event appears in the future,
* it will be given to the receiver (and any others watching that type).
* This is exposed to the rest of the game engine through the @reference[Receiver] class
* @param receiver The event receiver
* @param type The type or types of events to subscribe to
*/
subscribe(receiver: Receiver, type: string | Array<string>): void {
if(type instanceof Array){
// If it is an array, subscribe to all event types
for(let t of type){
this.addListener(receiver, t);
}
} else {
this.addListener(receiver, type);
}
}
/**
* Unsubscribes the specified receiver from all events, or from whatever events are provided
* @param receiver The receiver to unsubscribe
* @param keys The events to unsubscribe from. If none are provided, unsubscribe from all
*/
unsubscribe(receiver: Receiver, ...events: Array<string>): void {
this.receivers.forEach(eventName => {
// If keys were provided, only continue if this key is one of them
if(events.length > 0 && events.indexOf(eventName) === -1) return;
// Find the index of our receiver for this key
let index = this.receivers.get(eventName).indexOf(receiver);
// If an index was found, remove the receiver
if(index !== -1){
this.receivers.get(eventName).splice(index, 1);
}
});
}
// Associate the receiver and the type
private addListener(receiver: Receiver, type: string): void {
if(this.receivers.has(type)){
this.receivers.get(type).push(receiver);
} else {
this.receivers.add(type, [receiver]);
}
}
update(deltaT: number): void {
while(this.q.hasItems()){
// Retrieve each event
let event = this.q.dequeue();
// If a receiver has this event type, send it the event
if(this.receivers.has(event.type)){
for(let receiver of this.receivers.get(event.type)){
receiver.receive(event);
}
}
// If a receiver is subscribed to all events, send it the event
if(this.receivers.has(GameEventType.ALL)){
for(let receiver of this.receivers.get(GameEventType.ALL)){
receiver.receive(event);
}
}
}
}
}

View File

@ -0,0 +1,54 @@
import Map from "../DataTypes/Map"
/**
* A representation of an in-game event that is passed through the @reference[EventQueue]
*/
export default class GameEvent {
/** The type of the event */
public type: string;
/** The data contained by the event */
public data: Map<any>;
/** The time of the event in ms */
public time: number;
/**
* Creates a new GameEvent.
* This is handled implicitly through the @reference[Emitter] class
* @param type The type of the GameEvent
* @param data The data contained by the GameEvent
*/
constructor(type: string, data: Map<any> | Record<string, any> = null) {
// Parse the game event data
if (data === null) {
this.data = new Map<any>();
} else if (!(data instanceof Map)){
// data is a raw object, unpack
this.data = new Map<any>();
for(let key in data){
this.data.add(key, data[key]);
}
} else {
this.data = data;
}
this.type = type;
this.time = Date.now();
}
/**
* Checks the type of the GameEvent
* @param type The type to check
* @returns True if the GameEvent is the specified type, false otherwise.
*/
isType(type: string): boolean {
return this.type === type;
}
/**
* Returns this GameEvent as a string
* @returns The string representation of the GameEvent
*/
toString(): string {
return this.type + ": @" + this.time;
}
}

View File

@ -0,0 +1,91 @@
// @ignorePage
export enum GameEventType {
/**
* Mouse Down event. Has data: {position: Vec2 - Mouse Position}
*/
MOUSE_DOWN = "mouse_down",
/**
* Mouse Up event. Has data: {position: Vec2 - Mouse Position}
*/
MOUSE_UP = "mouse_up",
/**
* Mouse Move event. Has data: {position: Vec2 - Mouse Position}
*/
MOUSE_MOVE = "mouse_move",
/**
* Key Down event. Has data: {key: string - The key that is down}
*/
KEY_DOWN = "key_down",
/**
* Key Up event. Has data: {key: string - The key that is up}
*/
KEY_UP = "key_up",
/**
* Canvas Blur event. Has data: {}
*/
CANVAS_BLUR = "canvas_blur",
/**
* Mouse wheel up event. Has data: {}
*/
WHEEL_UP = "wheel_up",
/**
* Mouse wheel down event. Has data: {}
*/
WHEEL_DOWN = "wheel_down",
/**
* Start Recording event. Has data: {}
*/
START_RECORDING = "start_recording",
/**
* Stop Recording event. Has data: {}
*/
STOP_RECORDING = "stop_recording",
/**
* Play Recording event. Has data: {}
*/
PLAY_RECORDING = "play_recording",
/**
* Play Sound event. Has data: {key: string, loop: boolean, holdReference: boolean }
*/
PLAY_SOUND = "play_sound",
/**
* Play Sound event. Has data: {key: string}
*/
STOP_SOUND = "stop_sound",
/**
* Play Sound event. Has data: {key: string, loop: boolean, holdReference: boolean, channel: AudioChannelType }
*/
PLAY_SFX = "play_sfx",
/**
* Play Sound event. Has data: {key: string, loop: boolean, holdReference: boolean }
*/
PLAY_MUSIC = "play_music",
/**
* Mute audio channel event. Has data: {channel: AudioChannelType}
*/
MUTE_CHANNEL = "mute_channel",
/**
* Unmute audio channel event. Has data: {channel: AudioChannelType}
*/
UNMUTE_CHANNEL = "unmute_channel",
/**
* Encompasses all event types. Used for receivers only.
*/
ALL = "all",
}

View File

@ -0,0 +1,77 @@
import Queue from "../DataTypes/Queue";
import EventQueue from "./EventQueue";
import GameEvent from "./GameEvent";
/**
* Receives subscribed events from the EventQueue.
*/
export default class Receiver {
/** The maximum number of events this Receiver can hold at one time */
readonly MAX_SIZE: number;
/** The inbox of the Receiver */
private q: Queue<GameEvent>;
/** Creates a new Receiver */
constructor(){
this.MAX_SIZE = 100;
this.q = new Queue(this.MAX_SIZE);
}
destroy(){
EventQueue.getInstance().unsubscribe(this);
}
/**
* Adds these types of events to this receiver's queue every update.
* @param eventTypes The types of events this receiver will be subscribed to
*/
subscribe(eventTypes: string | Array<string>): void {
EventQueue.getInstance().subscribe(this, eventTypes);
this.q.clear();
}
/**
* Adds an event to the queue of this reciever. This is used by the @reference[EventQueue] to distribute events
* @param event The event to receive
*/
receive(event: GameEvent): void {
try{
this.q.enqueue(event);
} catch(e){
console.warn("Receiver overflow for event " + event.toString());
throw e;
}
}
/**
* Retrieves the next event from the receiver's queue
* @returns The next GameEvent
*/
getNextEvent(): GameEvent {
return this.q.dequeue();
}
/**
* Looks at the next event in the receiver's queue, but doesn't remove it from the queue
* @returns The next GameEvent
*/
peekNextEvent(): GameEvent {
return this.q.peekNext()
}
/**
* Returns true if the receiver has any events in its queue
* @returns True if the receiver has another event, false otherwise
*/
hasNextEvent(): boolean {
return this.q.hasItems();
}
/**
* Ignore all events this frame
*/
ignoreEvents(): void {
this.q.clear();
}
}

View File

@ -0,0 +1,322 @@
import Receiver from "../Events/Receiver";
import Map from "../DataTypes/Map";
import Vec2 from "../DataTypes/Vec2";
import EventQueue from "../Events/EventQueue";
import Viewport from "../SceneGraph/Viewport";
import GameEvent from "../Events/GameEvent";
import { GameEventType } from "../Events/GameEventType";
/**
* Receives input events from the @reference[EventQueue] and allows for easy access of information about input by other systems
*/
export default class Input {
private static mousePressed: boolean;
private static mouseJustPressed: boolean;
private static keyJustPressed: Map<boolean>;
private static keyPressed: Map<boolean>;
private static mousePosition: Vec2;
private static mousePressPosition: Vec2;
private static scrollDirection: number;
private static justScrolled: boolean;
private static eventQueue: EventQueue;
private static receiver: Receiver;
private static viewport: Viewport;
private static keyMap: Map<Array<string>>;
private static keysDisabled: boolean;
private static mouseDisabled: boolean;
/**
* Initializes the Input object
* @param viewport A reference to the viewport of the game
*/
static initialize(viewport: Viewport, keyMap: Array<Record<string, any>>){
Input.viewport = viewport;
Input.mousePressed = false;
Input.mouseJustPressed = false;
Input.receiver = new Receiver();
Input.keyJustPressed = new Map<boolean>();
Input.keyPressed = new Map<boolean>();
Input.mousePosition = new Vec2(0, 0);
Input.mousePressPosition = new Vec2(0, 0);
Input.scrollDirection = 0;
Input.justScrolled = false;
Input.keysDisabled = false;
Input.mouseDisabled = false;
// Initialize the keymap
Input.keyMap = new Map();
// Add all keys to the keymap
for(let entry in keyMap){
let name = keyMap[entry].name;
let keys = keyMap[entry].keys;
Input.keyMap.add(name, keys);
}
Input.eventQueue = EventQueue.getInstance();
// Subscribe to all input events
Input.eventQueue.subscribe(Input.receiver, [GameEventType.MOUSE_DOWN, GameEventType.MOUSE_UP, GameEventType.MOUSE_MOVE,
GameEventType.KEY_DOWN, GameEventType.KEY_UP, GameEventType.CANVAS_BLUR, GameEventType.WHEEL_UP, GameEventType.WHEEL_DOWN]);
}
static update(deltaT: number): void {
// Reset the justPressed values to false
Input.mouseJustPressed = false;
Input.keyJustPressed.forEach((key: string) => Input.keyJustPressed.set(key, false));
Input.justScrolled = false;
Input.scrollDirection = 0;
while(Input.receiver.hasNextEvent()){
let event = Input.receiver.getNextEvent();
// Handle each event type
if(event.type === GameEventType.MOUSE_DOWN){
Input.mouseJustPressed = true;
Input.mousePressed = true;
Input.mousePressPosition = event.data.get("position");
}
if(event.type === GameEventType.MOUSE_UP){
Input.mousePressed = false;
}
if(event.type === GameEventType.MOUSE_MOVE){
Input.mousePosition = event.data.get("position");
}
if(event.type === GameEventType.KEY_DOWN){
let key = event.data.get("key");
// Handle space bar
if(key === " "){
key = "space";
}
if(!Input.keyPressed.get(key)){
Input.keyJustPressed.set(key, true);
Input.keyPressed.set(key, true);
}
}
if(event.type === GameEventType.KEY_UP){
let key = event.data.get("key");
// Handle space bar
if(key === " "){
key = "space";
}
Input.keyPressed.set(key, false);
}
if(event.type === GameEventType.CANVAS_BLUR){
Input.clearKeyPresses()
}
if(event.type === GameEventType.WHEEL_UP){
Input.scrollDirection = -1;
Input.justScrolled = true;
} else if(event.type === GameEventType.WHEEL_DOWN){
Input.scrollDirection = 1;
Input.justScrolled = true;
}
}
}
private static clearKeyPresses(): void {
Input.keyJustPressed.forEach((key: string) => Input.keyJustPressed.set(key, false));
Input.keyPressed.forEach((key: string) => Input.keyPressed.set(key, false));
}
/**
* Returns whether or not a key was newly pressed Input frame.
* If the key is still pressed from last frame and wasn't re-pressed, Input will return false.
* @param key The key
* @returns True if the key was just pressed, false otherwise
*/
static isKeyJustPressed(key: string): boolean {
if(Input.keysDisabled) return false;
if(Input.keyJustPressed.has(key)){
return Input.keyJustPressed.get(key)
} else {
return false;
}
}
/**
* Returns an array of all of the keys that are newly pressed Input frame.
* If a key is still pressed from last frame and wasn't re-pressed, it will not be in Input list.
* @returns An array of all of the newly pressed keys.
*/
static getKeysJustPressed(): Array<string> {
if(Input.keysDisabled) return [];
let keys = Array<string>();
Input.keyJustPressed.forEach(key => {
if(Input.keyJustPressed.get(key)){
keys.push(key);
}
});
return keys;
}
/**
* Returns whether or not a key is being pressed.
* @param key The key
* @returns True if the key is currently pressed, false otherwise
*/
static isKeyPressed(key: string): boolean {
if(Input.keysDisabled) return false;
if(Input.keyPressed.has(key)){
return Input.keyPressed.get(key)
} else {
return false;
}
}
/**
* Changes the binding of an input name to keys
* @param inputName The name of the input
* @param keys The corresponding keys
*/
static changeKeyBinding(inputName: string, keys: Array<string>): void {
Input.keyMap.set(inputName, keys);
}
/**
* Clears all key bindings
*/
static clearAllKeyBindings(): void {
Input.keyMap.clear();
}
/**
* Returns whether or not an input was just pressed this frame
* @param inputName The name of the input
* @returns True if the input was just pressed, false otherwise
*/
static isJustPressed(inputName: string): boolean {
if(Input.keysDisabled) return false;
if(Input.keyMap.has(inputName)){
const keys = Input.keyMap.get(inputName);
let justPressed = false;
for(let key of keys){
justPressed = justPressed || Input.isKeyJustPressed(key);
}
return justPressed;
} else {
return false;
}
}
/**
* Returns whether or not an input is currently pressed
* @param inputName The name of the input
* @returns True if the input is pressed, false otherwise
*/
static isPressed(inputName: string): boolean {
if(Input.keysDisabled) return false;
if(Input.keyMap.has(inputName)){
const keys = Input.keyMap.get(inputName);
let pressed = false;
for(let key of keys){
pressed = pressed || Input.isKeyPressed(key);
}
return pressed;
} else {
return false;
}
}
/**
* Returns whether or not the mouse was newly pressed Input frame
* @returns True if the mouse was just pressed, false otherwise
*/
static isMouseJustPressed(): boolean {
return Input.mouseJustPressed && !Input.mouseDisabled;
}
/**
* Returns whether or not the mouse is currently pressed
* @returns True if the mouse is currently pressed, false otherwise
*/
static isMousePressed(): boolean {
return Input.mousePressed && !Input.mouseDisabled;
}
/**
* Returns whether the user scrolled or not
* @returns True if the user just scrolled Input frame, false otherwise
*/
static didJustScroll(): boolean {
return Input.justScrolled && !Input.mouseDisabled;
}
/**
* Gets the direction of the scroll
* @returns -1 if the user scrolled up, 1 if they scrolled down
*/
static getScrollDirection(): number {
return Input.scrollDirection;
}
/**
* Gets the position of the player's mouse
* @returns The mouse position stored as a Vec2
*/
static getMousePosition(): Vec2 {
return Input.mousePosition.scaled(1/this.viewport.getZoomLevel());
}
/**
* Gets the position of the player's mouse in the game world,
* taking into consideration the scrolling of the viewport
* @returns The mouse position stored as a Vec2
*/
static getGlobalMousePosition(): Vec2 {
return Input.mousePosition.clone().scale(1/this.viewport.getZoomLevel()).add(Input.viewport.getOrigin());
}
/**
* Gets the position of the last mouse press
* @returns The mouse position stored as a Vec2
*/
static getMousePressPosition(): Vec2 {
return Input.mousePressPosition;
}
/**
* Gets the position of the last mouse press in the game world,
* taking into consideration the scrolling of the viewport
* @returns The mouse position stored as a Vec2
*/
static getGlobalMousePressPosition(): Vec2 {
return Input.mousePressPosition.clone().add(Input.viewport.getOrigin());
}
/**
* Disables all keypress and mouse click inputs
*/
static disableInput(): void {
Input.keysDisabled = true;
Input.mouseDisabled = true;
}
/**
* Enables all keypress and mouse click inputs
*/
static enableInput(): void {
Input.keysDisabled = false;
Input.mouseDisabled = false;
}
}

View File

@ -0,0 +1,93 @@
import EventQueue from "../Events/EventQueue";
import Vec2 from "../DataTypes/Vec2";
import GameEvent from "../Events/GameEvent";
import { GameEventType } from "../Events/GameEventType";
/**
* Handles communication with the web browser to receive asynchronous events and send them to the @reference[EventQueue]
*/
export default class InputHandler {
private eventQueue: EventQueue;
/**
* Creates a new InputHandler
* @param canvas The game canvas
*/
constructor(canvas: HTMLCanvasElement){
this.eventQueue = EventQueue.getInstance();
canvas.onmousedown = (event) => this.handleMouseDown(event, canvas);
canvas.onmouseup = (event) => this.handleMouseUp(event, canvas);
canvas.oncontextmenu = this.handleContextMenu;
canvas.onmousemove = (event) => this.handleMouseMove(event, canvas);
document.onkeydown = this.handleKeyDown;
document.onkeyup = this.handleKeyUp;
document.onblur = this.handleBlur;
document.oncontextmenu = this.handleBlur;
document.onwheel = this.handleWheel;
}
private handleMouseDown = (event: MouseEvent, canvas: HTMLCanvasElement): void => {
let pos = this.getMousePosition(event, canvas);
let gameEvent = new GameEvent(GameEventType.MOUSE_DOWN, {position: pos});
this.eventQueue.addEvent(gameEvent);
}
private handleMouseUp = (event: MouseEvent, canvas: HTMLCanvasElement): void => {
let pos = this.getMousePosition(event, canvas);
let gameEvent = new GameEvent(GameEventType.MOUSE_UP, {position: pos});
this.eventQueue.addEvent(gameEvent);
}
private handleMouseMove = (event: MouseEvent, canvas: HTMLCanvasElement): void => {
let pos = this.getMousePosition(event, canvas);
let gameEvent = new GameEvent(GameEventType.MOUSE_MOVE, {position: pos});
this.eventQueue.addEvent(gameEvent);
}
private handleKeyDown = (event: KeyboardEvent): void => {
let key = this.getKey(event);
let gameEvent = new GameEvent(GameEventType.KEY_DOWN, {key: key});
this.eventQueue.addEvent(gameEvent);
}
private handleKeyUp = (event: KeyboardEvent): void => {
let key = this.getKey(event);
let gameEvent = new GameEvent(GameEventType.KEY_UP, {key: key});
this.eventQueue.addEvent(gameEvent);
}
private handleBlur = (event: Event): void => {
let gameEvent = new GameEvent(GameEventType.CANVAS_BLUR, {});
this.eventQueue.addEvent(gameEvent);
}
private handleContextMenu = (event: Event): void => {
event.preventDefault();
event.stopPropagation();
}
private handleWheel = (event: WheelEvent): void => {
event.preventDefault();
event.stopPropagation();
let gameEvent: GameEvent;
if(event.deltaY < 0){
gameEvent = new GameEvent(GameEventType.WHEEL_UP, {});
} else {
gameEvent = new GameEvent(GameEventType.WHEEL_DOWN, {});
}
this.eventQueue.addEvent(gameEvent);
}
private getKey(keyEvent: KeyboardEvent){
return keyEvent.key.toLowerCase();
}
private getMousePosition(mouseEvent: MouseEvent, canvas: HTMLCanvasElement): Vec2 {
let rect = canvas.getBoundingClientRect();
let x = mouseEvent.clientX - rect.left;
let y = mouseEvent.clientY - rect.top;
return new Vec2(x, y);
}
}

View File

@ -0,0 +1,47 @@
import {} from "../../index"; // This import allows us to modify the CanvasRenderingContext2D to add extra functionality
// @ignorePage
/**
* Sets up the environment of the game engine
*/
export default class EnvironmentInitializer {
static setup(){
CanvasRenderingContext2D.prototype.roundedRect = function(x: number, y: number, w: number, h: number, r: number): void {
// Clamp the radius between 0 and the min of the width or height
if(r < 0) r = 0;
if(r > Math.min(w, h)) r = Math.min(w, h);
// Draw the rounded rect
this.beginPath();
// Top
this.moveTo(x + r, y);
this.lineTo(x + w - r, y);
this.arcTo(x + w, y, x + w, y + r, r);
// Right
this.lineTo(x + w, y + h - r);
this.arcTo(x + w, y + h, x + w - r, y + h, r);
// Bottom
this.lineTo(x + r, y + h);
this.arcTo(x, y + h, x, y + h - r, r);
// Left
this.lineTo(x, y + r);
this.arcTo(x, y, x + r, y, r)
this.closePath();
}
CanvasRenderingContext2D.prototype.strokeRoundedRect = function(x, y, w, h, r){
this.roundedRect(x, y, w, h, r);
this.stroke();
}
CanvasRenderingContext2D.prototype.fillRoundedRect = function(x, y, w, h, r){
this.roundedRect(x, y, w, h, r);
this.fill();
}
}
}

View File

@ -0,0 +1,235 @@
import GameLoop from "./GameLoop";
import Debug from "../Debug/Debug";
import Stats from "../Debug/Stats";
/**
* A game loop with a fixed update time and a variable render time.
* Every frame, the game updates until all time since the last frame has been processed.
* If too much time has passed, such as if the last update was too slow,
* or if the browser was put into the background, the loop will panic and discard time.
* A render happens at the end of every frame. This happens as fast as possible unless specified.
* A loop of this type allows for deterministic behavior - No matter what the frame rate is, the update should behave the same,
* as it is occuring in a fixed interval.
*/
export default class FixedUpdateGameLoop extends GameLoop {
/** The max allowed update fps.*/
private maxUpdateFPS: number;
/** The timestep for each update. This is the deltaT passed to update calls. */
private updateTimestep: number;
/** The amount of time we are yet to simulate. */
private frameDelta: number;
/** The time when the last frame was drawn. */
private lastFrameTime: number;
/** The minimum time we want to wait between game frames. */
private minFrameDelay: number;
/** The current frame of the game. */
private frame: number;
/** The actual fps of the game. */
private fps: number;
/** The time between fps measurement updates. */
private fpsUpdateInterval: number;
/** The time of the last fps update. */
private lastFpsUpdate: number;
/** The number of frames since the last fps update was done. */
private framesSinceLastFpsUpdate: number;
/** The status of whether or not the game loop has started. */
private started: boolean;
/** The status of whether or not the game loop is paused */
private paused: boolean;
/** The status of whether or not the game loop is currently running. */
private running: boolean;
/** The number of update steps this iteration of the game loop. */
private numUpdateSteps: number;
constructor() {
super();
this.maxUpdateFPS = 60;
this.updateTimestep = Math.floor(1000/this.maxUpdateFPS);
this.frameDelta = 0;
this.lastFrameTime = 0;
this.minFrameDelay = 0;
this.frame = 0;
this.fps = this.maxUpdateFPS; // Initialize the fps to the max allowed fps
this.fpsUpdateInterval = 1000;
this.lastFpsUpdate = 0;
this.framesSinceLastFpsUpdate = 0;
this.started = false;
this.paused = false;
this.running = false;
this.numUpdateSteps = 0;
}
getFPS(): number {
return 0;
}
/**
* Updates the frame count and sum of time for the framerate of the game
* @param timestep The current time in ms
*/
protected updateFPS(timestamp: number): void {
this.fps = 0.9 * this.framesSinceLastFpsUpdate * 1000 / (timestamp - this.lastFpsUpdate) +(1 - 0.9) * this.fps;
this.lastFpsUpdate = timestamp;
this.framesSinceLastFpsUpdate = 0;
Debug.log("fps", "FPS: " + this.fps.toFixed(1));
Stats.updateFPS(this.fps);
}
/**
* Changes the maximum allowed physics framerate of the game
* @param initMax The max framerate
*/
setMaxUpdateFPS(initMax: number): void {
this.maxUpdateFPS = initMax;
this.updateTimestep = Math.floor(1000/this.maxUpdateFPS);
}
/**
* Sets the maximum rendering framerate
* @param maxFPS The max framerate
*/
setMaxFPS(maxFPS: number): void {
this.minFrameDelay = 1000/maxFPS;
}
/**
* This function is called when the game loop panics, i.e. it tries to process too much time in an entire frame.
* This will reset the amount of time back to zero.
* @returns The amount of time we are discarding from processing.
*/
resetFrameDelta() : number {
let oldFrameDelta = this.frameDelta;
this.frameDelta = 0;
return oldFrameDelta;
}
/**
* Starts up the game loop and calls the first requestAnimationFrame
*/
start(): void {
if(!this.started){
this.started = true;
window.requestAnimationFrame((timestamp) => this.doFirstFrame(timestamp));
}
}
pause(): void {
this.paused = true;
}
resume(): void {
this.paused = false;
}
/**
* The first game frame - initializes the first frame time and begins the render
* @param timestamp The current time in ms
*/
protected doFirstFrame(timestamp: number): void {
this.running = true;
this._doRender();
this.lastFrameTime = timestamp;
this.lastFpsUpdate = timestamp;
this.framesSinceLastFpsUpdate = 0;
window.requestAnimationFrame((t) => this.doFrame(t));
}
/**
* Handles any processing that needs to be done at the start of the frame
* @param timestamp The time of the frame in ms
*/
protected startFrame(timestamp: number): void {
// Update the amount of time we need our update to process
this.frameDelta += timestamp - this.lastFrameTime;
// Set the new time of the last frame
this.lastFrameTime = timestamp;
// Update the estimate of the framerate
if(timestamp > this.lastFpsUpdate + this.fpsUpdateInterval){
this.updateFPS(timestamp);
}
// Increment the number of frames
this.frame++;
this.framesSinceLastFpsUpdate++;
}
/**
* The main loop of the game. Updates until the current time is reached. Renders once
* @param timestamp The current time in ms
*/
protected doFrame = (timestamp: number): void => {
// If a pause was executed, stop doing the loop.
if(this.paused){
return;
}
// Request animation frame to prepare for another update or render
window.requestAnimationFrame((t) => this.doFrame(t));
// If we are trying to render too soon, do nothing.
if(timestamp < this.lastFrameTime + this.minFrameDelay){
return;
}
// A frame is actually happening
this.startFrame(timestamp);
// Update while there is still time to make up. If we do too many update steps, panic and exit the loop.
this.numUpdateSteps = 0;
let panic = false;
while(this.frameDelta >= this.updateTimestep){
// Do an update
this._doUpdate(this.updateTimestep/1000);
// Remove the update step time from the time we have to process
this.frameDelta -= this.updateTimestep;
// Increment steps and check if we've done too many
this.numUpdateSteps++;
if(this.numUpdateSteps > 100){
panic = true;
break;
}
}
// Updates are done, render
this._doRender();
// Wrap up the frame
this.finishFrame(panic);
}
/**
* Wraps up the frame and handles the panic state if there is one
* @param panic Whether or not the loop panicked
*/
protected finishFrame(panic: boolean): void {
if(panic) {
var discardedTime = Math.round(this.resetFrameDelta());
console.warn('Main loop panicked, probably because the browser tab was put in the background. Discarding ' + discardedTime + 'ms');
}
}
}

View File

@ -0,0 +1,214 @@
import EventQueue from "../Events/EventQueue";
import Input from "../Input/Input";
import InputHandler from "../Input/InputHandler";
import Recorder from "../Playback/Recorder";
import Debug from "../Debug/Debug";
import ResourceManager from "../ResourceManager/ResourceManager";
import Viewport from "../SceneGraph/Viewport";
import SceneManager from "../Scene/SceneManager";
import AudioManager from "../Sound/AudioManager";
import Stats from "../Debug/Stats";
import RenderingManager from "../Rendering/RenderingManager";
import CanvasRenderer from "../Rendering/CanvasRenderer";
import Color from "../Utils/Color";
import GameOptions from "./GameOptions";
import GameLoop from "./GameLoop";
import FixedUpdateGameLoop from "./FixedUpdateGameLoop";
import EnvironmentInitializer from "./EnvironmentInitializer";
import Vec2 from "../DataTypes/Vec2";
import RegistryManager from "../Registry/RegistryManager";
import WebGLRenderer from "../Rendering/WebGLRenderer";
import Scene from "../Scene/Scene";
/**
* The main loop of the game engine.
* Handles the update order, and initializes all subsystems.
* The Game manages the update cycle, and requests animation frames to render to the browser.
*/
export default class Game {
gameOptions: GameOptions;
private showDebug: boolean;
private showStats: boolean;
// The game loop
private loop: GameLoop;
// Game canvas and its width and height
readonly GAME_CANVAS: HTMLCanvasElement;
readonly DEBUG_CANVAS: HTMLCanvasElement;
readonly WIDTH: number;
readonly HEIGHT: number;
private viewport: Viewport;
private ctx: CanvasRenderingContext2D | WebGLRenderingContext;
private clearColor: Color;
// All of the necessary subsystems that need to run here
private eventQueue: EventQueue;
private inputHandler: InputHandler;
private recorder: Recorder;
private resourceManager: ResourceManager;
private sceneManager: SceneManager;
private audioManager: AudioManager;
private renderingManager: RenderingManager;
/**
* Creates a new Game
* @param options The options for Game initialization
*/
constructor(options?: Record<string, any>){
// Before anything else, build the environment
EnvironmentInitializer.setup();
// Typecast the config object to a GameConfig object
this.gameOptions = GameOptions.parse(options);
this.showDebug = this.gameOptions.showDebug;
this.showStats = this.gameOptions.showStats;
// Create an instance of a game loop
this.loop = new FixedUpdateGameLoop();
// Get the game canvas and give it a background color
this.GAME_CANVAS = <HTMLCanvasElement>document.getElementById("game-canvas");
this.DEBUG_CANVAS = <HTMLCanvasElement>document.getElementById("debug-canvas");
// Give the canvas a size and get the rendering context
this.WIDTH = this.gameOptions.canvasSize.x;
this.HEIGHT = this.gameOptions.canvasSize.y;
// This step MUST happen before the resource manager does anything
if(this.gameOptions.useWebGL){
this.renderingManager = new WebGLRenderer();
} else {
this.renderingManager = new CanvasRenderer();
}
this.initializeGameWindow();
this.ctx = this.renderingManager.initializeCanvas(this.GAME_CANVAS, this.WIDTH, this.HEIGHT);
this.clearColor = new Color(this.gameOptions.clearColor.r, this.gameOptions.clearColor.g, this.gameOptions.clearColor.b);
// Initialize debugging and stats
Debug.initializeDebugCanvas(this.DEBUG_CANVAS, this.WIDTH, this.HEIGHT);
Stats.initStats();
if(this.gameOptions.showStats) {
// Find the stats output and make it no longer hidden
document.getElementById("stats").hidden = false;
}
// Size the viewport to the game canvas
const canvasSize = new Vec2(this.WIDTH, this.HEIGHT);
this.viewport = new Viewport(canvasSize, this.gameOptions.zoomLevel);
// Initialize all necessary game subsystems
this.eventQueue = EventQueue.getInstance();
this.inputHandler = new InputHandler(this.GAME_CANVAS);
Input.initialize(this.viewport, this.gameOptions.inputs);
this.recorder = new Recorder();
this.resourceManager = ResourceManager.getInstance();
this.sceneManager = new SceneManager(this.viewport, this.renderingManager);
this.audioManager = AudioManager.getInstance();
}
/**
* Set up the game window that holds the canvases
*/
private initializeGameWindow(): void {
const gameWindow = document.getElementById("game-window");
// Set the height of the game window
gameWindow.style.width = this.WIDTH + "px";
gameWindow.style.height = this.HEIGHT + "px";
}
/**
* Retreives the SceneManager from the Game
* @returns The SceneManager
*/
getSceneManager(): SceneManager {
return this.sceneManager;
}
/**
* Starts the game
*/
start(InitialScene: new (...args: any) => Scene, options: Record<string, any>): void {
// Set the update function of the loop
this.loop.doUpdate = (deltaT: number) => this.update(deltaT);
// Set the render function of the loop
this.loop.doRender = () => this.render();
// Preload registry items
RegistryManager.preload();
// Load the items with the resource manager
this.resourceManager.loadResourcesFromQueue(() => {
// When we're done loading, start the loop
console.log("Finished Preload - loading first scene");
this.sceneManager.changeToScene(InitialScene, {}, options);
this.loop.start();
});
}
/**
* Updates all necessary subsystems of the game. Defers scene updates to the sceneManager
* @param deltaT The time sine the last update
*/
update(deltaT: number): void {
try{
// Handle all events that happened since the start of the last loop
this.eventQueue.update(deltaT);
// Update the input data structures so game objects can see the input
Input.update(deltaT);
// Update the recording of the game
this.recorder.update(deltaT);
// Update all scenes
this.sceneManager.update(deltaT);
// Update all sounds
this.audioManager.update(deltaT);
// Load or unload any resources if needed
this.resourceManager.update(deltaT);
} catch(e){
this.loop.pause();
console.warn("Uncaught Error in Update - Crashing gracefully");
console.error(e);
}
}
/**
* Clears the canvas and defers scene rendering to the sceneManager. Renders the debug canvas
*/
render(): void {
try{
// Clear the canvases
Debug.clearCanvas();
this.renderingManager.clear(this.clearColor);
this.sceneManager.render();
// Hacky debug mode
if(Input.isKeyJustPressed("g")){
this.showDebug = !this.showDebug;
}
// Debug render
if(this.showDebug){
Debug.render();
}
if(this.showStats){
Stats.render();
}
} catch(e){
this.loop.pause();
console.warn("Uncaught Error in Render - Crashing gracefully");
console.error(e);
}
}
}

View File

@ -0,0 +1,69 @@
import NullFunc from "../DataTypes/Functions/NullFunc";
/**
* The main game loop of the game. Keeps track of fps and handles scheduling of updates and rendering.
* This class is left abstract, so that a subclass can handle exactly how the loop is scheduled.
* For an example of different types of game loop scheduling, check out @link(Game Programming Patterns)(https://gameprogrammingpatterns.com/game-loop.html)
*/
export default abstract class GameLoop {
/** The function to call when an update occurs */
protected _doUpdate: Function = NullFunc;
set doUpdate(update: Function){
this._doUpdate = update;
}
/** The function to call when a render occurs */
protected _doRender: Function = NullFunc;
set doRender(render: Function){
this._doRender = render;
}
/**
* Retrieves the current FPS of the game
*/
abstract getFPS(): number;
/**
* Starts up the game loop
*/
abstract start(): void;
/**
* Pauses the game loop, usually for an error condition.
*/
abstract pause(): void;
/**
* Resumes the game loop.
*/
abstract resume(): void;
/**
* Runs the first frame of the game. No update occurs here, only a render.
* This is needed to initialize delta time values
* @param timestamp The timestamp of the frame. This is received from the browser
*/
protected abstract doFirstFrame(timestamp: number): void;
/**
* Run before any updates or the render of a frame.
* @param timestamp The timestamp of the frame. This is received from the browser
*/
protected abstract startFrame(timestamp: number): void;
/**
* The core of the frame, where any necessary updates occur, and where a render happens
* @param timestamp The timestamp of the frame. This is received from the browser
*/
protected abstract doFrame(timestamp: number): void;
/**
* Wraps up the frame
* @param panic Whether or not the update cycle panicked. This happens when too many updates try to happen in a single frame
*/
protected abstract finishFrame(panic: boolean): void;
}

View File

@ -0,0 +1,44 @@
// @ignorePage
/** The options for initializing the @reference[GameLoop] */
export default class GameOptions {
/** The size of the viewport */
canvasSize: {x: number, y: number};
/* The default level of zoom */
zoomLevel: number;
/** The color to clear the canvas to each frame */
clearColor: {r: number, g: number, b: number}
/* A list of input bindings */
inputs: Array<{name: string, keys: Array<string>}>;
/* Whether or not the debug rendering should occur */
showDebug: boolean;
/* Whether or not the stats rendering should occur */
showStats: boolean;
/* Whether or not to use webGL */
useWebGL: boolean;
/**
* Parses the data in the raw options object
* @param options The game options as a Record
* @returns A version of the options converted to a GameOptions object
*/
static parse(options: Record<string, any>): GameOptions {
let gOpt = new GameOptions();
gOpt.canvasSize = options.canvasSize ? options.canvasSize : {x: 800, y: 600};
gOpt.zoomLevel = options.zoomLevel ? options.zoomLevel : 1;
gOpt.clearColor = options.clearColor ? options.clearColor : {r: 255, g: 255, b: 255};
gOpt.inputs = options.inputs ? options.inputs : [];
gOpt.showDebug = !!options.showDebug;
gOpt.showStats = !!options.showStats;
gOpt.useWebGL = !!options.useWebGL;
return gOpt;
}
}

View File

@ -0,0 +1,137 @@
import GameNode from "./GameNode";
import Vec2 from "../DataTypes/Vec2";
import Region from "../DataTypes/Interfaces/Region";
import AABB from "../DataTypes/Shapes/AABB";
import Debug from "../Debug/Debug";
import Color from "../Utils/Color";
/**
* The representation of an object in the game world that can be drawn to the screen
*/
export default abstract class CanvasNode extends GameNode implements Region {
private _size: Vec2;
private _scale: Vec2;
private _boundary: AABB;
private _hasCustomShader: boolean;
private _customShaderKey: string;
private _alpha: number;
/** A flag for whether or not the CanvasNode is visible */
visible: boolean = true;
constructor(){
super();
this._size = new Vec2(0, 0);
this._size.setOnChange(() => this.sizeChanged());
this._scale = new Vec2(1, 1);
this._scale.setOnChange(() => this.scaleChanged());
this._boundary = new AABB();
this.updateBoundary();
this._hasCustomShader = false;
}
get alpha(): number {
return this._alpha;
}
set alpha(a: number) {
this._alpha = a;
}
get size(): Vec2 {
return this._size;
}
set size(size: Vec2){
this._size = size;
// Enter as a lambda to bind "this"
this._size.setOnChange(() => this.sizeChanged());
this.sizeChanged();
}
get scale(): Vec2 {
return this._scale;
}
set scale(scale: Vec2){
this._scale = scale;
// Enter as a lambda to bind "this"
this._scale.setOnChange(() => this.scaleChanged());
this.scaleChanged();
}
set scaleX(value: number) {
this.scale.x = value;
}
set scaleY(value: number) {
this.scale.y = value;
}
get hasCustomShader(): boolean {
return this._hasCustomShader;
}
get customShaderKey(): string {
return this._customShaderKey;
}
// @override
protected positionChanged(): void {
super.positionChanged();
this.updateBoundary();
}
/** Called if the size vector is changed or replaced. */
protected sizeChanged(): void {
this.updateBoundary();
}
/** Called if the scale vector is changed or replaced */
protected scaleChanged(): void {
this.updateBoundary();
}
// @docIgnore
/** Called if the position, size, or scale of the CanvasNode is changed. Updates the boundary. */
private updateBoundary(): void {
this._boundary.center.set(this.position.x, this.position.y);
this._boundary.halfSize.set(this.size.x*this.scale.x/2, this.size.y*this.scale.y/2);
}
get boundary(): AABB {
return this._boundary;
}
get sizeWithZoom(): Vec2 {
let zoom = this.scene.getViewScale();
return this.boundary.halfSize.clone().scaled(zoom, zoom);
}
/**
* Adds a custom shader to this CanvasNode
* @param key The registry key of the ShaderType
*/
useCustomShader(key: string): void {
this._hasCustomShader = true;
this._customShaderKey = key;
}
/**
* Returns true if the point (x, y) is inside of this canvas object
* @param x The x position of the point
* @param y The y position of the point
* @returns A flag representing whether or not this node contains the point.
*/
contains(x: number, y: number): boolean {
return this._boundary.containsPoint(new Vec2(x, y));
}
// @implemented
debugRender(): void {
Debug.drawBox(this.relativePosition, this.sizeWithZoom, false, Color.BLUE);
super.debugRender();
}
}

View File

@ -0,0 +1,478 @@
import Vec2 from "../DataTypes/Vec2";
import Receiver from "../Events/Receiver";
import Emitter from "../Events/Emitter";
import Scene from "../Scene/Scene";
import Layer from "../Scene/Layer";
import AI from "../DataTypes/Interfaces/AI";
import Physical from "../DataTypes/Interfaces/Physical";
import Positioned from "../DataTypes/Interfaces/Positioned";
import { isRegion } from "../DataTypes/Interfaces/Region";
import Unique from "../DataTypes/Interfaces/Unique";
import Updateable from "../DataTypes/Interfaces/Updateable";
import DebugRenderable from "../DataTypes/Interfaces/DebugRenderable";
import Actor from "../DataTypes/Interfaces/Actor";
import Shape from "../DataTypes/Shapes/Shape";
import AABB from "../DataTypes/Shapes/AABB";
import NavigationPath from "../Pathfinding/NavigationPath";
import TweenController from "../Rendering/Animations/TweenController";
import Debug from "../Debug/Debug";
import Color from "../Utils/Color";
import Circle from "../DataTypes/Shapes/Circle";
/**
* The representation of an object in the game world.
* To construct GameNodes, see the @reference[Scene] documentation.
*/
export default abstract class GameNode implements Positioned, Unique, Updateable, Physical, Actor, DebugRenderable {
/*---------- POSITIONED ----------*/
private _position: Vec2;
/*---------- UNIQUE ----------*/
private _id: number;
/*---------- PHYSICAL ----------*/
hasPhysics: boolean = false;
moving: boolean = false;
frozen: boolean = false;
onGround: boolean = false;
onWall: boolean = false;
onCeiling: boolean = false;
active: boolean = false;
collisionShape: Shape;
colliderOffset: Vec2;
isStatic: boolean;
isCollidable: boolean;
isTrigger: boolean;
triggerMask: number;
triggerEnters: Array<string>;
triggerExits: Array<string>;
_velocity: Vec2;
sweptRect: AABB;
collidedWithTilemap: boolean;
group: number;
isPlayer: boolean;
isColliding: boolean = false;
/*---------- ACTOR ----------*/
_ai: AI;
aiActive: boolean;
path: NavigationPath;
pathfinding: boolean = false;
/*---------- GENERAL ----------*/
/** An event receiver. */
protected receiver: Receiver;
/** An event emitter. */
protected emitter: Emitter;
/** A reference to the scene this GameNode is a part of. */
protected scene: Scene;
/** The visual layer this GameNode resides in. */
protected layer: Layer;
/** A utility that allows the use of tweens on this GameNode */
tweens: TweenController;
/** A tweenable property for rotation. Does not affect the bounding box of this GameNode - Only rendering. */
rotation: number;
/** The opacity value of this GameNode */
abstract set alpha(a: number);
abstract get alpha(): number;
// Constructor docs are ignored, as the user should NOT create new GameNodes with a raw constructor
constructor(){
this._position = new Vec2(0, 0);
this._position.setOnChange(() => this.positionChanged());
this.receiver = new Receiver();
this.emitter = new Emitter();
this.tweens = new TweenController(this);
this.rotation = 0;
}
destroy(){
this.tweens.destroy();
this.receiver.destroy();
if(this.hasPhysics){
this.removePhysics();
}
if(this._ai){
this._ai.destroy();
delete this._ai;
this.scene.getAIManager().removeActor(this);
}
this.scene.remove(this);
this.layer.removeNode(this);
}
/*---------- POSITIONED ----------*/
get position(): Vec2 {
return this._position;
}
set position(pos: Vec2) {
this._position = pos;
this._position.setOnChange(() => this.positionChanged());
this.positionChanged();
}
get relativePosition(): Vec2 {
return this.inRelativeCoordinates(this.position);
}
/**
* Converts a point to coordinates relative to the zoom and origin of this node
* @param point The point to conver
* @returns A new Vec2 representing the point in relative coordinates
*/
inRelativeCoordinates(point: Vec2): Vec2 {
let origin = this.scene.getViewTranslation(this);
let zoom = this.scene.getViewScale();
return point.clone().sub(origin).scale(zoom);
}
/*---------- UNIQUE ----------*/
get id(): number {
return this._id;
}
set id(id: number) {
// id can only be set once
if(this._id === undefined){
this._id = id;
} else {
throw "Attempted to assign id to object that already has id."
}
}
/*---------- PHYSICAL ----------*/
// @implemented
/**
* @param velocity The velocity with which to move the object.
*/
move(velocity: Vec2): void {
if(this.frozen) return;
this.moving = true;
this._velocity = velocity;
};
moveOnPath(speed: number, path: NavigationPath): void {
if(this.frozen) return;
this.path = path;
let dir = path.getMoveDirection(this);
this.moving = true;
this.pathfinding = true;
this._velocity = dir.scale(speed);
}
// @implemented
/**
* @param velocity The velocity with which the object will move.
*/
finishMove(): void {
this.moving = false;
this.position.add(this._velocity);
if(this.pathfinding){
this.path.handlePathProgress(this);
this.path = null;
this.pathfinding = false;
}
}
// @implemented
/**
* @param collisionShape The collider for this object. If this has a region (implements Region),
* it will be used when no collision shape is specified (or if collision shape is null).
* @param isCollidable Whether this is collidable or not. True by default.
* @param isStatic Whether this is static or not. False by default
*/
addPhysics(collisionShape?: Shape, colliderOffset?: Vec2, isCollidable: boolean = true, isStatic: boolean = false): void {
// Initialize the physics variables
this.hasPhysics = true;
this.moving = false;
this.onGround = false;
this.onWall = false;
this.onCeiling = false;
this.active = true;
this.isCollidable = isCollidable;
this.isStatic = isStatic;
this.isTrigger = false;
this.triggerMask = 0;
this.triggerEnters = new Array(32);
this.triggerExits = new Array(32);
this._velocity = Vec2.ZERO;
this.sweptRect = new AABB();
this.collidedWithTilemap = false;
this.group = -1; // The default group, collides with everything
// Set the collision shape if provided, or simply use the the region if there is one.
if(collisionShape){
this.collisionShape = collisionShape;
this.collisionShape.center = this.position;
} else if (isRegion(this)) {
// If the gamenode has a region and no other is specified, use that
this.collisionShape = (<any>this).boundary.clone();
} else {
throw "No collision shape specified for physics object."
}
// If we were provided with a collider offset, set it. Otherwise there is no offset, so use the zero vector
if(colliderOffset){
this.colliderOffset = colliderOffset;
} else {
this.colliderOffset = Vec2.ZERO;
}
// Initialize the swept rect
this.sweptRect = this.collisionShape.getBoundingRect();
// Register the object with physics
this.scene.getPhysicsManager().registerObject(this);
}
/** Removes this object from the physics system */
removePhysics(): void {
// Remove this from the physics manager
this.scene.getPhysicsManager().deregisterObject(this);
// Nullify all physics fields
this.hasPhysics = false;
this.moving = false;
this.onGround = false;
this.onWall = false;
this.onCeiling = false;
this.active = false;
this.isCollidable = false;
this.isStatic = false;
this.isTrigger = false;
this.triggerMask = 0;
this.triggerEnters = null;
this.triggerExits = null;
this._velocity = Vec2.ZERO;
this.sweptRect = null;
this.collidedWithTilemap = false;
this.group = -1;
this.collisionShape = null;
this.colliderOffset = Vec2.ZERO;
this.sweptRect = null;
}
/** Disables physics movement for this node */
freeze(): void {
this.frozen = true;
}
/** Reenables physics movement for this node */
unfreeze(): void {
this.frozen = false;
}
/** Prevents this object from participating in all collisions and triggers. It can still move. */
disablePhysics(): void {
this.active = false;
}
/** Enables this object to participate in collisions and triggers. This is only necessary if disablePhysics was called */
enablePhysics(): void {
this.active = true;
}
/**
* Sets the collider for this GameNode
* @param collider The new collider to use
*/
setCollisionShape(collider: Shape): void {
this.collisionShape = collider;
this.collisionShape.center.copy(this.position);
}
// @implemented
/**
* Sets this object to be a trigger for a specific group
* @param group The name of the group that activates the trigger
* @param onEnter The name of the event to send when this trigger is activated
* @param onExit The name of the event to send when this trigger stops being activated
*/
setTrigger(group: string, onEnter: string, onExit: string): void {
// Make this object a trigger
this.isTrigger = true;
// Get the number of the physics layer
let layerNumber = this.scene.getPhysicsManager().getGroupNumber(group);
if(layerNumber === 0){
console.warn(`Trigger for GameNode ${this.id} not set - group "${group}" was not recognized by the physics manager.`);
return;
}
// Add this to the trigger mask
this.triggerMask |= layerNumber;
// Layer numbers are bits, so get which bit it is
let index = Math.log2(layerNumber);
// Set the event names
this.triggerEnters[index] = onEnter;
this.triggerExits[index] = onExit;
};
// @implemented
/**
* @param group The physics group this node should belong to
*/
setGroup(group: string): void {
this.scene.getPhysicsManager().setGroup(this, group);
}
// @implemened
getLastVelocity(): Vec2 {
return this._velocity;
}
/*---------- ACTOR ----------*/
get ai(): AI {
return this._ai;
}
set ai(ai: AI) {
if(!this._ai){
// If we haven't been previously had an ai, register us with the ai manager
this.scene.getAIManager().registerActor(this);
}
this._ai = ai;
this.aiActive = true;
}
// @implemented
addAI<T extends AI>(ai: string | (new () => T), options?: Record<string, any>): void {
if(!this._ai){
this.scene.getAIManager().registerActor(this);
}
if(typeof ai === "string"){
this._ai = this.scene.getAIManager().generateAI(ai);
} else {
this._ai = new ai();
}
this._ai.initializeAI(this, options);
this.aiActive = true;
}
// @implemented
setAIActive(active: boolean, options: Record<string, any>): void {
this.aiActive = active;
if(this.aiActive){
this.ai.activate(options);
}
}
/*---------- TWEENABLE PROPERTIES ----------*/
set positionX(value: number) {
this.position.x = value;
}
set positionY(value: number) {
this.position.y = value;
}
abstract set scaleX(value: number);
abstract set scaleY(value: number);
/*---------- GAME NODE ----------*/
/**
* Sets the scene for this object.
* @param scene The scene this object belongs to.
*/
setScene(scene: Scene): void {
this.scene = scene;
}
/**
* Gets the scene this object is in.
* @returns The scene this object belongs to
*/
getScene(): Scene {
return this.scene;
}
/**
* Sets the layer of this object.
* @param layer The layer this object will be on.
*/
setLayer(layer: Layer): void {
this.layer = layer;
}
/**
* Returns the layer this object is on.
* @returns This layer this object is on.
*/
getLayer(): Layer {
return this.layer;
}
/** Called if the position vector is modified or replaced */
protected positionChanged(): void {
if(this.collisionShape){
if(this.colliderOffset){
this.collisionShape.center = this.position.clone().add(this.colliderOffset);
} else {
this.collisionShape.center = this.position.clone();
}
}
};
/**
* Updates this GameNode
* @param deltaT The timestep of the update.
*/
update(deltaT: number): void {
// Defer event handling to AI.
while(this.receiver.hasNextEvent()){
this._ai.handleEvent(this.receiver.getNextEvent());
}
}
// @implemented
debugRender(): void {
// Draw the position of this GameNode
Debug.drawPoint(this.relativePosition, Color.BLUE);
// If velocity is not zero, draw a vector for it
if(this._velocity && !this._velocity.isZero()){
Debug.drawRay(this.relativePosition, this._velocity.clone().scaleTo(20).add(this.relativePosition), Color.BLUE);
}
// If this has a collider, draw it
if(this.collisionShape){
let color = this.isColliding ? Color.RED : Color.GREEN;
if(this.isTrigger){
color = Color.MAGENTA;
}
color.a = 0.2;
if(this.collisionShape instanceof AABB){
Debug.drawBox(this.inRelativeCoordinates(this.collisionShape.center), this.collisionShape.halfSize.scaled(this.scene.getViewScale()), true, color);
} else if(this.collisionShape instanceof Circle){
Debug.drawCircle(this.inRelativeCoordinates(this.collisionShape.center), this.collisionShape.hw*this.scene.getViewScale(), true, color);
}
}
}
}
export enum TweenableProperties{
posX = "positionX",
posY = "positionY",
scaleX = "scaleX",
scaleY = "scaleY",
rotation = "rotation",
alpha = "alpha"
}

View File

@ -0,0 +1,32 @@
import CanvasNode from "./CanvasNode";
import Color from "../Utils/Color";
/**
* The representation of a game object that doesn't rely on any resources to render - it is drawn to the screen by the canvas
*/
export default abstract class Graphic extends CanvasNode {
/** The color of the Graphic */
color: Color;
constructor(){
super();
this.color = Color.RED;
}
get alpha(): number {
return this.color.a;
}
set alpha(a: number) {
this.color.a = a;
}
// @deprecated
/**
* Sets the color of the Graphic. DEPRECATED
* @param color The new color of the Graphic.
*/
setColor(color: Color){
this.color = color;
}
}

View File

@ -0,0 +1,5 @@
export enum GraphicType {
POINT = "POINT",
RECT = "RECT",
LINE = "LINE",
}

View File

@ -0,0 +1,33 @@
import Vec2 from "../../DataTypes/Vec2";
import Graphic from "../Graphic";
export default class Line extends Graphic {
protected _end: Vec2;
thickness: number;
constructor(start: Vec2, end: Vec2){
super();
this.start = start;
this.end = end;
this.thickness = 2;
// Does this really have a meaning for lines?
this.size.set(5, 5);
}
set start(pos: Vec2){
this.position = pos;
}
get start(): Vec2 {
return this.position;
}
set end(pos: Vec2){
this._end = pos;
}
get end(): Vec2 {
return this._end;
}
}

View File

@ -0,0 +1,12 @@
import Graphic from "../Graphic";
import Vec2 from "../../DataTypes/Vec2";
/** A basic point to be drawn on the screen. */
export default class Point extends Graphic {
constructor(position: Vec2){
super();
this.position = position;
this.size.set(5, 5);
}
}

View File

@ -0,0 +1,46 @@
import Graphic from "../Graphic";
import Vec2 from "../../DataTypes/Vec2";
import Color from "../../Utils/Color";
/** A basic rectangle to be drawn on the screen. */
export default class Rect extends Graphic {
/** The border color of the Rect */
borderColor: Color;
/** The width of the border */
borderWidth: number;
constructor(position: Vec2, size: Vec2){
super();
this.position = position;
this.size = size;
this.borderColor = Color.TRANSPARENT;
this.borderWidth = 0;
}
/**
* Sets the border color of this rectangle
* @param color The border color
*/
setBorderColor(color: Color): void {
this.borderColor = color;
}
// @deprecated
getBorderColor(): Color {
return this.borderColor;
}
/**
* Sets the border width of this rectangle
* @param width The width of the rectangle in pixels
*/
setBorderWidth(width: number){
this.borderWidth = width;
}
getBorderWidth(): number {
return this.borderWidth;
}
}

View File

@ -0,0 +1,49 @@
import Sprite from "./Sprite";
import AnimationManager from "../../Rendering/Animations/AnimationManager";
import Spritesheet from "../../DataTypes/Spritesheet";
import Vec2 from "../../DataTypes/Vec2";
/** An sprite with specified animation frames. */
export default class AnimatedSprite extends Sprite {
/** The number of columns in this sprite sheet */
protected numCols: number;
get cols(): number {
return this.numCols;
}
/** The number of rows in this sprite sheet */
protected numRows: number;
get rows(): number {
return this.numRows;
}
/** The animationManager for this sprite */
animation: AnimationManager;
constructor(spritesheet: Spritesheet){
super(spritesheet.name);
this.numCols = spritesheet.columns;
this.numRows = spritesheet.rows;
// Set the size of the sprite to the sprite size specified by the spritesheet
this.size.set(spritesheet.spriteWidth, spritesheet.spriteHeight);
this.animation = new AnimationManager(this);
// Add the animations to the animated sprite
for(let animation of spritesheet.animations){
this.animation.add(animation.name, animation);
}
}
/**
* Gets the image offset for the current index of animation
* @param index The index we're at in the animation
* @returns A Vec2 containing the image offset
*/
getAnimationOffset(index: number): Vec2 {
return new Vec2((index % this.numCols) * this.size.x, Math.floor(index / this.numCols) * this.size.y);
}
}

View File

@ -0,0 +1,35 @@
import CanvasNode from "../CanvasNode";
import ResourceManager from "../../ResourceManager/ResourceManager";
import Vec2 from "../../DataTypes/Vec2";
/**
* The representation of a sprite - an in-game image
*/
export default class Sprite extends CanvasNode {
/** The id of the image from the resourceManager */
imageId: string;
/** The offset of the sprite in an atlas image */
imageOffset: Vec2;
/** Whether or not the x-axis should be inverted on render */
invertX: boolean;
/** Whether or not the y-axis should be inverted on render */
invertY: boolean;
constructor(imageId: string){
super();
this.imageId = imageId;
let image = ResourceManager.getInstance().getImage(this.imageId);
this.size = new Vec2(image.width, image.height);
this.imageOffset = Vec2.ZERO;
this.invertX = false;
this.invertY = false;
}
/**
* Sets the offset of the sprite from (0, 0) in the image's coordinates
* @param offset The offset of the sprite from (0, 0) in image coordinates
*/
setImageOffset(offset: Vec2): void {
this.imageOffset = offset;
}
}

View File

@ -0,0 +1,119 @@
import Vec2 from "../DataTypes/Vec2";
import Tileset from "../DataTypes/Tilesets/Tileset";
import { TiledTilemapData, TiledLayerData } from "../DataTypes/Tilesets/TiledData"
import CanvasNode from "./CanvasNode";
import PhysicsManager from "../Physics/PhysicsManager";
/**
* The representation of a tilemap - this can consist of a combination of tilesets in one layer
*/
export default abstract class Tilemap extends CanvasNode {
/** An array of the tilesets that this tilemap uses */
protected tilesets: Array<Tileset>;
/** The size of a tile in this tilemap */
protected tileSize: Vec2;
/** An array of tile data */
protected data: Array<number>;
/** An array of tile collision data */
protected collisionMap: Array<boolean>;
/** The name of the tilemap */
name: string;
// TODO: Make this no longer be specific to Tiled
constructor(tilemapData: TiledTilemapData, layer: TiledLayerData, tilesets: Array<Tileset>, scale: Vec2) {
super();
this.tilesets = tilesets;
this.tileSize = new Vec2(0, 0);
this.name = layer.name;
let tilecount = 0;
for(let tileset of tilesets){
tilecount += tileset.getTileCount() + 1;
}
this.collisionMap = new Array(tilecount);
for(let i = 0; i < this.collisionMap.length; i++){
this.collisionMap[i] = false;
}
// Defer parsing of the data to child classes - this allows for isometric vs. orthographic tilemaps and handling of Tiled data or other data
this.parseTilemapData(tilemapData, layer);
this.scale.set(scale.x, scale.y);
}
/**
* Returns an array of the tilesets associated with this tilemap
* @returns An array of all of the tilesets assocaited with this tilemap.
*/
getTilesets(): Tileset[] {
return this.tilesets;
}
/**
* Returns the size of tiles in this tilemap as they appear in the game world after scaling
* @returns A vector containing the size of tiles in this tilemap as they appear in the game world after scaling.
*/
getTileSize(): Vec2 {
return this.tileSize.scaled(this.scale.x, this.scale.y);
}
/**
* Gets the tile size taking zoom into account
* @returns The tile size with zoom
*/
getTileSizeWithZoom(): Vec2 {
let zoom = this.scene.getViewScale();
return this.getTileSize().scale(zoom);
}
/**
* Adds this tilemap to the physics system
*/
addPhysics(): void {
this.hasPhysics = true;
this.active = true;
this.group = -1;
this.scene.getPhysicsManager().registerTilemap(this);
}
/**
* Returns the value of the tile at the specified position
* @param worldCoords The position in world coordinates
* @returns A number that represents the data value of the tile at the specified world position.
*/
abstract getTileAtWorldPosition(worldCoords: Vec2): number;
/**
* Returns the world position of the top left corner of the tile at the specified index
* @param index The index of the tile in the tileData array
* @returns The world position of the tile at the specified index
*/
abstract getTileWorldPosition(index: number): Vec2;
/**
* Returns the value of the tile at the specified index
* @param index The index of the tile in the tileData array
* @returns The value of the tile in the tileData array
*/
abstract getTile(index: number): number;
/**
* Sets the tile at the specified index
* @param index The index of the tile
* @param type The new data value of the tile
*/
abstract setTile(index: number, type: number): void;
// TODO: This shouldn't use tiled data specifically - it should be more general
/**
* Sets up the tileset using the data loaded from file
* @param tilemapData The tilemap data from file
* @param layer The layer data from file
*/
protected abstract parseTilemapData(tilemapData: TiledTilemapData, layer: TiledLayerData): void;
}

View File

@ -0,0 +1,190 @@
import Tilemap from "../Tilemap";
import Vec2 from "../../DataTypes/Vec2";
import { TiledTilemapData, TiledLayerData } from "../../DataTypes/Tilesets/TiledData";
import Debug from "../../Debug/Debug";
import Color from "../../Utils/Color";
/**
* The representation of an orthogonal tilemap - i.e. a top down or platformer tilemap
*/
export default class OrthogonalTilemap extends Tilemap {
/** The number of columns in the tilemap */
protected numCols: number;
/** The number of rows in the tilemap */
protected numRows: number;
// @override
protected parseTilemapData(tilemapData: TiledTilemapData, layer: TiledLayerData): void {
// The size of the tilemap in local space
this.numCols = tilemapData.width;
this.numRows = tilemapData.height;
// The size of tiles
this.tileSize.set(tilemapData.tilewidth, tilemapData.tileheight);
// The size of the tilemap on the canvas
this.size.set(this.numCols * this.tileSize.x, this.numRows * this.tileSize.y);
this.position.copy(this.size.scaled(0.5));
this.data = layer.data;
this.visible = layer.visible;
// Whether the tilemap is collidable or not
this.isCollidable = false;
if(layer.properties){
for(let item of layer.properties){
if(item.name === "Collidable"){
this.isCollidable = item.value;
// Set all tiles besides "empty: 0" to be collidable
for(let i = 1; i < this.collisionMap.length; i++){
this.collisionMap[i] = true;
}
}
}
}
}
/**
* Gets the dimensions of the tilemap
* @returns A Vec2 containing the number of columns and the number of rows in the tilemap.
*/
getDimensions(): Vec2 {
return new Vec2(this.numCols, this.numRows);
}
/**
* Gets the data value of the tile at the specified world position
* @param worldCoords The coordinates in world space
* @returns The data value of the tile
*/
getTileAtWorldPosition(worldCoords: Vec2): number {
let localCoords = this.getColRowAt(worldCoords);
return this.getTileAtRowCol(localCoords);
}
/**
* Get the tile at the specified row and column
* @param rowCol The coordinates in tilemap space
* @returns The data value of the tile
*/
getTileAtRowCol(rowCol: Vec2): number {
if(rowCol.x < 0 || rowCol.x >= this.numCols || rowCol.y < 0 || rowCol.y >= this.numRows){
return -1;
}
return this.data[rowCol.y * this.numCols + rowCol.x];
}
/**
* Gets the world position of the tile at the specified index
* @param index The index of the tile
* @returns A Vec2 containing the world position of the tile
*/
getTileWorldPosition(index: number): Vec2 {
// Get the local position
let col = index % this.numCols;
let row = Math.floor(index / this.numCols);
// Get the world position
let x = col * this.tileSize.x;
let y = row * this.tileSize.y;
return new Vec2(x, y);
}
/**
* Gets the data value of the tile at the specified index
* @param index The index of the tile
* @returns The data value of the tile
*/
getTile(index: number): number {
return this.data[index];
}
// @override
setTile(index: number, type: number): void {
this.data[index] = type;
}
/**
* Sets the tile at the specified row and column
* @param rowCol The position of the tile in tilemap space
* @param type The new data value of the tile
*/
setTileAtRowCol(rowCol: Vec2, type: number): void {
let index = rowCol.y * this.numCols + rowCol.x;
this.setTile(index, type);
}
/**
* Returns true if the tile at the specified row and column of the tilemap is collidable
* @param indexOrCol The index of the tile or the column it is in
* @param row The row the tile is in
* @returns A flag representing whether or not the tile is collidable.
*/
isTileCollidable(indexOrCol: number, row?: number): boolean {
// The value of the tile
let tile = 0;
if(row){
// We have a column and a row
tile = this.getTileAtRowCol(new Vec2(indexOrCol, row));
if(tile < 0){
return false;
}
} else {
if(indexOrCol < 0 || indexOrCol >= this.data.length){
// Tiles that don't exist aren't collidable
return false;
}
// We have an index
tile = this.getTile(indexOrCol);
}
return this.collisionMap[tile];
}
/**
* Takes in world coordinates and returns the row and column of the tile at that position
* @param worldCoords The coordinates of the potential tile in world space
* @returns A Vec2 containing the coordinates of the potential tile in tilemap space
*/
getColRowAt(worldCoords: Vec2): Vec2 {
let col = Math.floor(worldCoords.x / this.tileSize.x / this.scale.x);
let row = Math.floor(worldCoords.y / this.tileSize.y / this.scale.y);
return new Vec2(col, row);
}
// @override
update(deltaT: number): void {}
// @override
debugRender(){
// Half of the tile size
let zoomedHalfTileSize = this.getTileSizeWithZoom().scaled(0.5);
let halfTileSize = this.getTileSize().scaled(0.5);
// The center of the top left tile
let topLeft = this.position.clone().sub(this.size.scaled(0.5));
// A vec to store the center
let center = Vec2.ZERO;
for(let col = 0; col < this.numCols; col++){
// Calculate the x-position
center.x = topLeft.x + col*2*halfTileSize.x + halfTileSize.x;
for(let row = 0; row < this.numRows; row++){
if(this.isCollidable && this.isTileCollidable(col, row)){
// Calculate the y-position
center.y = topLeft.y + row*2*halfTileSize.y + halfTileSize.y;
// Draw a box for this tile
Debug.drawBox(this.inRelativeCoordinates(center), zoomedHalfTileSize, false, Color.BLUE);
}
}
}
}
}

View File

@ -0,0 +1,149 @@
import CanvasNode from "./CanvasNode";
import Color from "../Utils/Color";
import Vec2 from "../DataTypes/Vec2";
import Input from "../Input/Input";
/**
* The representation of a UIElement - the parent class of things like buttons
*/
export default abstract class UIElement extends CanvasNode {
// Style attributes - TODO - abstract this into a style object/interface
/** The backgound color */
backgroundColor: Color;
/** The border color */
borderColor: Color;
/** The border radius */
borderRadius: number;
/** The border width */
borderWidth: number;
/** The padding */
padding: Vec2;
// EventAttributes
/** The reaction of this UIElement on a click */
onClick: Function;
/** The event propagated on click */
onClickEventId: string;
/** The reaction to the release of a click */
onRelease: Function;
/** The event propagated on the release of a click */
onReleaseEventId: string;
/** The reaction when a mouse enters this UIElement */
onEnter: Function;
/** The event propagated when a mouse enters this UIElement */
onEnterEventId: string;
/** The reaction when a mouse leaves this UIElement */
onLeave: Function;
/** The event propogated when a mouse leaves this UIElement */
onLeaveEventId: string;
/** Whether or not this UIElement is currently clicked on */
protected isClicked: boolean;
/** Whether or not this UIElement is currently hovered over */
protected isEntered: boolean;
constructor(position: Vec2){
super();
this.position = position;
this.backgroundColor = new Color(0, 0, 0, 0);
this.borderColor = new Color(0, 0, 0, 0);
this.borderRadius = 5;
this.borderWidth = 1;
this.padding = Vec2.ZERO;
this.onClick = null;
this.onClickEventId = null;
this.onRelease = null;
this.onReleaseEventId = null;
this.onEnter = null;
this.onEnterEventId = null;
this.onLeave = null;
this.onLeaveEventId = null;
this.isClicked = false;
this.isEntered = false;
}
// @deprecated
setBackgroundColor(color: Color): void {
this.backgroundColor = color;
}
// @deprecated
setPadding(padding: Vec2): void {
this.padding.copy(padding);
}
update(deltaT: number): void {
super.update(deltaT);
// See of this object was just clicked
if(Input.isMouseJustPressed()){
let clickPos = Input.getMousePressPosition();
if(this.contains(clickPos.x, clickPos.y) && this.visible && !this.layer.isHidden()){
this.isClicked = true;
if(this.onClick !== null){
this.onClick();
}
if(this.onClickEventId !== null){
let data = {};
this.emitter.fireEvent(this.onClickEventId, data);
}
}
}
// If the mouse wasn't just pressed, then we definitely weren't clicked
if(!Input.isMousePressed()){
if(this.isClicked){
this.isClicked = false;
}
}
// Check if the mouse is hovering over this element
let mousePos = Input.getMousePosition();
if(mousePos && this.contains(mousePos.x, mousePos.y)){
this.isEntered = true;
if(this.onEnter !== null){
this.onEnter();
}
if(this.onEnterEventId !== null){
let data = {};
this.emitter.fireEvent(this.onEnterEventId, data);
}
} else if(this.isEntered) {
this.isEntered = false;
if(this.onLeave !== null){
this.onLeave();
}
if(this.onLeaveEventId !== null){
let data = {};
this.emitter.fireEvent(this.onLeaveEventId, data);
}
} else if(this.isClicked) {
// If mouse is dragged off of element while down, it is not clicked anymore
this.isClicked = false;
}
}
/**
* Overridable method for calculating background color - useful for elements that want to be colored on different after certain events
* @returns The background color of the UIElement
*/
calculateBackgroundColor(): Color {
return this.backgroundColor;
}
/**
* Overridable method for calculating border color - useful for elements that want to be colored on different after certain events
* @returns The border color of the UIElement
*/
calculateBorderColor(): Color {
return this.borderColor;
}
}

View File

@ -0,0 +1,27 @@
import Label from "./Label";
import Color from "../../Utils/Color";
import Vec2 from "../../DataTypes/Vec2";
/** A clickable button UIElement */
export default class Button extends Label {
constructor(position: Vec2, text: string){
super(position, text);
this.backgroundColor = new Color(150, 75, 203);
this.borderColor = new Color(41, 46, 30);
this.textColor = new Color(255, 255, 255);
}
// @override
calculateBackgroundColor(): Color {
// Change the background color if clicked or hovered
if(this.isEntered && !this.isClicked){
return this.backgroundColor.lighten();
} else if(this.isClicked){
return this.backgroundColor.darken();
} else {
return this.backgroundColor;
}
}
}

View File

@ -0,0 +1,152 @@
import Vec2 from "../../DataTypes/Vec2";
import Color from "../../Utils/Color";
import UIElement from "../UIElement";
/** A basic text-containing label */
export default class Label extends UIElement{
/** The color of the text of this UIElement */
textColor: Color;
/** The value of the text of this UIElement */
text: string;
/** The name of the font */
font: string;
/** The size of the font */
fontSize: number;
/** The horizontal alignment of the text within the label */
protected hAlign: string;
/** The vertical alignment of text within the label */
protected vAlign: string;
/** A flag for if the width of the text has been measured on the canvas for auto width assignment */
protected sizeAssigned: boolean;
constructor(position: Vec2, text: string){
super(position);
this.text = text;
this.textColor = new Color(0, 0, 0, 1);
this.font = "Arial";
this.fontSize = 30;
this.hAlign = "center";
this.vAlign = "center";
this.sizeAssigned = false;
}
// @deprecated
setText(text: string): void {
this.text = text;
}
// @deprecated
setTextColor(color: Color): void {
this.textColor = color;
}
/**
* Gets a string containg the font details for rendering
* @returns A string containing the font details
*/
getFontString(): string {
return this.fontSize + "px " + this.font;
}
/**
* Overridable method for calculating text color - useful for elements that want to be colored on different after certain events
* @returns a string containg the text color
*/
calculateTextColor(): string {
return this.textColor.toStringRGBA();
}
/**
* Uses the canvas to calculate the width of the text
* @param ctx The rendering context
* @returns A number representing the rendered text width
*/
protected calculateTextWidth(ctx: CanvasRenderingContext2D): number {
ctx.font = this.fontSize + "px " + this.font;
return ctx.measureText(this.text).width;
}
setHAlign(align: string): void {
this.hAlign = align;
}
setVAlign(align: string): void {
this.vAlign = align;
}
/**
* Calculate the offset of the text - this is used for rendering text with different alignments
* @param ctx The rendering context
* @returns The offset of the text in a Vec2
*/
calculateTextOffset(ctx: CanvasRenderingContext2D): Vec2 {
let textWidth = this.calculateTextWidth(ctx);
let offset = new Vec2(0, 0);
let hDiff = this.size.x - textWidth;
if(this.hAlign === HAlign.CENTER){
offset.x = hDiff/2;
} else if (this.hAlign === HAlign.RIGHT){
offset.x = hDiff;
}
if(this.vAlign === VAlign.TOP){
ctx.textBaseline = "top";
offset.y = 0;
} else if (this.vAlign === VAlign.BOTTOM){
ctx.textBaseline = "bottom";
offset.y = this.size.y;
} else {
ctx.textBaseline = "middle";
offset.y = this.size.y/2;
}
return offset;
}
protected sizeChanged(): void {
super.sizeChanged();
this.sizeAssigned = true;
}
/**
* Automatically sizes the element to the text within it
* @param ctx The rendering context
*/
protected autoSize(ctx: CanvasRenderingContext2D): void {
let width = this.calculateTextWidth(ctx);
let height = this.fontSize;
this.size.set(width + this.padding.x*2, height + this.padding.y*2);
this.sizeAssigned = true;
}
/**
* Initially assigns a size to the UIElement if none is provided
* @param ctx The rendering context
*/
handleInitialSizing(ctx: CanvasRenderingContext2D): void {
if(!this.sizeAssigned){
this.autoSize(ctx);
}
}
/** On the next render, size this element to it's current text using its current font size */
sizeToText(): void {
this.sizeAssigned = false;
}
}
export enum VAlign {
TOP = "top",
CENTER = "center",
BOTTOM = "bottom"
}
export enum HAlign {
LEFT = "left",
CENTER = "center",
RIGHT = "right"
}

View File

@ -0,0 +1,64 @@
import Vec2 from "../../DataTypes/Vec2";
import Input from "../../Input/Input";
import Color from "../../Utils/Color";
import MathUtils from "../../Utils/MathUtils";
import UIElement from "../UIElement";
/** A slider UIElement */
export default class Slider extends UIElement {
/** The value of the slider from [0, 1] */
protected value: number;
/** The color of the slider nib */
public nibColor: Color;
/** The size of the nib */
public nibSize: Vec2;
/** The color of the slider track */
public sliderColor: Color;
/** The reaction of this UIElement to a value change */
public onValueChange: Function;
/** The event propagated by this UIElement when value changes */
public onValueChangeEventId: string;
constructor(position: Vec2, initValue: number){
super(position);
this.value = initValue;
this.nibColor = Color.RED;
this.sliderColor = Color.BLACK;
this.backgroundColor = Color.TRANSPARENT;
this.borderColor = Color.TRANSPARENT;
this.nibSize = new Vec2(10, 20);
// Set a default size
this.size.set(200, 20);
}
/**
* Retrieves the value of the slider
* @returns The value of the slider
*/
getValue(): number {
return this.value;
}
/** A method called in response to the value changing */
protected valueChanged(): void {
if(this.onValueChange){
this.onValueChange(this.value);
}
if(this.onValueChangeEventId){
this.emitter.fireEvent(this.onValueChangeEventId, {target: this, value: this.value});
}
}
update(deltaT: number): void {
super.update(deltaT);
if(this.isClicked){
let val = MathUtils.invLerp(this.position.x - this.size.x/2, this.position.x + this.size.x/2, Input.getMousePosition().x);
this.value = MathUtils.clamp01(val);
this.valueChanged();
}
}
}

View File

@ -0,0 +1,64 @@
import Vec2 from "../../DataTypes/Vec2";
import Color from "../../Utils/Color";
import Label from "./Label";
import Input from "../../Input/Input";
/** A text input UIElement */
export default class TextInput extends Label {
/** A flag the represents whether the user can type in this TextInput */
focused: boolean;
/** The position of the cursor in this TextInput */
cursorCounter: number;
constructor(position: Vec2){
super(position, "");
this.focused = false;
this.cursorCounter = 0;
// Give a default size to the x only
this.size.set(200, this.fontSize);
this.hAlign = "left";
this.borderColor = Color.BLACK;
this.backgroundColor = Color.WHITE;
}
update(deltaT: number): void {
super.update(deltaT);
if(Input.isMouseJustPressed()){
let clickPos = Input.getMousePressPosition();
if(this.contains(clickPos.x, clickPos.y)){
this.focused = true;
this.cursorCounter = 30;
} else {
this.focused = false;
}
}
if(this.focused){
let keys = Input.getKeysJustPressed();
let nums = "1234567890";
let specialChars = "`~!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?";
let letters = "qwertyuiopasdfghjklzxcvbnm";
let mask = nums + specialChars + letters;
keys = keys.filter(key => mask.includes(key));
let shiftPressed = Input.isKeyPressed("shift");
let backspacePressed = Input.isKeyJustPressed("backspace");
let spacePressed = Input.isKeyJustPressed("space");
if(backspacePressed){
this.text = this.text.substring(0, this.text.length - 1);
} else if(spacePressed){
this.text += " ";
} else if(keys.length > 0) {
if(shiftPressed){
this.text += keys[0].toUpperCase();
} else {
this.text += keys[0];
}
}
}
}
}

View File

@ -0,0 +1,6 @@
export enum UIElementType {
BUTTON = "BUTTON",
LABEL = "LABEL",
SLIDER = "SLIDER",
TEXT_INPUT = "TEXTINPUT"
}

Some files were not shown because too many files have changed in this diff Show More