You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
313 lines
14 KiB
HTML
313 lines
14 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<meta name="description" content="">
|
|
<meta name="author" content="Nathan (Aney) Steel">
|
|
<meta name="theme-color" content="white">
|
|
<meta name="theme-color" content="green">
|
|
<link rel="stylesheet" type="text/css" href="/main.css">
|
|
<link rel="icon" type="image/png" href="/images/favicon.svg">
|
|
<title>Project 1: ECSey Code Restructure</title>
|
|
</head>
|
|
|
|
<body>
|
|
<header>
|
|
<a href="#main" class="vh">Jump directly to main content</a>
|
|
<h1 style="font-size: 2.4rem">Project 1: ECSey Code Restructure</h1>
|
|
<input id="burger-toggle" type="checkbox"/>
|
|
<label class="burger-container" for="burger-toggle"><div class="burger"></div><span class="sr">Burger menu</span></label>
|
|
<hr/>
|
|
<nav>
|
|
<a href="/">home</a>
|
|
<a href="/about.html">about</a>
|
|
<a href="/projects.html">projects</a>
|
|
<a href="/blog/">blog</a>
|
|
<a href="/sitemap.html">misc</a>
|
|
<a href="/support.html">support</a>
|
|
</nav>
|
|
<hr/>
|
|
</header>
|
|
|
|
<main id="main">
|
|
<p>Believe me when I say, the <a href="/blog/p1-basic-mechanic-hack-in.html">hack in</a> code hurts me more than it hurts you. With that said, it was essential to getting the core in, and can easily be re-written, which I started on immediately after it's completion.</p>
|
|
|
|
<h2>Data Oriented?</h2>
|
|
<p>While creating the core of the game, even during the hack in I had a somewhat data-oriented approach, with the use of arrays, and objects of pure data scattered around rather than classes that could bulk and slow the game down.</p>
|
|
<p>As it was hacky though, it eventially become that there were too many arrays with too much similar data, that were accessed slightly differently.</p>
|
|
<p>This led me to look into slowly writting the code base over, replacing individual arrays and functions with more universal data, with each piece of data only being referrenced once, and any functionality directly working with that data.</p>
|
|
|
|
<h2>ECSey</h2>
|
|
<p>With my mind in a semi data oriented mode, I thought of other games (and performant required software) and how they would typically implement an ECS (Entity Component System), and I opted to go the same route.</p>
|
|
<p>I have never written an ECS, and didn't look into it, as I vaguely understood what's what and the purpose. Since I did this however I'm unsure if I ended up with a 'true ECS', so I opted to refer to my codebase as 'ECSey'.</p>
|
|
<p>Here's how it works, mostly.</p>
|
|
|
|
<h3>The Breakdown</h3>
|
|
<h4>Entity</h4>
|
|
<p>The 'object' itself, these are stored in an 'item' array, and are stored only as an integer, no other data at all.</p>
|
|
<pre class="pre--small"><code>item = [0,1,2,3,4];</code></pre>
|
|
<h4>Component</h4>
|
|
<p>The 'attributes' of the Entity. These contain the actual data for each of the 'objects'/entities. They are stored in an array (an object in JS) and are accessed by referring to said array with the key of the 'entity'/item ID the attribute belongs to.</p>
|
|
<pre class="pre--small"><code>size[entityId] = [width, height]; position[itemKey] = [positionX,positionY];</code></pre>
|
|
<h4>System</h4>
|
|
<p>Each system consists of the function calls, and processes that access and perform actions with the entity's attributes, iterating each of them and performing the actions according to their data. So, for instance I have a 'drawItems' function that loops each item, and draws that item to the board based on the entity's size, and position (if it has then both set).</p>
|
|
|
|
|
|
<h2>ECSey code examples</h2>
|
|
<p>And here are some snippets of what the new codebase looks like at current. Still not finalised, but so much better.</p>
|
|
|
|
<h3>Basic Components</h3>
|
|
<p>As this is the first pass for the ECSey design, I replicated the existing arrays and objects into components/attributes. This is not all there will be in the future, in fact this list has more than doubled already!</p>
|
|
<pre class="pre--small"><code>let item = [];
|
|
let itemCount = 0; // Used to keep track of how many items there have been
|
|
|
|
let boardElement = {};
|
|
let cardData = {};
|
|
let position = {};
|
|
let size = {};
|
|
let cardStatus = {};
|
|
let player = {};
|
|
let listPosition = {};</code></pre>
|
|
|
|
<h3>Get Items</h3>
|
|
<p>This loop all the entities, compares the passed 'boardElement', 'playerId', 'cardStatus', 'listPosition', and any other attributes passed, and returns a new array of only the entities that match the criteria.</p>
|
|
<pre class="pre--small"><code>getItems(playerId = null, boardElementId = null, ... ){
|
|
|
|
let newItems = item; // Set array to all items (item is current name of entities array)
|
|
let tempArray = []; // New array for filtering
|
|
|
|
// Check if each item shares the PLAYERID passed
|
|
if(playerId !== null){ // Only check if a playerId has been passed to getItems
|
|
|
|
for(let newItem = 0; newItem < newItems.length; newItem++){
|
|
|
|
let itemKey = newItems[newItem]; // Get the entity key/ID
|
|
|
|
// If the item shares the playerId, add it to the tempArray
|
|
if(playerId == player[itemKey]){
|
|
tempArray.push(newItems[newItem]);
|
|
}
|
|
|
|
}
|
|
|
|
// Set newItems to tempArray so it can be looped again with only what matched
|
|
newItems = tempArray;
|
|
}
|
|
// Reset tempArray so it can be reused in next loop
|
|
tempArray = [];
|
|
|
|
...
|
|
|
|
// Return the new specified itemList
|
|
return newItems;
|
|
</code></pre>
|
|
<p>Each of the components passed gets checked and looped in the same manner, slowly filtering out what's not wanted. Then the newItems array is returned to be used however needed.</p>
|
|
|
|
<h3>Get Item Key</h3>
|
|
<p>Sometimes only a single item is wanted. This can be retried by passing the three core components I have in the game to date.</p>
|
|
<pre class="pre--small"><code>getItemKey(boardElementId, listPositionId, playerId){
|
|
|
|
// This calls getItems from example above, passing the component data
|
|
let itemKey = this.getItems(boardElementId, playerId, null, listPositionId);
|
|
|
|
if(itemKey.length < 1){ return false; } // Didn't find a key
|
|
if(itemKey.length > 1){ return false } // Found more than 1 key
|
|
|
|
// Return first index of array, aka the itemKey
|
|
return itemKey[0];
|
|
}</code></pre>
|
|
<p>All this does is call getItems, does a little error handling and returns the first (only) entitiy.</p>
|
|
<p>This will only return one, unless something gets broken elsewhere, as each player's boardElements have unique 1..X listPositions that get moved up/down when something is added/removed from the boardElement. There are also error messages, but I've ommited them from the snippet.</p>
|
|
|
|
|
|
|
|
<h3>Playing to boardElement, Board/Mana/Shield</h3>
|
|
<p>To demonstrate the fact that getItemKey() will only return one entity we'll look at how I switch entities between the 'boardElements'</p>
|
|
<pre class="pre--small"><code>addFromBoardElement(playerFrom, fromPosition, elementFrom, elementTo, toPosition=null, playerTo=null){
|
|
|
|
// If no playerTo provided (typical behavior), pass between elements for same player
|
|
if(playerTo == null){ playerTo = playerFrom; }
|
|
|
|
// Check there is only 1 item that exists with the from info
|
|
let itemKey = this.getItemKey(elementFrom, fromPosition, fromPlayer);
|
|
|
|
// Check if there is a specific position the item needs to go to
|
|
if(toPosition == null){
|
|
// If not get the next available position of the elementTo
|
|
toPosition = getCurrentPositionAndLength(elementTo, playerTo)[0]+1;
|
|
}
|
|
|
|
this.setCardPosition(itemKey, toPosition, elementTo, playerTo, fromPosition, elementFrom, playerFrom);
|
|
this.removeItemStatus(itemKey); // Untap, remove 'attacking' etc.
|
|
|
|
}</code></pre>
|
|
<p>setCardPosition called first decreases the listPosition of anything from the old boardElement after the old listPosition by 1. It then increases the listPositions in the new boardElement up by one for anything equal to or higher than the new position.</p>
|
|
<p>Don't worry if it doesn't make sense at first, I wrote it and it took me a while of writing arrays in a physical notebook before I was sure what would work.</p>
|
|
<pre class="pre--small"><code>setCardPosition(card, newPosition, newElement, newPlayer, oldPosition, oldElement, oldPlayer){
|
|
|
|
// Move old boardElement listPositions down
|
|
this.moveElementPositions(0, oldElement, oldPosition, oldPlayer);
|
|
|
|
// Move new boardElement listPositions up
|
|
this.moveElementPositions(1, newElement, newPosition, newPlayer);
|
|
|
|
// Then fit the card into the new gap that's opened up in new boardElement
|
|
listPosition[card] = newPosition;
|
|
boardElement[card] = newElement;
|
|
}</code></pre>
|
|
|
|
<p>This is what moveElementPositions does.</p>
|
|
<pre class="pre--small"><code>moveElementPositions(direction, elementFrom, fromPosition, playerFrom){
|
|
|
|
// Loop the elementFrom, and move positions up/down by one
|
|
let items = this.getItems(elementFrom, playerFrom, null, null);
|
|
|
|
for(let item = 0; item < items.length; item++){
|
|
let itemKey = items[item];
|
|
|
|
// Move everything after the old position down
|
|
if(direction == 0 && listPosition[itemKey] > fromPosition){
|
|
listPosition[itemKey]--;
|
|
}
|
|
// Move everything from the new position up
|
|
if(direction == 1 && listPosition[itemKey] >= fromPosition){
|
|
listPosition[itemKey]++;
|
|
}
|
|
}
|
|
}</code></pre>
|
|
|
|
<h3>Attacking</h3>
|
|
<p>Some of the changes to the attacking code, that you may remember was shown in it's prior hacked-in form.</p>
|
|
<pre class="pre--small"><code>startAttack(itemAttacking){
|
|
if(this.isTapped(itemAttacking)){ return false; }
|
|
|
|
this.setEvent('attack', itemAttacking);
|
|
this.setCardStatus(itemAttacking, 'attacking');
|
|
}</code></pre>
|
|
|
|
<pre class="pre--small"><code>makeAttack(itemDefending, itemAttacking = null){
|
|
|
|
// If itemAttacking not defined, use the item from inEvent
|
|
if(itemAttacking == null){ itemAttacking = inEvent[1]; }
|
|
|
|
if(this.isTapped(itemAttacking)){ return false; }
|
|
|
|
switch (boardElement[itemDefending]) {
|
|
|
|
case 'board':
|
|
let atkAttacker = this.attackOf(itemAttacking);
|
|
let atkDefender = this.attackOf(itemDefending);
|
|
|
|
// Does Attacker kill Defender
|
|
if(atkDefender <= atkAttacker){ this.sendToGrave(itemDefending); }
|
|
|
|
// Does Defender kill Attacker
|
|
if(atkAttacker <= atkDefender){
|
|
this.sendToGrave(itemAttacking);
|
|
this.endAttackFor(itemAttacking);
|
|
}
|
|
else{ this.endAttackFor(itemAttacking); }
|
|
|
|
break;
|
|
|
|
case 'shield':
|
|
// If the shield is tapped 'destroy' it
|
|
if(this.isTapped(itemDefending)){ this.destroyShield(itemDefending); }
|
|
|
|
// Otherwise tap the shield, so it can be destroyed in future
|
|
else{ this.tapCard(itemDefending); }
|
|
|
|
this.endAttackFor(itemAttacking);
|
|
|
|
break;
|
|
}
|
|
|
|
}</code></pre>
|
|
|
|
<p>And here's the first pass of the eventListener change to start/make attacks.</p>
|
|
<pre class="pre--small"><code>for(let itemKey = 0; itemKey < item.length; itemKey++){
|
|
|
|
if(itemKey in size && itemKey in position && clickableCheck(x,y,itemKey)){
|
|
|
|
let playerId = player[itemKey];
|
|
|
|
// Check the location of element
|
|
switch(boardElement[itemKey]){
|
|
|
|
case 'board':
|
|
// playerBoard
|
|
if(!inEvent && !board.isTapped(itemKey) && playerId == yourPlayerId){
|
|
board.startAttack(itemKey);
|
|
break;
|
|
}
|
|
// opponentBoard
|
|
if(inEvent && inEvent[0] == 'attack' && inEvent[1] != itemKey && playerId != yourPlayerId){
|
|
// Make attack on the card clicked, with card in inEvent[1]
|
|
board.makeAttack(itemKey);
|
|
break;
|
|
}
|
|
|
|
...
|
|
|
|
}
|
|
|
|
...
|
|
|
|
board.drawBoard();
|
|
return true;
|
|
}
|
|
}</code></pre>
|
|
|
|
<h3>Helper Functions</h3>
|
|
<p>These are smaller functions that perform specific tasks, but written so that I can call them and not replicate code. The examples are simple ones, and some are more simplified for the demonstation.</p>
|
|
<pre class="pre--small"><code>tapCard(itemKey){ cardStatus[itemKey] = 'tapped'; } </code></pre>
|
|
<pre class="pre--small"><code>remainingShieldCount(playerId){ return getCurrentPositionAndLength('shield', playerId)[1]; }</code></pre>
|
|
<pre class="pre--small"><code>isTapped(itemKey){ if(cardStatus[itemKey] == 'tapped'){ return true; } return false; }</code></pre>
|
|
<pre class="pre--small"><code>isAttacking(itemKey){ if(cardStatus[itemKey] == 'attacking'){ return true; } return false; }</code></pre>
|
|
<pre class="pre--small"><code>destroyShield(itemKey){
|
|
|
|
if(!this.isTapped(itemKey)){ return false; }
|
|
|
|
let items = this.getItems('shield', player[itemKey], null, null);
|
|
for(let item = 0; item < items.length; item++){
|
|
|
|
let itemKey = items[item];
|
|
|
|
// If ANY of their shields are untapped, you can't destroy target
|
|
if(!board.isTapped(itemKey)){
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Shield is now destroyed, move it from shield to owners hand (for the catchup mechanic)
|
|
this.addShieldToHand(player[itemKey], listPosition[itemKey]);
|
|
return true;
|
|
}</code></pre>
|
|
|
|
<pre class="pre--small"><code>addShieldToHand(playerFrom = null, listPositionFrom = null){
|
|
this.addFromBoardElement(playerFrom, listPositionFrom, 'shield', 'hand', null, null);
|
|
}</code></pre>
|
|
<pre class="pre--small"><code>sendToGrave(itemKey){
|
|
this.addFromBoardElement(player[itemKey], listPosition[itemKey], boardElement[itemKey], 'grave', null, null);
|
|
}</code></pre>
|
|
|
|
<p>Some helpers like 'addShieldToHand' have other bits to them too that would trigger other functions, or effects too. Some are literally just one liners to make the code easier to understand and work with.</p>
|
|
|
|
<h2>No more hiding what the game is?</h2>
|
|
<p>For my code examples from now on, they will be near identical to the code at the time I merge the feature the topic is based on into my devlopment branch. Eventually (rather soon in episodic article terms) I will need to detail what the game is to convey the why, so look forwards to that.</p>
|
|
<p>Being said, up to then I won't directly address the genre, but won't be changing names of 'card', etc. anymore.</p>
|
|
<p>I hope to keep showing the progressing code-base, and would love for people to follow my development journey, and to inspire others to start a game. If you do start a game, feel free to reference my work in these articles, but try not to rip off everything (despite what I post being mostly unfinished/finalised).</p>
|
|
|
|
<h2>Updates</h2>
|
|
<p>29/10/24 - If you're reading this as a semi-guide, this is not the optimal way to deal with entities/components. I will be rewriting this once the core functionality of the game is written.</p>
|
|
</main>
|
|
|
|
<footer>
|
|
<hr/>
|
|
<p>Written by <a href="https://aney.co.uk" target="_blank" rel="noopener">@aney</a> with <a href="https://danluu.com/web-bloat/" target="_blank" rel="noopener">web bloat</a> in mind | <a href="https://github.com/Aney/website" target="_blank" rel="noopener">Source Code</a></p>
|
|
</footer>
|
|
</body>
|
|
</html>
|
|
|