This commit is contained in:
tetreum
2021-03-20 12:54:54 +01:00
parent 34bb70f1a5
commit dbde289d0e
10 changed files with 1066 additions and 6 deletions

View File

@@ -28,7 +28,7 @@ namespace Brickcraft.World
public void Generate()
{
ManualResetEvent[] genWait = new ManualResetEvent[threadsNum];
ThreadPool.SetMaxThreads(threadsNum,threadsNum);
ThreadPool.SetMaxThreads(threadsNum, threadsNum);
int totalLoops = (Math.Abs(fromChunkX) + Math.Abs(toChunkX));
int basePartition = totalLoops / threadsNum;

View File

@@ -68,11 +68,7 @@ namespace Brickcraft.World
ChunkGenManager chunkGenManager = new ChunkGenManager(MapMinChunkX, MapMaxChunkX + 1, 6, this, 13284938921, chunkEntries);
chunkGenManager.Generate();
/*
CalculateStartLight();
LightAlgorithmRecorder.InitPlayback();
*/
for(int x = 0; x < ChunksNum; ++x)
{
chunkEntries[x].Init();

50
docs/blog-editor.html Normal file
View File

@@ -0,0 +1,50 @@
<div class="container">
<form id="blog-editor">
<div class="form-group">
<label for="existing-entry-selector">Entry</label>
<select id="existing-entry-selector" class="form-control">
<option value="">New entry</option>
</select>
</div>
<div class="form-group">
<label for="title">Title</label>
<input type="text" class="form-control" id="title" placeholder="" required>
</div>
<div class="form-group">
<label for="date">Date</label>
<input type="datetime-local" class="form-control" id="date" required>
</div>
<br>
<div class="form-group">
<textarea id="editor" name="editor"></textarea>
</div>
<br>
<button type="submit" class="btn btn-primary">Generate entry</button>
<a id="preview" target="_blank" class="btn btn-secondary">Preview</a>
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/jodit/3.6.1/jodit.min.css"/>
<script src="//cdnjs.cloudflare.com/ajax/libs/jodit/3.6.1/jodit.min.js"></script>
<style>
#output textarea {
height: 30vh;
}
</style>
<script src="js/editor.js"></script>
</form>
<hr>
<div id="output" class="row d-none">
<div class="col-6">
<label for="post-summary">
config.json -> entries
</label>
<textarea id="post-summary" class="form-control"></textarea>
</div>
<div class="col-6">
<label for="post-html">
</label>
<textarea id="post-html" class="form-control"></textarea>
</div>
</div>
</div>

12
docs/config.json Normal file
View File

@@ -0,0 +1,12 @@
{
"title": "Brickcraft",
"logo": "https://i.imgur.com/Ns1x0fQ.png",
"entries" : [
{
"slug": "devblog-1-estudiando-los-bricks",
"title": "Devblog 1 - Estudiando los bricks",
"preview": "Estas navidades quería montar un set de Lego navideño con unos amigos, pero con el tema COVID por en medio no ha sido posible. Así que me ha dado por hacer un juego donde podamos montar...",
"date": "2021-03-20T17:22:00Z"
}
]
}

56
docs/css/seger.css Normal file
View File

@@ -0,0 +1,56 @@
a, a:focus, a:hover, a:active {
text-decoration: none;
color: #4183C4;
}
a:hover {
color: #266aad;
}
.btn-link {
text-decoration: none;
}
h1 {
font-weight: 800;
line-height: 1.1;
}
.post-title {
color: #404040;
}
.post-title:focus, .post-title:hover {
color: #0085A1;
}
.post-meta {
color: #808080;
font-style: italic;
}
.social-share-section .bi-linkedin {
color: #fff;
background-color: #007bb6;
}
.social-share-section .bi-facebook {
color: #fff;
background-color: #38529a;
}
.social-share-section .bi-twitter {
color: #fff;
background-color: #55acee;
}
.social-share-section .bi-telegram {
color: #fff;
background-color: #2fa9e1;
}
.social-share-section .bi-whatsapp {
color: #fff;
background-color: #24cc63;
}
.social-share-section .bi-envelope {
color: #000;
background-color: #f7f7f7;
}
article {
padding-bottom: 2em;
border-bottom: 1px solid #eee;
margin: 1em 0 15px;
}
article img, article video {
max-width: 100%;
}

80
docs/help.html Normal file
View File

@@ -0,0 +1,80 @@
<h2>Current issues / Stuck on</h2>
<p>Small list of issues that are preventing me from further progress.</p>
<ul>
<li><a href="#wrong-uvs">Set correct uvs on terrain mesh</a></li>
<li><a href="#broken-terrain-mesh">Terrain mesh is randomly broken because of brick's top face</a></li>
<li><a href="#remove-terrain-bricks">Ability to remove terrain bricks</a></li>
</ul>
<h3 id="wrong-uvs">Set correct uvs on terrain mesh</h3>
<p>Terrain is using a single texture:</p>
<p><img src="https://github.com/tetreum/brickcraft/raw/main/Assets/Resources/Textures/Terrain.png"></p>
<p>But i don't now how to set the uvs in order to use it properly.<br>
If i set all vertices, the entire texture is used, which is not what i'm looking for:
</p>
<img src="https://i.imgur.com/qhKWNES.png">
<p>With my latest attempt it looks like</p>
<img src="https://i.imgur.com/UZJQpQu.png">
<p>Each face has only 1 color => good. But it's not the same color across all faces and also it's always the wrong color => fail.</p>
<p>Here is the code that handles uvs setup: <a href="https://github.com/tetreum/brickcraft/blob/main/Assets/Scripts/World/ChunkRenderer.cs#L546" target="_blank" rel="nofollow">ChunkRenderer.cs#L546</a>
</p>
<hr>
<h3 id="broken-terrain-mesh">Terrain mesh is randomly broken because of brick's top face</h3>
<p>
In order to generate the terrain mesh, i grab the brick model and i use planes to delimitate each face.
</p>
<img src="https://i.imgur.com/XuwjStb.png">
<p>Notice the blue planes "cutting" each face.</p>
<p>Then i use a <a href="https://github.com/tetreum/brickcraft/blob/main/Assets/Scripts/Utils/FaceMapper.cs" target="_blank" rel="nofollow">small class</a> that iterates over all brick mesh's vertices to know to which face do they belong.</p>
<p>
After that, when building a mesh slice, i just add the visible brick faces of each case at
<a href="https://github.com/tetreum/brickcraft/blob/main/Assets/Scripts/World/ChunkRenderer.cs#L495" target="_blank" rel="nofollow">ChunkRenderer.cs#L495</a>
</p>
<p>If top face is properly cut:</p>
<img src="https://i.imgur.com/XuwjStb.png">
<p>Then it randomly breaks:</p>
<img src="https://i.imgur.com/yYOjSjG.png">
<p>If only studs are set as top face:</p>
<img src="https://i.imgur.com/s0gMxMJ.png">
<p>It doesn't break:</p>
<img src="https://i.imgur.com/NGUxIuA.png">
<p>
Notice how all other faces are also rendering properly now. Yet they haven't been edited.
</p>
<p></p>
How the heck can they "randomly" break?<br>
If vertices that are used both in top and lateral faces are the problem, why do they not always break?<br>
What am i missing out?......<br>
</p>
<p>The terrain generation scene is <a href="https://github.com/tetreum/brickcraft/blob/main/Assets/Scenes/WorldGenerationTest.unity" target="_blank" rel="nofollow">/Scenes/WorldGenerationTest.unity</a></p>
<p>This is the obj being used: <a href="https://raw.githubusercontent.com/tetreum/brickcraft/main/Assets/Models/Bricks/3003/3003.obj" target="_blank" rel="nofollow">/Models/Bricks/3003/3003.obj</a>
</p>
<hr>
<h3 id="remove-terrain-bricks">Remove terrain bricks</h3>
<p>Well, basic feature not made yet.</p>
<p>
Digging takes places at <a href="https://github.com/tetreum/brickcraft/blob/34bb70f1a5f7438acf396b9c10a50b8dae50c8a2/Assets/Scripts/Player/Player.cs#L53">/Scripts/Player/Player.cs#L53</a>.
<br>
And <a href="https://github.com/tetreum/brickcraft/blob/main/Assets/Scripts/World/WorldBehaviour.cs#L259">/Scripts/World/WorldBehaviour.cs#L259</a> has some methods to transform coords to Chunks.
</p>

113
docs/index.html Normal file
View File

@@ -0,0 +1,113 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Akaya+Telivigala&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev+nYRRuWlolflfl" crossorigin="anonymous">
<title>Loading</title>
<link href="css/seger.css" rel="stylesheet">
</head>
<body>
<div class="container-fluid d-flex min-vh-100 flex-column p-0">
<nav class="navbar navbar-light bg-light justify-content-between navbar-expand-lg">
<div class="container-fluid">
<a class="navbar-brand" href="?">
<div id="global-loader" style="display: none"><i class="fas fa-spinner fa-spin"></i></div>
</a>
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="?/help">HELP WANTED</a>
</li>
<li class="nav-item">
<a class="nav-link" href="?">Home</a>
</li>
</ul>
</div>
</nav>
<main id="main-container" role="main" class="flex-fill d-flex flex-column">
</main>
<footer class="pl-5 pt-3">
<div class="row p-0 m-0">
<div class="col-12 text-center">
Made with <a href="#" target="_blank" rel="nofollow">Seger</a>
</div>
</div>
</footer>
</div>
<script type="text/template" id="tpl-home">
<div class="container section-home">
<div class="row">
<div class="col-12">
<% entries.forEach(entry => { %>
<article>
<a href="<%= entry.url %>" class="post-title">
<h2><%= entry.title %></h2>
</a>
<p class="post-meta">
Posted on <%= seger.formatDate(entry.date) %>
</p>
<div class="post-entry">
<%= entry.preview %>
<a href="<%= entry.url %>" class="post-read-more">Read More</a>
</div>
</article>
<% }) %>
</div>
</div>
</div>
</script>
<script type="text/template" id="tpl-post">
<div>
<header>
<div class="container-md">
<div class="row text-center mt-3">
<h1><%= entry.title %></h1>
</div>
</div>
</header>
<div class="container-md">
<article>
<%= entry.html %>
</article>
<div class="row">
<div class="col-6">
<% if (relatedPosts.previous) { %>
<a href="<%= relatedPosts.previous.url %>" class="btn btn-link bi bi-arrow-left"> Previous entry: <%= relatedPosts.previous.title %></a>
<% } %>
</div>
<div class="col-6">
<% if (relatedPosts.next) { %>
<a href="<%= relatedPosts.next.url %>" class="float-end btn btn-link bi bi-arrow-right"> Next entry: <%= relatedPosts.next.title %></a>
<% } %>
</div>
</div>
<section class="social-share-section text-center">
<a href="https://twitter.com/intent/tweet?text=<%= encodeURIComponent(entry.title) %>&amp;url=<%= encodeURIComponent(location.href) %>" class="btn bi-twitter" title="Share on Twitter" target="_blank"></a>
<a href="https://www.facebook.com/sharer/sharer.php?u=<%= encodeURIComponent(location.href) %>" class="btn bi-facebook" title="Share on Facebook" target="_blank"></a>
<a href="https://www.linkedin.com/shareArticle?mini=true&amp;url=<%= encodeURIComponent(location.href) %>" class="btn bi-linkedin" title="Share on LinkedIn" target="_blank"></a>
<a href="whatsapp://send?text=<%= encodeURIComponent(location.href) %>" data-action="share/whatsapp/share" class="btn bi-whatsapp" title="Share on Whatsapp" target="_blank"></a>
<a href="https://telegram.me/share/url?url=<%= encodeURIComponent(location.href) %>&text=<%= encodeURIComponent(location.title) %>" class="btn bi-telegram" title="Share on Telegram" target="_blank"></a>
<a href="mailto:?subject=<%= encodeURIComponent("look at this blog") %>&amp;body=<%= encodeURIComponent(location.href) %>" class="btn bi-envelope" title="Share on Email" target="_blank"></a>
</section>
</div>
</div>
</script>
<script type="text/template" id="tpl-404">
<div class="container-md text-center align-middle">
<div class="row">
<h2>404 - Page not found</h2>
<p>The requested page could not be found.</p>
<p><a href="?/">Head to homepage</a></p>
</div>
</div>
</script>
<script src="https://cdn.jsdelivr.net/npm/underscore@1.11.0/underscore-min.js"></script>
<script src="js/seger.js"></script>
<script>
window.seger = new Seger();
</script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.4.0/font/bootstrap-icons.css">
</body>
</html>

181
docs/js/editor.js Normal file
View File

@@ -0,0 +1,181 @@
class BlogEditor {
constructor () {
this.loadedInterval = setInterval(() => {
if (typeof Jodit === "undefined") {
return;
}
clearInterval(this.loadedInterval);
this.onReady();
}, 200);
}
onReady () {
this.editor = new Jodit('#editor', {
buttons: [
'bold', 'strikethrough', 'underline', 'italic', 'eraser', '|',
'ul', 'ol', '|',
'outdent', 'indent', '|',
'fontsize', 'brush', 'paragraph', '|',
'image', 'video', 'table', 'link', '|',
'left', 'center', 'right', 'justify', '|',
'undo', 'redo', '|', 'hr', 'source'
]
});
this.dateInput = document.getElementById('date');
this.titleInput = document.getElementById('title');
this.entrySelector = document.getElementById('existing-entry-selector');
this.previewButton = document.getElementById('preview');
this.dateInput.value = this.getTodayDate();
this.setupEntrySelector();
this.setupListeners();
this.applyDraft();
}
setupListeners () {
document.getElementById('blog-editor').addEventListener("submit", (e) => {
e.preventDefault();
this.generateEntry();
});
this.titleInput.addEventListener("change", () => {
this.previewButton.href = "?/post/" + this.slugify(this.titleInput.value);
});
this.previewButton.addEventListener("click", () => {
this.saveDraft();
});
// handle CRTL + S
document.addEventListener("keydown", (e) => {
if (e.keyCode == 83 && (navigator.platform.match("Mac") ? e.metaKey : e.ctrlKey)) {
e.preventDefault();
this.saveDraft();
}
}, false);
}
getTodayDate () {
return new Date().toISOString().slice(0, 16);
}
saveDraft () {
localStorage.setItem("draft", JSON.stringify(this.getCurrentEntry()));
}
setupEntrySelector () {
seger.config.entries.forEach(entry => {
this.addEntryOption(entry);
});
this.entrySelector.addEventListener("change", (e) => {
this.recoverEntry();
});
}
addEntryOption (entry, prepend) {
let option = document.createElement("option");
option.innerText = entry.title;
option.value = entry.slug;
if (prepend) {
this.entrySelector.prepend(option);
} else {
this.entrySelector.appendChild(option);
}
}
recoverEntry () {
if (this.entrySelector.value.length === 0) {
this.applyEntry({
html: '',
title: '',
date: this.getTodayDate(),
});
} else {
seger.getFullEntry(this.entrySelector.value).then(entry => {
this.applyEntry(entry);
});
}
}
applyEntry (entry) {
this.editor.value = entry.html;
this.titleInput.value = entry.title;
this.dateInput.value = entry.date.slice(0, 16);
// trigger change event so preview url is updated
this.titleInput.dispatchEvent(new Event('change'));
}
applyDraft () {
const draft = seger.getDraft();
if (draft === null) {
return;
}
this.applyEntry(draft);
draft.title = "Draft: " + draft.title;
this.addEntryOption(draft, true);
}
// from https://gist.github.com/codeguy/6684588
slugify (value) {
return value
.normalize('NFD') // split an accented letter in the base letter and the acent
.replace(/[\u0300-\u036f]/g, '') // remove all previously split accents
.toLowerCase()
.trim()
.replace(/[^a-z0-9 ]/g, '') // remove all chars not letters, numbers and spaces (to be replaced)
.replace(/\s+/g, '-') // separator
}
generatePreview (text) {
text = text.replace(/(\r\n|\n|\r)/gm, "").replace(/\s+/g,' ').trim();
text = text.replace(/(<([^>]+)>)/gi, "").trim(); // strip html tags
text = text.substr(0, 200);
text += "...";
return text;
}
getCurrentEntry () {
const title = this.titleInput.value;
return {
slug : this.slugify(title),
title : title,
preview : this.generatePreview(this.editor.value),
date : this.dateInput.value + ":00Z",
html : this.editor.value
};
}
generateEntry () {
this.saveDraft();
const htmlContainer = document.getElementById('post-html');
const summaryContainer = document.getElementById('post-summary');
const entry = this.getCurrentEntry();
summaryContainer.innerHTML = JSON.stringify({
slug : entry.slug,
title : entry.title,
preview : entry.preview,
date : entry.date,
}, null, 2);
htmlContainer.innerHTML = entry.html;
document.querySelector('[for="post-html"]').innerText = "posts/" + entry.slug + "/index.html";
document.getElementById('output').classList.remove("d-none");
}
}
window.blogEditor = new BlogEditor();

325
docs/js/seger.js Normal file
View File

@@ -0,0 +1,325 @@
class Seger {
constructor () {
this.mainContainer = document.getElementById('main-container');
this.getConfig().then(() => {
this.setup();
this.goTo(this.getCurrentRoute());
});
}
setup () {
if (typeof this.config.logo === "undefined") {
document.querySelector('.navbar-brand').innerText = this.config.title;
} else {
let img = document.createElement("img");
img.src = this.config.logo;
img.title = this.config.title;
document.querySelector('.navbar-brand').prepend(img);
}
this.setupListeners();
}
goTo (route) {
const splitted = route.split("/");
if (route.length === 0) {
return this.showHome();
} else if (splitted[0] === 'post') {
const slug = splitted[1];
return this.showPost(slug);
} else {
this.showPage(route);
}
}
getLanguage = () => {
return navigator.userLanguage || (navigator.languages && navigator.languages.length && navigator.languages[0]) || navigator.language || navigator.browserLanguage || navigator.systemLanguage || 'en';
}
formatDate(date) {
return (new Date(date)).toLocaleString(this.getLanguage(), {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'});
}
getCurrentRoute () {
return this.getRoute(location.search);
}
getRoute (queryString) {
if (queryString.length === 0) {
return "";
}
const route = (new URLSearchParams(queryString)).entries().next().value;
if (!route[0].startsWith("/") || route[1].length > 0) {
return "";
}
return route[0].substr(1);
}
setupListeners () {
document.addEventListener("click", (e) => {
const a = e.target.closest("a");
// not clicking a local link
if (a == null ||
!a.href.startsWith(location.origin + location.pathname) ||
a.target.length > 0
) {
return;
}
// avoid redirecting but change the url
e.preventDefault();
history.pushState({}, '', a.href);
const route = this.getRoute(new URL(a.href).search);
this.goTo(route);
});
// detect when user goes back/forward through navigation history
window.addEventListener("popstate", (e) => {
this.goTo(this.getCurrentRoute());
});
}
getTemplate(name) {
return document.getElementById("tpl-" + name).innerHTML.trim();
}
parseTemplate(name, data) {
let parsedTemplate = _.template(this.getTemplate(name))(data);
return this.stringToDOM(parsedTemplate);
}
stringToDOM (str) {
let div = document.createElement('div');
div.innerHTML = str;
return div.firstChild;
}
getEntry (slug) {
return new Promise((resolve, reject) => {
const count = this.config.entries.length - 1;
this.config.entries.forEach((entry, i) => {
if (entry.slug === slug) {
return resolve(entry);
}
// requested entry not found
if (i === count) {
reject();
}
});
});
}
getFullEntry (slug) {
return new Promise((resolve, reject) => {
this.getEntry(slug).then((entry) => {
if (typeof entry.html !== 'undefined') {
resolve(entry);
return;
}
fetch('posts/' + slug + "/index.html")
.then(response => response.text())
.then(html => {
html = html.replace(/src="((?!http).+)"/, 'src="posts/' + slug + '/$1"');
return html;
})
.then(html => {
entry.html = html;
resolve(entry);
});
}).catch(() => {
const draft = this.getDraft();
if (draft === null || draft.slug !== slug) {
return reject();
}
resolve(draft);
});
});
}
getDraft () {
let draft = localStorage.getItem("draft");
if (draft === null) {
return null;
}
return JSON.parse(draft);
}
getConfig () {
return fetch('config.json')
.then(response => response.json())
.then(config => {
this.config = config;
this.config.entries.forEach(entry => {
entry.url = "?/post/" + entry.slug;
});
});
}
setTitle (title) {
document.title = title + " - " + this.config.title;
}
getPreviousAndNextPosts (slug) {
let entry;
let related = {
previous: null,
next: null,
};
const count = this.config.entries.length;
for (let i = 0; i < this.config.entries.length; i++) {
entry = this.config.entries[i];
if (entry.slug === slug) {
if (i > 0) {
related.previous = this.config.entries[i - 1];
}
i++;
if (i < count) {
related.next = this.config.entries[i];
}
break;
}
}
return related;
}
showPost (slug) {
this.getFullEntry(slug).then((entry) => {
const relatedPosts = this.getPreviousAndNextPosts(slug);
this.render(this.parseTemplate("post", {entry, relatedPosts}));
this.setTitle(entry.title);
this.setMetas([
{
property: "og:title",
content: entry.title,
},
{
name: "twitter:title",
content: entry.title,
},
{
name: "description",
content: entry.preview,
},
{
name: "twitter:description",
content: entry.preview,
},
{
property: "og:description",
content: entry.preview,
}
]);
}).catch(() => {
this.show404();
});
}
showHome () {
this.setTitle("Home");
this.render(this.parseTemplate("home", {
entries: this.config.entries
}));
}
slugToString (slug) {
slug = slug.charAt(0).toUpperCase() + slug.slice(1);
slug = slug.replace("-", " ");
return slug;
}
showPage (route) {
return fetch(route + '.html')
.then(response => {
if (response.status === 404) {
return null;
}
return response.text();
}).then(html => {
if (html === null) {
return this.show404();
}
this.setTitle(this.slugToString(route));
html = this.stringToDOM(html);
this.render(html);
// appendChild won't load/execute scripts
// so we have to do it manually
html.querySelectorAll("script").forEach(el => {
let newScript = document.createElement("script");
if (el.src.length > 0) {
newScript.src = el.src;
} else {
var inlineScript = document.createTextNode(el.innerText);
newScript.appendChild(inlineScript);
}
document.body.appendChild(newScript);
});
});
}
show404 () {
this.setTitle("Page not found");
this.render(this.parseTemplate("404"));
}
render (html) {
this.setMetas([]);
if (typeof html === "string") {
html = this.stringToDOM(html);
}
this.mainContainer.innerHTML = '';
this.mainContainer.appendChild(html);
window.scrollTo({ top: 0, left: 0, behavior: 'auto' });
}
setMetas (list) {
// remove previous entries
document.querySelectorAll('meta').forEach(el => {
el.remove();
});
list.forEach(meta => {
let tag = document.createElement("meta");
if (typeof meta.name !== "undefined") {
tag.name = meta.name;
}
if (typeof meta.property !== "undefined") {
tag.setAttribute("property", meta.property);
}
if (typeof meta.content !== "undefined") {
tag.content = meta.content;
}
document.head.appendChild(tag);
});
}
}

View File

@@ -0,0 +1,247 @@
<p style="text-align: center;"><strong><a href="https://www.indiedb.com/games/brickcraft/news/devblog-1-studying-the-bricks" rel="nofollow">ENGLISH VERSION</a></strong></p><p style="text-align: center;"><img src="https://i.imgur.com/UUoTVvk.gif" alt="" style="width: 506px; height: 371px;"></p>
<p>Estas navidades quería montar un set de Lego navideño con unos amigos, pero con el tema COVID por en medio no ha sido posible. Así que me ha dado por hacer un juego donde podamos montarlos online.</p>
<p><br>
Pensaba que Lego tendría algo así, pero tras mirarlo sólo he encontrado Lego Worlds, que para nada es la experiencia que busco (minecraft). Aunque también es posible que esté cegato y sí exista.</p>
<p><br>
Tengo la (mala) costumbre de no terminar lo que empiezo, y no veo por qué este caso vaya a ser una excepción. Aún así, la mayoría de las cosas que hago las publico open source, por lo que siempre hay margen para que otro las continúe de quererlo.</p>
<p><img src="https://i.imgur.com/eto9I6s.png" alt="" style="width: 1066px; height: 597px;"></p>
<h2>1. Estudiando los bricks</h2>
<p>
Para saber qué estructura de datos tendrá cada brick, hay que mirar un poco qué tipos hay, su mínima unidad, el vocabulario, etc..<br>
</p><p>No son un fan/AFOL de lego y prueba de ello es que estaba convencido que la mínima unidad era:</p>
<p><img src="https://i.imgur.com/NDRZIOc.png" alt="" style=""><br>
Pero no, resulta que la unidad más pequeña es:</p>
<p><img src="https://i.imgur.com/LcRh55X.png" alt="" style=""><br>
Mientras que los bloques se llaman bricks, los más pequeños se llaman plates.<br>
Con esto ya sabemos que la altura deberá medirse en plates.<br>
</p><p>1 brick equivale a 3 plates:</p>
<p><img src="https://i.imgur.com/Yzs1KAa.png" alt="" style="width: 300px;"><br>
Otra parte importante a tener en cuenta es los studs.<br>
Los studs son los "conectores", la parte cilíndrica que nos permite conectar unos con otros.<br>
</p><p>Los bricks pueden tener studs en múltiples sitios y ángulos:</p>
<p><img src="https://i.imgur.com/y46D2vN.png" alt="" style=""><br>
La variedad de colores está (por suerte) limitada a:</p>
<p><img src="https://i.imgur.com/DQcvTCG.jpg" alt="" style="width: 1174px; height: 685px;"><br>
Para saber si algo así sería publicable he mirado el tema patente y resulta que esta expiró ya hace tiempo.</p>
<p><img src="https://i.imgur.com/A5hTiC0.png" alt="" style="width: 300px;"><br>
Lo que significa que hay vía libre siempre y cuando no se ponga la marca en ningún sitio.<br>
También hay que tener en cuenta que sólo la patente para los bricks ha expirado. La de sus figuras aún está vigente.</p><p><img src="https://i.imgur.com/pEYfjKy.png" alt="" style="width: 642px; height: 377px;"><br><a href="https://theconversation.com/how-lego-legally-locked-in-the-iconic-status-of-its-mini-figures-43489" rel="nofollow">https://theconversation.com/how-lego-legally-locked-in-the-iconic-status-of-its-mini-figures-43489</a><br>
</p>
<h2>2. Tecnología y definición del proyecto</h2>
<p>
Lo único que he tocado para hacer alguna que otra ñapa es Unity, de modo que el engine es fácil.<br>
Para la parte multiplayer (si es que llego a hacerla), la librería oficial de Unity está muerta, por lo que optaré por Mirror.<br>
Es una librería open source mantenida por gente competente.<br>
</p><p>En cuanto a la definición del proyecto, cuanto más acotado lo haga, mayor será la probabilidad de que lo termine.<br>
La idea es poder construir en primera persona como en minecraft, pero con bricks. Sín enemigos, ni un mundo procedural.<br>
- FPV (First Person View) controller<br>
- Sistema de inventario<br>
- Sistema de construcción<br>
- Multiplayer (si llego a terminar todo lo anterior)<br>
</p>
<h2>3. Primeros pasos</h2>
<p>
Antes que nada, necesito tener los modelos 3d de los bricks. Tras buscar, parece que <a href="https://www.ldraw.org/" rel="nofollow">LLDraw</a> es el repositorio perfecto, pero los ficheros son .dat. Descargué sín éxito LLD View para intentar convertirlos a un formato útil estilo FBX o OBJ. Toca buscar más.....<br>
Finalmente encuentro <a href="https://www.mecabricks.com/" rel="nofollow">https://www.mecabricks.com/</a>, una web que permite crear lo que quieras usando bricks desde el navegador y... ¡Tiene opción de exportar en DAE o OBJ!<br>
Con esto ya tenemos una fuente desde la que ir descargando brick a brick.<br>
</p><p>Para empezar sólo he pillado los que he considerado básicos, 1 plate de 1x1, 1 brick de 1x1, 1 brick de 2x2 y un brick con múltiples studs en distintos ángulos.</p>
<p><img src="https://i.imgur.com/jlqf3TM.png" alt="" style="width: 581px; height: 387px;"><br>
Toca definir el modelo con lo aprendido en el primer punto.</p>
<p><img src="https://i.imgur.com/ti1qbIM.png" alt="" style=""><br>
</p><p>Ahora se viene la parte chunga, posicionar nuevos bloques.<br>
</p><p>En Minecraft colocar un nuevo bloque es muy fácil. Todo el mundo es una gran matriz tridimensional y todos los objetos tienen la misma forma (cubo), con lo que lo único que debes hacer es mirar si el espacio en esa matriz está libre.</p>
<p><img src="https://i.imgur.com/T9DGuMC.png" alt="" style="width: 748px; height: 426px;"><br>
¿Pero con los bricks?... Menudo marronazo. Podríamos hacer una matriz usando los plates como unidad, pero tendríamos que calcular los espacios que ocupan bricks como:</p>
<p><img src="https://i.imgur.com/2UCfflu.png" alt="" style="width: 807px; height: 401px;"><br>
Y no me apetece. Así que optaremos por algo más cutre.<br>
</p><p>Unity tiene un componente llamado "Collider" que te permite trabajar con las físicas. Es decir, hacer que un objeto ocupe espacio o bien actuar de hitbox, detectar cuando otro objeto esta dentro de él (trigger).</p>
<p><img src="https://i.stack.imgur.com/e949n.gif" alt="" style="width: 964px; height: 572px;"><br>
En este ejemplo, los cubos con bordes verdes son los colliders.<br>
</p><p>Haremos que haya preview de dónde aparecerá el nuevo bloque y a este preview le pondremos un collider en modo trigger.<br>
Si el jugador lo pone en un sitio que toca/hay otro brick, lo sabremos y no le dejaremos.<br>
</p><p>Pero antes que eso, tenemos que saber exactamente hacia dónde está mirando el jugador. De nuevo en minecraft es fácil, haces raycast y al momento sabes qué bloque y qué cara de este está mirando.<br>
Para el que no lo sepa, hacer raycasting es esto:</p>
<p><img src="https://i.imgur.com/EIAI59N.png" alt="" style="width: 1062px; height: 591px;"><br>
Mandar un "haz" hacia una dirección para detectar con qué se encuentra por el camino.<br>
</p><p>¿Pero con bricks?... No toda la superfície de un brick es "útil", realmente necesitamos saber si está mirando a un stud.<br>
Si hasta ahora me habéis seguido el rollo habréis pensado "pues le metes un collider a cada stud y listo". Pues sí, es una opción:</p>
<p><img src="https://i.imgur.com/h3fu88Q.png" alt="" style="width: 705px; height: 620px;"><br>
Sín embargo, va a ser que no. Cada componente consume recursos, hay que ratear en todo.</p>
<p><img src="https://i.imgur.com/dKxSr8B.png" alt="" style="width: 683px; height: 717px;"><br>
¡Con esto estás en las mismas, no sabes a cuál de ellos está mirando!<br>
Eso me temo, esa parte la tendremos que calcular. Los studs tienen todos la misma superfície, por lo que de si sabemos en qué coordenada ha dado el raycast, podremos saber a qué stud pertenece.</p>
<p><img src="https://i.imgur.com/xQ4Rb5I.png" alt="" style="width: 629px; height: 631px;"><br>
(Imagina que el punto rojo es donde mira el jugador)<br>
</p><p>¿No hay una forma más fácil de saberlo? Seguro que sí, pero yo no tengo ni idea.<br>
</p><p>Ahora ya sabemos dónde colocar el bloque y si está chocando con algo o no.<br>
¿Pues ya lo tenemos no? Caaaaaaasi. Si hago la prueba veréis por qué:</p>
<p><img src="https://i.imgur.com/3iarFAh.png" alt="" style=""><br>
De nuevo falla la colocación, pero porque no sabemos cómo lo quiere el jugador. En Minecraft sólo hay una manera, pero aquí hay múltiples combinaciones posibles.</p>
<p><img src="https://i.imgur.com/jui8bmZ.png" alt="" style="width: 456px; height: 114px;"><br>
¿Y cómo arreglamos? Toca hacer un botoncito que permita ir rotando la pieza hasta que quede en la posición deseada.<br>
Obviamente podemos intentar hacer el constructor algo "listo". Si el jugador está intentando colocar una pieza de 2x2 encima de otra pieza de 2x2, es probable que quiera que quede así:</p>
<p><img src="https://i.imgur.com/xJ8AM9b.png" alt="" style=""><br>
De modo que añadimos un check para que en los casos en los que el número de studs coincida, copie la posición cambiando únicamente la altura.</p>
<p><img src="https://s8.gifyu.com/images/324a01c789706951c.gif" alt="" style="width: 594px; height: 410px;"><br>
El resultado del sistema de rotación me temo es bastante chuchurrio. Se combina el hecho de que no sé que está ocurriendo + me da pereza entenderlo.<br>
Supongo que tiene que ver con cómo se realiza la rotación de la pieza. Seguramente se haga en base al centro de la misma y no en base al eje que coincide con el stud. De ser así, no es algo que sepa arreglar, no creo que pueda cambiar el eje de rotación temporalmente, aunque a saber.</p>
<p><img src="https://i.imgur.com/Ldz6yWU.png" alt="" style="width: 624px; height: 431px;"><br>
En rojo el eje de rotación deseado (tener el stud como base). En negro el punto de rotación actual.<br>
Con esto ya podemos intentar construir algo:</p>
<p><img src="https://i.imgur.com/UUoTVvk.gif" alt="" style="width: 583px; height: 428px;"></p>
<p><img src="https://i.imgur.com/UuH1sfN.png" alt="" style="width: 588px; height: 402px;"><br>
</p>
<h2>4. El maldito sistema de inventario</h2>
<p>
Como de costumbre subestimé la complejidad que conllevaría hacerlo. Pensaba que serían 10 líneas mal contadas y andando, pero no. Resulta que tiene su miga.<br>
</p><p>Siempre necesito empezar por la parte visual, no porque sea bueno en ello, sino porque a diferencia de picar código, a poco que hagas ya se aprecia un cambio.<br>
Haciendo uso de mi GRAN creatividad, he dispuesto el siguiente diseño:</p>
<p><img src="https://i.imgur.com/cehyvTx.png" alt="" style="width: 827px; height: 535px;"><br>
Ahora toca volver al código.<br>
</p><p>Empezaremos con el modelo de datos. ¿Qué vamos a tener que guardar? El id del item, la cantidad y su estado.</p>
<p><img src="https://i.imgur.com/ZXofzNW.png" alt="" style="width: 300px;"><br>
</p><p>¡Hecho! ¿Verdad? Pues no.. Nos dejamos el slot en el que se encuentra dentro del inventario. Y esa mierdecilla es la que añade la complicación, porque caaada vez que se añada un item al inventario, tendremos que buscarle un slot disponible (siempre y cuando no lo tengamos ya). Y dentro de lo posible, habrá que priorizar los slots de acceso rápido, los de la barrita inferior vamos.</p>
<p><img src="https://i.imgur.com/gYr6a05.png" alt="" style="width: 300px;"><br>
Venga, le añadimos el maldito slot.<br>
</p><p>¿Qué tendrá que hacer nuestro inventario?/¿Qué debemos tener en cuenta?<br>
- Añadir y quitar items<br>
- Cuando se añadan, que siempre de prioridad a llenar la barra inferior<br>
- Cuando se añaden o quiten items tiene que actualizarse la UI<br>
- En el panel de inventario hay que poder cambiar vía drag & drop los items de slot.<br>
- En el panel de inventario, si se arrastra un item fuera de la ventana, tiene que descartarlo.<br>
- En la barra inferior siempre tiene que haber un slot seleccionado<br>
- Hay que poder cambiar el slot seleccionado de la barra inferior<br>
- Cuando se esté en el panel de inventario, hay que congelar la cámara del jugador<br>
- Todos los bricks necesitan un icono de preview<br>
</p><p>Cachís, esto es más largo de lo que creía.<br>
</p><p>Sólo con la función de añadir item ya he superado las 10 líneas mal contadas que esperaba escribir xD</p>
<p><img src="https://i.imgur.com/YlQt1t7.png" alt="" style="width: 724px; height: 804px;"><br>
Como se puede observar, al añadir un item, lo primero que hago es iterar sobre todo el inventario, para hacer 2 cosas:<br>
Mirar si ya tengo ese item, de modo que sólo tenga que aumentar la cantidad y apuntarme los slots ocupados.<br>
</p><p>En caso de que no encuentre el item, sabré al menos qué slots no puedo llenar. Tras hacer esto, como sé seguro que hay que ocupar un nuevo slot, miro si el inventario está lleno. En caso de que no sea así, continuo, y me dedico a iterar sobre los slots que quedan libres para ver si hay alguno en la barra inferior (el inventario es de 36 slots, así que miro si el slot libre es > 27).<br>
Si eso no es posible, pues pillo el primero disponible y que le den. Ah bueno sí, y actualizo la UI.<br>
</p><p>A continuación hacemos la parte de mover items entre slots. Para ello nos valdremos del sistema drag&drop que Unity tiene, mediante el uso de IBeginDragHandler, IDragHandler, IEndDragHandler.</p>
<p><img src="https://i.imgur.com/fe2SbmZ.gif" alt="" style="width: 742px; height: 586px;"><br>
Puede parece que haya movido el icono de sitio pero en realidad no lo estoy haciendo.<br>
En vez de generar dinámicamente los slots ocupados, los tengo todos ya generados de forma estático, estén llenos o vacíos.<br>
Después de arrastrar, cuando se suelta el icono, lo que hago es mirar qué slot movías y dónde lo has dejado. Vuelvo a dejar el slot que has movido en su sitio y vía código intercambio el contenido de los 2 slots (en caso de que tengan algo).</p><p><br>
Pasamos a lo de tener un slot seleccionado. Esta parte me resulta algo problemática porque estamos acostumbrados a usar la rueda del ratón para movernos entre nuestras armas/inventario. Pero yo he asignado la función de rotar los bricks a la rueda. Me parecía natural y veo que pueda distinguir cuando quieres rotar un cubo a cuando quieres cambiar de slot de modo que los 2 puedan usar la rueda.<br>
La única manera sería si el proceso de añadir un brick fuera:<br>
1. Apuntas al stud y haces clic izquierdo.<br>
2. Esto hace que aparezca el preview, pudiendo ahora usar la ruedecita para rotarlo.<br>
3. Haces clic izquierdo para confirmar la construcción.</p><p><br>
Me parece engorroso/más lento. Y como además en el primer paso no tienes el preview, es posible que no entiendas que a diferencia de minecraft, puedes apuntar exactamente en qué stud quieres ponerlo.<br>
Aunque por otro lado..<br>
1. Esto me permitiría usar la ruedecita para moverme entre el inventario, pues sólo habría un momento en el que el sistema de construcción lo usa y sabría si está en ese paso o no.<br>
2. También ganaría que podrías ir por el mapa con un brick seleccionado en el inventario sín que el preview te apareciera allá donde miras.<br>
3. No tendría que estar haciendo raycasting en cada frame, sólo en cuanto el jugador hiciera clic izquierdo, porque sabría que quiere construir. [Aunque ahora que lo pienso, esto también puedo evitarlo ahora, mirando si el item que tiene seleccionado es un brick].</p><p><br>
MMMMMMMmmm. Puede que al final haya más cosas a favor que en contra.</p><p><br>
Habrá que rumiarlo bien.</p>
<p><img src="https://statics.memondo.com/p/99/cfs/2016/09/CF_44573_301b77648ed846cb989e7054fc274524_salvajes_dfsfdf.gif" alt="" style="width: 537px; height: 303px;"></p>
<p>
De momento esto es todo.<br>
Como he dicho al principio, todo lo que hago suele ser open source. Así que dejo el repositorio por aquí con el progreso actual.</p>
<p style="text-align: center;">
<a href="https://github.com/tetreum/brickcraft" rel="nofollow">
<img src="https://i.imgur.com/zRrABr3.png" alt="" style=""><br>
https://github.com/tetreum/brickcraft</a>
</p>