Refactored fonts, added start of articles pages.
This commit is contained in:
parent
c5e2036cbd
commit
1aff5368c1
|
@ -0,0 +1,82 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Andrew's Articles</title>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="description" content="Articles written by Andrew Lalis.">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="stylesheet" href="styles/font.css" type="text/css">
|
||||||
|
|
||||||
|
<!-- CSS class to hide elements if JS is not enabled. -->
|
||||||
|
<noscript><style>.jsonly{display: none !important;}</style></noscript>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// An inline script that manages the site's color theme.
|
||||||
|
const THEMES = ["light", "dark"];
|
||||||
|
|
||||||
|
function getPreferredTheme() {
|
||||||
|
const storedTheme = localStorage.getItem("theme");
|
||||||
|
if (storedTheme !== null && THEMES.includes(storedTheme)) return storedTheme;
|
||||||
|
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||||
|
return "dark";
|
||||||
|
}
|
||||||
|
return "light";
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPreferredTheme(theme) {
|
||||||
|
document.documentElement.className = theme;
|
||||||
|
localStorage.setItem("theme", theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
setPreferredTheme(getPreferredTheme());
|
||||||
|
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", event => {
|
||||||
|
const newTheme = event.matches ? "dark" : "light";
|
||||||
|
setPreferredTheme(newTheme);
|
||||||
|
});
|
||||||
|
document.addEventListener("DOMContentLoaded", event => {
|
||||||
|
const themeToggleButton = document.getElementById("themeToggleButton");
|
||||||
|
themeToggleButton.onclick = clickEvent => {
|
||||||
|
const currentTheme = getPreferredTheme();
|
||||||
|
const idx = THEMES.indexOf(currentTheme);
|
||||||
|
const nextIdx = idx === THEMES.length - 1 ? 0 : idx + 1;
|
||||||
|
setPreferredTheme(THEMES[nextIdx]);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="styles/style.css" type="text/css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<header class="page-header">
|
||||||
|
<h1>Andrew's Articles</h1>
|
||||||
|
<nav>
|
||||||
|
<div>
|
||||||
|
<a href="index.html">Home</a>
|
||||||
|
<a class="page-header-selected" href="articles.html">Articles</a>
|
||||||
|
<a href="projects.html">Projects</a>
|
||||||
|
<a href="training.html">Training</a>
|
||||||
|
<a href="contact.html">Contact</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="https://github.com/andrewlalis">GitHub</a>
|
||||||
|
<a href="https://www.linkedin.com/in/andrew-lalis/">LinkedIn</a>
|
||||||
|
<a href="https://www.youtube.com/channel/UC9X4mx6-ObPUB6-ud2IGAFQ">YouTube</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<button id="themeToggleButton" class="jsonly">Change Color Theme</button>
|
||||||
|
<hr>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<article>
|
||||||
|
<h2>About</h2>
|
||||||
|
<p>
|
||||||
|
Although I don't generally write much, sometimes I'll find something interesting, or something that I notice a significant number of people could benefit from reading about. In that case, it'll most likely end up on this page as an article.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
|
@ -0,0 +1,271 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>A Beginner's Guide to Searching With Lucene</title>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta name="description" content="A simple, easy-to-follow guide that walks you through creating your first index, and searching through it with queries."/>
|
||||||
|
<link rel="stylesheet" href="../../css/style.css" type="text/css"/>
|
||||||
|
<link rel="stylesheet" href="../../css/prism.css" type="text/css"/>
|
||||||
|
<link rel="stylesheet" href="../../css/blog-article.css" type="text/css"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<article>
|
||||||
|
<header>
|
||||||
|
<h1>A Beginner's Guide to Searching With Lucene</h1>
|
||||||
|
<p>
|
||||||
|
<em>Written on <time>6 February, 2023</time>, by Andrew Lalis.</em>
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
<section>
|
||||||
|
<p>
|
||||||
|
Nowadays, if you want to build the next fancy new web app, chances are pretty good that you'll need a search bar in it, and for that, you've probably heard of <a class="link_external" target="_blank" href="https://elastic.co">ElasticSearch</a>, or some other fancy, all-in-one solution. However, in this article, I'd like to try and convince you that you don't need any of that, and instead, you can brew up your own homemade search feature using <a class="link_external" target="_blank" href="https://lucene.apache.org/">Apache Lucene</a>.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Hopefully you'll be surprised by how easy it is.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h3>The Use Case</h3>
|
||||||
|
<p>
|
||||||
|
Before we dive into the code, it's important to make sure that you actually need an indexing and searching tool that goes beyond simple SQL queries.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If you can answer "yes" to any of these questions, then continue right along:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>I want to search over multiple different types of entities.</li>
|
||||||
|
<li>I want to prioritize matching certain fields from entities over other fields. (For example, a user's name should be more important than their nickname.)</li>
|
||||||
|
<li>I'm okay with search results being eventually consistent (that is, it might take a moment for new data to appear in results).</li>
|
||||||
|
<li>I want to search for results that match a wildcard search. (For example, <em>"find all animals whose name matches <code>tig*</code>.</em></li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h3>Indexing and Searching Basics</h3>
|
||||||
|
<p>
|
||||||
|
No matter what searching solution you end up choosing, they all generally follow the same approach:
|
||||||
|
</p>
|
||||||
|
<ol>
|
||||||
|
<li>Ingest data and produce an index.</li>
|
||||||
|
<li>Search for data quickly using the index.</li>
|
||||||
|
</ol>
|
||||||
|
<p>
|
||||||
|
In most situations, <em>ingesting data</em> roughly translates to scraping content from a database or message queue, or even CSV content. The contents of each entity are analyzed and the important bits are extracted and stored in a compressed format that's optimized for high-speed searching. The exact implementation depends on what sort of solution you choose, but a lot of databases use a sort of red-black tree structure.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Searching over your index involves parsing a user's query (and sanitizing it, if necessary), and then constructing a well-formed query that's accepted by your searching solution, possibly with different weights or criteria applied to different fields.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
This is no different for Lucene, and in this guide, we'll go through how to create an index and search through it.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h3>Setting Up a New Project</h3>
|
||||||
|
<p>
|
||||||
|
In this guide, I'll be creating a small Java program for searching over a huge set of airports which is available for free here: <a class="link_external" target="_blank" href="https://ourairports.com/data/">https://ourairports.com/data/</a>. The full source code for this project is <a class="link_external" target="_blank" href="https://github.com/andrewlalis/SampleLuceneSearch">available on GitHub</a>, if you'd like to take a look.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
I'll be using Maven as the build tool of choice, but feel free to use whatever you'd like.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
We start by creating a new project, and add the <a class="link_external" target="_blank" href="https://mvnrepository.com/artifact/org.apache.lucene/lucene-core">apache-lucene</a> dependency, and the <a class="link_external" target="_blank" href="https://commons.apache.org/proper/commons-csv/">Apache Commons CSV</a> library for parsing the CSV dataset.
|
||||||
|
</p>
|
||||||
|
<figure>
|
||||||
|
<pre><code class="language-xml">
|
||||||
|
<dependencies>
|
||||||
|
<!-- https://mvnrepository.com/artifact/org.apache.lucene/lucene-core -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.lucene</groupId>
|
||||||
|
<artifactId>lucene-core</artifactId>
|
||||||
|
<version>9.5.0</version>
|
||||||
|
</dependency>
|
||||||
|
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-csv -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.commons</groupId>
|
||||||
|
<artifactId>commons-csv</artifactId>
|
||||||
|
<version>1.10.0</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</code></pre>
|
||||||
|
</figure>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h3>Parsing the Data</h3>
|
||||||
|
<p>
|
||||||
|
First of all, we need to parse the CSV data into a programming construct that we can use elsewhere in our code. In this case, I've defined the <code>Airport</code> record like so:
|
||||||
|
</p>
|
||||||
|
<figure>
|
||||||
|
<pre><code class="language-java">
|
||||||
|
public record Airport(
|
||||||
|
long id,
|
||||||
|
String ident,
|
||||||
|
String type,
|
||||||
|
String name,
|
||||||
|
double latitude,
|
||||||
|
double longitude,
|
||||||
|
Optional<Integer> elevationFt,
|
||||||
|
String continent,
|
||||||
|
String isoCountry,
|
||||||
|
String isoRegion,
|
||||||
|
String municipality,
|
||||||
|
boolean scheduledService,
|
||||||
|
Optional<String> gpsCode,
|
||||||
|
Optional<String> iataCode,
|
||||||
|
Optional<String> localCode,
|
||||||
|
Optional<String> homeLink,
|
||||||
|
Optional<String> wikipediaLink,
|
||||||
|
Optional<String> keywords
|
||||||
|
) {}
|
||||||
|
</code></pre>
|
||||||
|
</figure>
|
||||||
|
<p>
|
||||||
|
And a simple <code>AirportParser</code> class that just reads in a CSV file and returns a <code>List<Airport></code> (Check the source code to see how I did that).
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Now that we've got our list of entities, we can build an index from them.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h3>Indexing</h3>
|
||||||
|
<p>
|
||||||
|
In order to efficiently search over a massive set of data, we need to prepare a special set of index files that Lucene can read during searches. To do that, we need to create a new directory for the index to live in, construct a new <a class="link_external" target="_blank" href="https://javadoc.io/doc/org.apache.lucene/lucene-core/latest/org/apache/lucene/index/IndexWriter.html">IndexWriter</a>, and create a <a class="link_external" target="_blank" href="https://javadoc.io/doc/org.apache.lucene/lucene-core/latest/org/apache/lucene/document/Document.html">Document</a> for each airport we're indexing.
|
||||||
|
</p>
|
||||||
|
<figure>
|
||||||
|
<pre><code class="language-java">
|
||||||
|
public static void buildIndex(List<Airport> airports) throws IOException {
|
||||||
|
Path indexDir = Path.of("airports-index");
|
||||||
|
// We use a try-with-resources block to prepare the components needed for writing the index.
|
||||||
|
try (
|
||||||
|
Analyzer analyzer = new StandardAnalyzer();
|
||||||
|
Directory luceneDir = FSDirectory.open(indexDir)
|
||||||
|
) {
|
||||||
|
IndexWriterConfig config = new IndexWriterConfig(analyzer);
|
||||||
|
config.setOpenMode(IndexWriterConfig.OpenMode.CREATE);
|
||||||
|
IndexWriter indexWriter = new IndexWriter(luceneDir, config);
|
||||||
|
for (var airport : airports) {
|
||||||
|
// Create a new document for each airport.
|
||||||
|
Document doc = new Document();
|
||||||
|
doc.add(new StoredField("id", airport.id()));
|
||||||
|
doc.add(new TextField("ident", airport.ident(), Field.Store.YES));
|
||||||
|
doc.add(new TextField("type", airport.type(), Field.Store.YES));
|
||||||
|
doc.add(new TextField("name", airport.name(), Field.Store.YES));
|
||||||
|
doc.add(new TextField("continent", airport.continent(), Field.Store.YES));
|
||||||
|
doc.add(new TextField("isoCountry", airport.isoCountry(), Field.Store.YES));
|
||||||
|
doc.add(new TextField("municipality", airport.municipality(), Field.Store.YES));
|
||||||
|
doc.add(new IntPoint("elevationFt", airport.elevationFt().orElse(0)));
|
||||||
|
doc.add(new StoredField("elevationFt", airport.elevationFt().orElse(0)));
|
||||||
|
if (airport.wikipediaLink().isPresent()) {
|
||||||
|
doc.add(new StoredField("wikipediaLink", airport.wikipediaLink().get()));
|
||||||
|
}
|
||||||
|
// And add it to the writer.
|
||||||
|
indexWriter.addDocument(doc);
|
||||||
|
}
|
||||||
|
indexWriter.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</code></pre>
|
||||||
|
<figcaption>Note that some of the airport's properties are <code>Optional</code>, so we need to be a little careful to not end up with unexpected null values in our documents.</figcaption>
|
||||||
|
</figure>
|
||||||
|
<p>
|
||||||
|
An important takeaway here is the construction of the <code>Document</code>. There are a variety of fields that you could add to your document, which have different effects on the search.
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li><em>StoredFields</em> are fields that just store plain data, but can't be searched on. In the above code, we store the id and wikipedia link, since they might be nice to have when fetching results, but nobody is going to want to search for airports by our internal id.</li>
|
||||||
|
<li><em>TextFields</em> are fields that allow for a full-text search of its value. This is generally the most popular "searchable" field type. It also allows us to specify whether or not we want to store its value, just like with a StoredField. In our case, we do want to store all our fields.</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
For more information about the types of fields that you can use, <a class="link_external" target="_blank" href="https://javadoc.io/doc/org.apache.lucene/lucene-core/latest/org/apache/lucene/index/package-summary.html">check the Lucene documentation</a>. It's very well-written.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Also important to note is that once a document is added, it's staying in the index until either the index is removed or overwritten, or the document is deleted through another IndexWriter method. I'd suggest reading the documentation if you'd like to learn more about how to dynamically update a <em>living</em> index that grows with your data. But for 95% of use cases, regenerating the search index occasionally is just fine.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h3>Searching</h3>
|
||||||
|
<p>
|
||||||
|
Now that we've built an index from our dataset, we can search over it to find the most relevant results for a user's query.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The following code might look a bit daunting, but I've added some comments to explain what's going on, and I'll walk you through the process below.
|
||||||
|
</p>
|
||||||
|
<figure>
|
||||||
|
<pre><code class="language-java">
|
||||||
|
public static List<String> searchAirports(String rawQuery) {
|
||||||
|
Path indexDir = Path.of("airports-index");
|
||||||
|
// If the query is empty or there's no index, quit right away.
|
||||||
|
if (rawQuery == null || rawQuery.isBlank() || Files.notExists(indexDir)) return new ArrayList<>();
|
||||||
|
|
||||||
|
// Prepare a weight for each of the fields we want to search on.
|
||||||
|
Map<String, Float> fieldWeights = Map.of(
|
||||||
|
"name", 3f,
|
||||||
|
"municipality", 2f,
|
||||||
|
"ident", 2f,
|
||||||
|
"type", 1f,
|
||||||
|
"continent", 0.25f
|
||||||
|
);
|
||||||
|
|
||||||
|
// Build a boolean query made up of "boosted" wildcard term queries, that'll match any term.
|
||||||
|
BooleanQuery.Builder queryBuilder = new BooleanQuery.Builder();
|
||||||
|
String[] terms = rawQuery.toLowerCase().split("\\s+");
|
||||||
|
for (String term : terms) {
|
||||||
|
// Make the term into a wildcard term, where we match any field value starting with the given text.
|
||||||
|
// For example, "airp*" will match "airport" and "airplane", but not "airshow".
|
||||||
|
// This is usually the natural way in which people like to search.
|
||||||
|
String wildcardTerm = term + "*";
|
||||||
|
for (var entry : fieldWeights.entrySet()) {
|
||||||
|
String fieldName = entry.getKey();
|
||||||
|
float weight = entry.getValue();
|
||||||
|
Query baseQuery = new WildcardQuery(new Term(fieldName, wildcardTerm));
|
||||||
|
queryBuilder.add(new BoostQuery(baseQuery, weight), BooleanClause.Occur.SHOULD);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Query query = queryBuilder.build();
|
||||||
|
|
||||||
|
// Use the query we built to fetch up to 10 results.
|
||||||
|
try (var reader = DirectoryReader.open(FSDirectory.open(indexDir))) {
|
||||||
|
IndexSearcher searcher = new IndexSearcher(reader);
|
||||||
|
List<String> results = new ArrayList<>(10);
|
||||||
|
TopDocs topDocs = searcher.search(query, 10, Sort.RELEVANCE, false);
|
||||||
|
for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
|
||||||
|
Document doc = searcher.storedFields().document(scoreDoc.doc);
|
||||||
|
results.add(doc.get("name"));
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
} catch (IOException e) {
|
||||||
|
System.err.println("Failed to search index.");
|
||||||
|
e.printStackTrace();
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</code></pre>
|
||||||
|
</figure>
|
||||||
|
<ol>
|
||||||
|
<li>We check to make sure that the user's query is legitimate. If it's just empty or null, we can exit right away and return an empty result.</li>
|
||||||
|
<li>Since we want to make some fields have a greater effect than others, we prepare a mapping that specifies a <em>weight</em> for each field.</li>
|
||||||
|
<li>In Lucene, the <code>Query</code> object is passed to an index searcher to do the searching. But first, we need to build such a query. In our case, we want to match each term the user enters against any of the fields we've added a weight for. By using a BooleanQuery, we can construct this as a big OR clause, where each term is a wildcard query that's <em>boosted</em> by the weight of the field it applies to.</li>
|
||||||
|
<li>Finally, we open up a DirectoryReader on the index directory, create an IndexSearcher, and get our results. The searcher produces a <code>TopDocs</code> object that has a <code>scoreDocs</code> property containing the list of document ids that appear in the results. We can use the searcher to lookup the stored fields of each document in the result set, and in this case, we just fetch the <em>name</em> of the airport.</li>
|
||||||
|
</ol>
|
||||||
|
<p>
|
||||||
|
That's it! In my sample project, the whole Lucene implementation for indexing and searching, <em>including imports and comments</em>, is less than 150 lines of pure Java! It's so simple that it can just be tucked away into a single class.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Now, with your newfound knowledge, go forth and build advanced search features into your apps, and be content that you've built your solution from the ground up, <em>without</em> reinventing the wheel or getting roped into a complex cloud solution.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Once again, my sample code is <a class="link_external" target="_blank" href="https://github.com/andrewlalis/SampleLuceneSearch">available on GitHub here</a>.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
<a href="../blog.html">Back to Dev Discussions</a>
|
||||||
|
</article>
|
||||||
|
<script src="../../js/prism.js"></script>
|
||||||
|
<script src="../../js/themes.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,139 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>The D Programming Language: Does it have a future?</title>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta name="description" content="Discussion around the D programming language and its potential and future as a general language."/>
|
||||||
|
<link rel="stylesheet" href="../../css/style.css" type="text/css"/>
|
||||||
|
<link rel="stylesheet" href="../../css/prism.css" type="text/css"/>
|
||||||
|
<link rel="stylesheet" href="../../css/blog-article.css" type="text/css"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<article>
|
||||||
|
<header>
|
||||||
|
<h1>The D Programming Language: Does it have a future?</h1>
|
||||||
|
<p>
|
||||||
|
<em>Written on <time>March 26, 2022</time>, by Andrew Lalis.</em>
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
<section>
|
||||||
|
If you've spent any time around me in programming forums or Discord channels, you might be aware of my somewhat
|
||||||
|
zealous nature regarding the <a href="https://dlang.org/">D programming language</a> and its potential to be the
|
||||||
|
<em>next great language</em>. In this article, I'd like to dive into the various reasons I have for taking such
|
||||||
|
a radical stance, and contrast those ideals with some harsh realities.
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h3>The Potential</h3>
|
||||||
|
<p>
|
||||||
|
Because it was designed many years after the advent of most of the main programming languages that form the
|
||||||
|
backbone of our software (C, C++, Java, Python, etc.), D has the benefit of 20/20 hindsight that it uses to
|
||||||
|
improve over some quite glaring flaws in earlier languages.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
From our past, we've learned a few key things which contribute to great languages:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Expressiveness</strong>: it should be very easy to express complex structures and processes.</li>
|
||||||
|
<li><strong>Abstraction</strong>: programmers should be able to create abstractions without significant extra cost.</li>
|
||||||
|
<li><strong>Ease of use</strong>: it should be really easy for new programmers to get started, and for pros to do things quickly.</li>
|
||||||
|
<li><strong>Performance</strong>: programs written in the language should be reasonably efficient and fast.</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
Of the other languages I've mentioned, each of those easily achieves a few of these requirements: C++
|
||||||
|
is expressive and allows efficient abstraction, and it's very fast. However, it's generally accepted that
|
||||||
|
the learning C++ is quite difficult. Again, Python can arguably meet the first three requirements, but fails
|
||||||
|
to provide satisfactory performance for many applications.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
I think that D is a language which addresses all of these requirements in a modest, satisfactory approach.
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>D allows the same level of expressiveness as C++, while offering the convenience of Java when you don't want that.</li>
|
||||||
|
<li>Abstraction in D is as simple as in Java.</li>
|
||||||
|
<li>D is relatively easy to learn, because of its simple module structure and automatic memory management by default.</li>
|
||||||
|
<li>Because D compiles to machine code, it is inherently open to all sorts of optimizations from LLVM and GCC compiler backends, earning it very good performance that's faster than Java but just shy of C++ in most cases.</li>
|
||||||
|
</ul>
|
||||||
|
<figure>
|
||||||
|
<pre><code class="language-d">
|
||||||
|
import std.stdio, std.array, std.algorithm;
|
||||||
|
void main() {
|
||||||
|
stdin
|
||||||
|
.byLineCopy
|
||||||
|
.array
|
||||||
|
.sort!((a, b) => a > b) // descending order
|
||||||
|
.each!writeln;
|
||||||
|
}
|
||||||
|
</code></pre>
|
||||||
|
<figcaption>This program sorts lines of input from <code>stdin</code> in alphabetical order and prints them.</figcaption>
|
||||||
|
</figure>
|
||||||
|
<figure>
|
||||||
|
<pre><code class="language-java">
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
class Main {
|
||||||
|
public static void main(String[] args) throws IOException {
|
||||||
|
try (var in = new BufferedReader(new InputStreamReader(System.in))) {
|
||||||
|
in
|
||||||
|
.lines()
|
||||||
|
.sorted(String::compareTo)
|
||||||
|
.forEachOrdered(System.out::println);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</code></pre>
|
||||||
|
<figcaption>An equivalent program written in Java.</figcaption>
|
||||||
|
</figure>
|
||||||
|
<p>
|
||||||
|
Of course, I could list all of the language features here which allow D to excel in each of the basic
|
||||||
|
requirements, but in short, it offers all of the power of a low-level systems language like C++, while at
|
||||||
|
the same time offering the convenience of high-level languages like automatic memory management, and
|
||||||
|
metaprogramming. So, I think that D is somewhat unique in that it gives programmers a freedom to choose
|
||||||
|
their own preferred style; it's a language accomplishes as much as a family of languages.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h3>Why Isn't D Popular?</h3>
|
||||||
|
<p>
|
||||||
|
You'd think that all these benefits, developers would be flocking in droves to D. But clearly in the real
|
||||||
|
world, that's not the case. <strong>Why?</strong> This section outlines some of the reasons why the language
|
||||||
|
hasn't attained the same level of popularity as others.
|
||||||
|
</p>
|
||||||
|
<h4>Late to the Party</h4>
|
||||||
|
<p>
|
||||||
|
Despite their modern-day popularity, all of the major programming languages are several decades old: Java
|
||||||
|
was created in 1995, Python in 1991, C++ in 1985, and so on. D was first conceived in 2001, giving all of
|
||||||
|
these other languages <em>years</em> to build a loyal following. Additionally, the early version of D
|
||||||
|
was highly volatile and suffered from having two competing implementations of a standard library. In 2007,
|
||||||
|
D2 was released for the first time, marking the "stabilization" of D, and the language has essentially grown
|
||||||
|
from there. So we're dealing with a relatively immature language as of yet, which still needs to prove its
|
||||||
|
worthiness in various disciplines before it gains popularity.
|
||||||
|
</p>
|
||||||
|
<h4>Paradoxical Loneliness</h4>
|
||||||
|
<p>
|
||||||
|
One fatal issue which affects many software projects is this: the project receives little attention from new
|
||||||
|
users, so it is not promoted to new users. Therefore, the project yet again receives little attention from
|
||||||
|
its limited userbase, and so on... D is no exception. Other popular languages were quickly adopted by large
|
||||||
|
corporations: Java by Oracle, C++ by Microsoft, Python by Google, etc. Now we're in a situation where most
|
||||||
|
large employers will choose a "tried and true" programming language that's been used by other corporations,
|
||||||
|
and thus less popular languages stay in the shadows. Since D isn't so radically different than other
|
||||||
|
languages but instead offers smaller meaningful, incremental improvements, it's also not the most flashy
|
||||||
|
new language for individual developers to rave about in the free time.
|
||||||
|
</p>
|
||||||
|
<h4>A New Ecosystem</h4>
|
||||||
|
<p>
|
||||||
|
Stemming from the other reasons mentioned above, because D is relatively new, its software ecosystem is
|
||||||
|
still growing, and lacks some of the mature, standard libraries we've come to expect from a general-purpose
|
||||||
|
programming language: a standard async library, an HTTP client, a standard GUI framework, and the list goes
|
||||||
|
on. Over time, these problems are usually solved by the community, so I believe that D will emerge with a
|
||||||
|
robust library of modules that rivals the Java and C++ ecosystems.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<a href="../blog.html">Back to Dev Discussions</a>
|
||||||
|
</article>
|
||||||
|
<script src="../../js/prism.js"></script>
|
||||||
|
<script src="../../js/themes.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,134 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>DSH - Easier Scripting in D</title>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta name="description" content="DSH is a library I developed to make it easier to write scripts in the D programming language."/>
|
||||||
|
<link rel="stylesheet" href="../../css/style.css" type="text/css"/>
|
||||||
|
<link rel="stylesheet" href="../../css/prism.css" type="text/css"/>
|
||||||
|
<link rel="stylesheet" href="../../css/blog-article.css" type="text/css"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<article>
|
||||||
|
<header>
|
||||||
|
<h1>DSH - Easier Scripting in D</h1>
|
||||||
|
<p>
|
||||||
|
<em>Written on <time>5 May, 2022</time>, by Andrew Lalis.</em>
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
<section>
|
||||||
|
<p>
|
||||||
|
You're probably used to writing your automation scripts in a language like Bash, Powershell, Ruby, or Python. With DSH, it can be just as easy to write those scripts in D. I've designed DSH as a combination of a standalone programming library, and a set of tools to facilitate faster development and deployment. This article will discuss some of the basic use cases and how DSH works to make it easier to accomplish them.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
In case you're interested, you can check out DSH on <a class="link_external" href="https://code.dlang.org/packages/dsh">code.dlang.org</a> or on <a class="link_external" href="https://github.com/andrewlalis/dsh">GitHub</a>.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h3>Why write scripts in D?</h3>
|
||||||
|
<p>
|
||||||
|
While many people are used to scripting in languages that have been built over the years specifically for that use case, this introduces extra complexity into projects that are mainly built using some other language. In an ideal world, we'd use one single language to describe everything about our app, from the way it looks, to how we compile and distribute it. DSH is my attempt to make this a reality, at least for applications written in D. When developing D programs that require some auxiliary scripts, you don't have to make a cognitive jump to another language; just keep writing code in the environment you're most efficient in. This way, we reap the benefits of specializing in becoming an expert in a single language and its ecosystem.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If you're still not fully convinced, that's alright; read ahead and see if any of my examples can sway your opinion.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h3>Writing Your Script</h3>
|
||||||
|
<p>
|
||||||
|
There are two main ways to write scripts using DSH:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Add a <code>dub.sdl</code> package descriptor to the top of your file, and an executable shebang. For example:
|
||||||
|
<pre><code class="language-d">
|
||||||
|
#!/usr/bin/env dub
|
||||||
|
/+ dub.sdl:
|
||||||
|
dependency "dsh" version="~>1.6.1"
|
||||||
|
+/
|
||||||
|
import dsh;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
print("Hello world!");
|
||||||
|
}
|
||||||
|
</code></pre>
|
||||||
|
Execute this script via <code>./my_script.d</code>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Use the single-file <code>dshs.d</code> version of DSH, for compiled scripts which don't use the dub package manager:
|
||||||
|
<pre><code class="language-d">
|
||||||
|
import dsh;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
print("Hello world!");
|
||||||
|
}
|
||||||
|
</code></pre>
|
||||||
|
Executing these scripts involves compiling them first. <code>dmd my_script.d /usr/include/dshs.d && ./my_script</code>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
Generally, the <em>dynamic</em> form is more useful for scripts you'd like to distribute to anyone, since they only require the user to have the D toolchain installed in order to run it. However, it is slower, since the script and all its dependencies must be compiled on-the-fly.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Alternatively, the <em>static</em> form is more useful for complex scripts that are best pre-compiled, or for when you don't want to use the dub package manager, just a D compiler.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Luckily for either of these solutions, DSH comes with a <code>dshutil</code> tool to help you get started. You can install it like so:
|
||||||
|
<pre><code class="language-bash">
|
||||||
|
wget https://raw.githubusercontent.com/andrewlalis/dsh/main/tools/dshutil.d -O dshutil.d
|
||||||
|
chmod +x dshutil.d
|
||||||
|
./dshutil.d install
|
||||||
|
</code></pre>
|
||||||
|
I won't go into all the features of dshutil here, since you can read the full documentation on <a class="link_external" href="https://github.com/andrewlalis/dsh/tree/main/tools">GitHub</a>, but in short, it provides shortcuts for creating boilerplate scripts, automatically compiling when changes are detected, and gives you access to the dshs.d single-file library for static scripts.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h3>Helper Functions</h3>
|
||||||
|
<p>
|
||||||
|
While dshutil is a handy tool on its own, the real benefit of using DSH is its library of helper functions which are designed specifically for scripting tasks.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4>File Utilities</h4>
|
||||||
|
<p>
|
||||||
|
DSH offers quite a few helpful functions that reduce the verbosity of your IO operations. Below is a sampling of some of the most prominent functions.
|
||||||
|
</p>
|
||||||
|
<pre><code class="language-d">
|
||||||
|
import dsh;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
copyDir("fromdir", "todir");
|
||||||
|
string filename = findFile("dir", "config\\.json");
|
||||||
|
string[] files = findFiles("dir", "^config.*$");
|
||||||
|
string[] dFiles = findFilesByExtension("dir", ".d");
|
||||||
|
string home = getHomeDir();
|
||||||
|
removeAnyIfExists(dFiles);
|
||||||
|
}
|
||||||
|
</code></pre>
|
||||||
|
|
||||||
|
<h4>Process Utilities</h4>
|
||||||
|
<p>
|
||||||
|
In addition to the functions in D's <code>std.process</code> module, DSH provides some shortcuts for scripts to make it easy to manage processes.
|
||||||
|
</p>
|
||||||
|
<pre><code class="language-d">
|
||||||
|
import dsh;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
run("cat out.txt");
|
||||||
|
runOrQuit("git pull"); // exits the program if the process fails.
|
||||||
|
setEnv("MY_VAR", "TRUE");
|
||||||
|
print(getEnv("MY_VAR"));
|
||||||
|
}
|
||||||
|
</code></pre>
|
||||||
|
<p>
|
||||||
|
Also included is a <code>ProcessBuilder</code> class with which you can use a fluent method interface to construct processes with custom input and output sources and environments.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<a href="../blog.html">Back to Dev Discussions</a>
|
||||||
|
</article>
|
||||||
|
<script src="../../js/prism.js"></script>
|
||||||
|
<script src="../../js/themes.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,252 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Java + Swing 2D Games</title>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta name="description" content="Important information for programmers who want to make simple 2D games with Java using the Swing framework."/>
|
||||||
|
<link rel="stylesheet" href="../../css/style.css" type="text/css"/>
|
||||||
|
<link rel="stylesheet" href="../../css/prism.css" type="text/css"/>
|
||||||
|
<link rel="stylesheet" href="../../css/blog-article.css" type="text/css"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<article>
|
||||||
|
<header>
|
||||||
|
<h1>2D Game Development with Java and Swing</h1>
|
||||||
|
<p>
|
||||||
|
<em>Written on <time>May 9, 2021</time>, by Andrew Lalis.</em>
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
<section>
|
||||||
|
<p>
|
||||||
|
Often times, new programmers will immediately jump into making graphical applications using their new-found skills, without getting a firm grasp of how to design something that doesn't require refactoring literally any time new functionality is required. Obviously, we can't just collectively tell all new programmers to <em>just be patient</em>; that will never work. So hopefully this article can serve as a guide to put you on the right track to developing 2D applications (games, most likely) using Java and Swing.
|
||||||
|
<br>
|
||||||
|
<small>I will be referencing snippets of code that come from <a href="https://github.com/andrewlalis/BeforeAsteroids">this repository</a>, so check there if you want to read through all the code yourself.</small>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Since this is quite a large guide, you can skip to a particular section if you want:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li><a href="#mvc">Model-View-Control Design</a></li>
|
||||||
|
<li><a href="#swing_setup">Setting up a Swing GUI</a></li>
|
||||||
|
<li><a href="#game_loop">The Game Loop</a></li>
|
||||||
|
<li><a href="#listeners">Listeners and Control</a></li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
<section id="mvc">
|
||||||
|
<h3>Model-View-Control Design</h3>
|
||||||
|
<p>
|
||||||
|
At first, it's super easy to make a new JFrame and start drawing on it, and you can add action listeners everywhere, whenever they're needed. If you ever plan on expanding beyond a simple 20-line drawing method however, you should organize your code using the <a href="https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller">Model-View-Controller</a> pattern. For example, in my <a href="https://github.com/andrewlalis/BeforeAsteroids/tree/main/src/main/java/nl/andrewlalis"><em>Before Asteroids</em></a> sample game, I have split up my code into a few different packages.
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>control</strong> - Contains all my game's listeners for player interaction.</li>
|
||||||
|
<li><strong>model</strong> - Contains all the code that makes up the core game model.</li>
|
||||||
|
<li><strong>view</strong> - Contains all the code for rendering the game model.</li>
|
||||||
|
<li><strong>physics</strong> - Some special physics code that is needed when updating the game model.</li>
|
||||||
|
<li><strong>util</strong> - Extra utilities for things like file IO.</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
In most cases, it's especially useful to define a single class that represents your entire game's state; a so-called <strong>Game Model</strong>. This model would contain all the entities in your game that exist in the world, for example. Or in the case of something like a simple tic-tac-toe game, it would contain data about the status of each of the nine squares on the board.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Keep this design pattern in mind, and working on your project as it grows larger hopefully won't become as much of a headache anymore.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<section id="swing_setup">
|
||||||
|
<h3>Setting up a Swing GUI</h3>
|
||||||
|
<p>
|
||||||
|
Making a Swing GUI usually consists of two basic components: the <strong>JFrame</strong> that acts as your application's window, and a <strong>JPanel</strong> that is used for actually drawing your model. Usually we start by defining our own frame to extend from the base JFrame, and set its properties in a constructor.
|
||||||
|
</p>
|
||||||
|
<figure>
|
||||||
|
<pre><code class="language-java">
|
||||||
|
public class GameFrame extends JFrame {
|
||||||
|
public GameFrame(GameModel model) {
|
||||||
|
this.setTitle("My Game");
|
||||||
|
this.setPreferredSize(new Dimension(800, 600));
|
||||||
|
// Set the frame to be disposed when the top-right 'X' is clicked.
|
||||||
|
this.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
|
||||||
|
GamePanel gamePanel = new GamePanel(model);
|
||||||
|
this.setContentPane(gamePanel);
|
||||||
|
this.pack();
|
||||||
|
// Start with the frame centered in the screen.
|
||||||
|
this.setLocationRelativeTo(null);
|
||||||
|
// TODO: Setup listeners and game loop logic.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</code></pre>
|
||||||
|
<figcaption>Notice that we pass in our game model as a constructor argument. This is so that we can pass it to our <code>GamePanel</code>, which will ultimately be drawing the model.</figcaption>
|
||||||
|
</figure>
|
||||||
|
<p>
|
||||||
|
Inside the frame, there's a single <code>GamePanel</code> that we give a reference to the game model. This panel is responsible for rendering the model to the screen, and usually we can just define it as a child class of Swing's <code>JPanel</code>.
|
||||||
|
</p>
|
||||||
|
<figure>
|
||||||
|
<pre><code class="language-java">
|
||||||
|
public class GamePanel extends JPanel {
|
||||||
|
private GameModel model;
|
||||||
|
|
||||||
|
public GamePanel(GameModel model) {
|
||||||
|
this.model = model;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void paintComponent(Graphics g) {
|
||||||
|
super.paintComponent(g);
|
||||||
|
Graphics2D g2 = (Graphics2D) g;
|
||||||
|
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
|
g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
|
||||||
|
g2.setColor(Color.BLACK);
|
||||||
|
g2.fillRect(0, 0, this.getWidth(), this.getHeight());
|
||||||
|
// TODO: Draw model on screen.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</code></pre>
|
||||||
|
<figcaption>To draw on a panel, all you need to do is override <code>paintComponent</code>. The Javadoc advise calling the super method to avoid graphical bugs, so we do that first. Then we set some rendering hints to turn on anti-aliasing and how strokes (outlines) are rendered. Finally we clear the screen to a black rectangle.</figcaption>
|
||||||
|
</figure>
|
||||||
|
</section>
|
||||||
|
<section id="game_loop">
|
||||||
|
<h3>The Game Loop</h3>
|
||||||
|
<p>
|
||||||
|
First of all, this guide won't go too in-depth about the details of game loops in general, but focuses specifically on their implementation with Java/Swing. For more information, you can read <a href="https://gameprogrammingpatterns.com/game-loop.html">this excellent article on the subject</a>.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
In Swing GUIs, your application is by-default multi-threaded, since Swing has its own thread for managing the user interface. In order to run our game and not interfere with Swing's own GUI processing, we should have our game update logic run in its own thread. The thread should periodically update the model, according to some defined frequency, and it should also trigger the rendering of frames of your game, once again according to some defined frequency. In the example below, we update physics 60 times per second, and render the model also 60 times per second, but these values can easily be changed due to how I've set up the math.
|
||||||
|
</p>
|
||||||
|
<figure>
|
||||||
|
<pre><code class="language-java">
|
||||||
|
public class GameUpdater extends Thread {
|
||||||
|
public static final double PHYSICS_FPS = 60.0;
|
||||||
|
public static final double MILLISECONDS_PER_PHYSICS_TICK = 1000.0 / PHYSICS_FPS;
|
||||||
|
public static final double PHYSICS_SPEED = 1.0;
|
||||||
|
|
||||||
|
public static final double DISPLAY_FPS = 60.0;
|
||||||
|
public static final double MILLISECONDS_PER_DISPLAY_FRAME = 1000.0 / DISPLAY_FPS;
|
||||||
|
|
||||||
|
private final GameModel model;
|
||||||
|
private final GamePanel gamePanel;
|
||||||
|
private volatile boolean running = true;
|
||||||
|
|
||||||
|
public GameUpdater(GameModel model, GamePanel gamePanel) {
|
||||||
|
this.model = model;
|
||||||
|
this.gamePanel = gamePanel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRunning(boolean running) {
|
||||||
|
this.running = running;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
long lastPhysicsUpdate = System.currentTimeMillis();
|
||||||
|
long lastDisplayUpdate = System.currentTimeMillis();
|
||||||
|
while (this.running) {
|
||||||
|
long currentTime = System.currentTimeMillis();
|
||||||
|
long timeSinceLastPhysicsUpdate = currentTime - lastPhysicsUpdate;
|
||||||
|
long timeSinceLastDisplayUpdate = currentTime - lastDisplayUpdate;
|
||||||
|
if (timeSinceLastPhysicsUpdate >= MILLISECONDS_PER_PHYSICS_TICK) {
|
||||||
|
double elapsedSeconds = timeSinceLastPhysicsUpdate / 1000.0;
|
||||||
|
this.updateModelPhysics(elapsedSeconds * PHYSICS_SPEED);
|
||||||
|
lastPhysicsUpdate = currentTime;
|
||||||
|
timeSinceLastPhysicsUpdate = 0L;
|
||||||
|
}
|
||||||
|
if (timeSinceLastDisplayUpdate >= MILLISECONDS_PER_DISPLAY_FRAME) {
|
||||||
|
this.gamePanel.repaint();
|
||||||
|
lastDisplayUpdate = currentTime;
|
||||||
|
timeSinceLastDisplayUpdate = 0L;
|
||||||
|
}
|
||||||
|
long timeUntilNextPhysicsUpdate = (long) (MILLISECONDS_PER_PHYSICS_TICK - timeSinceLastPhysicsUpdate);
|
||||||
|
long timeUntilNextDisplayUpdate = (long) (MILLISECONDS_PER_DISPLAY_FRAME - timeSinceLastDisplayUpdate);
|
||||||
|
|
||||||
|
// Sleep to reduce CPU usage.
|
||||||
|
try {
|
||||||
|
Thread.sleep(Math.min(timeUntilNextPhysicsUpdate, timeUntilNextDisplayUpdate));
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Other methods omitted for brevity.
|
||||||
|
}
|
||||||
|
</code></pre>
|
||||||
|
<figcaption>Basically, our game loop logic runs continuously until something else sets the <code>running</code> flag to <strong>false</strong>. During the loop, we determine how long it's been since the last physics and display updates, and if enough time has passed, we do an update and reset the "timeSince..." variable. To avoid wasting CPU usage on rapidly looping, we sleep until the next update.</figcaption>
|
||||||
|
</figure>
|
||||||
|
<p>
|
||||||
|
We can add our game updater thread to the end of our frame's constructor, so that when we initialize the game frame, the game loop begins.
|
||||||
|
</p>
|
||||||
|
<figure>
|
||||||
|
<pre><code class="language-java">
|
||||||
|
public class GameFrame extends JFrame {
|
||||||
|
public GameFrame(GameModel model) {
|
||||||
|
this.setTitle("My Game");
|
||||||
|
this.setPreferredSize(new Dimension(800, 600));
|
||||||
|
// Set the frame to be disposed when the top-right 'X' is clicked.
|
||||||
|
this.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
|
||||||
|
GamePanel gamePanel = new GamePanel(model);
|
||||||
|
this.setContentPane(gamePanel);
|
||||||
|
this.pack();
|
||||||
|
// Start with the frame centered in the screen.
|
||||||
|
this.setLocationRelativeTo(null);
|
||||||
|
|
||||||
|
this.updater = new GameUpdater(model, gamePanel);
|
||||||
|
this.updater.start();
|
||||||
|
this.addWindowListener(new WindowAdapter() {
|
||||||
|
@Override
|
||||||
|
public void windowClosing(WindowEvent e) {
|
||||||
|
updater.setRunning(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// TODO: Setup listeners
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</code></pre>
|
||||||
|
</figure>
|
||||||
|
</section>
|
||||||
|
<section id="listeners">
|
||||||
|
<h3>Listeners and Control</h3>
|
||||||
|
<p>
|
||||||
|
Most often, you'll want something to happen when the player presses a key or clicks the mouse. To do this, you should create your own class which extends from <a href="https://docs.oracle.com/javase/8/docs/api/java/awt/event/KeyAdapter.html">KeyAdapter</a> or <a href="https://docs.oracle.com/javase/8/docs/api/java/awt/event/MouseAdapter.html">MouseAdapter</a>, depending on what you want to do.
|
||||||
|
<br>
|
||||||
|
<small><em>You can also directly implement <a href="https://docs.oracle.com/javase/8/docs/api/java/awt/event/KeyListener.html">KeyListener</a> and <a href="https://docs.oracle.com/javase/8/docs/api/java/awt/event/MouseListener.html">MouseListener</a> at the same time, but this isn't as clean, and you have to implement every method declared in the interfaces, even if you aren't using them.</em></small>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Let's take a look at a common case: doing something when a key is pressed. To do so, we override the KeyAdapter's <code>keyPressed</code> method like so:
|
||||||
|
</p>
|
||||||
|
<figure>
|
||||||
|
<pre><code class="language-java">
|
||||||
|
@Override
|
||||||
|
public void keyPressed(KeyEvent e) {
|
||||||
|
int c = e.getKeyCode();
|
||||||
|
if (c == KeyEvent.VK_W) {
|
||||||
|
player.getShip().forwardThrusterEnabled = true;
|
||||||
|
} else if (c == KeyEvent.VK_A) {
|
||||||
|
player.getShip().turningLeft = true;
|
||||||
|
} else if (c == KeyEvent.VK_D) {
|
||||||
|
player.getShip().turningRight = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</code></pre>
|
||||||
|
<figcaption>To check what key was pressed, we call <code>getKeyCode()</code> on the key event that was received, and then check if it matches one of the constants defined in the <a href="https://docs.oracle.com/javase/8/docs/api/java/awt/event/KeyEvent.html">KeyEvent</a> class, and if so, then we do something to our player. In this case, we set some boolean flags that indicate how the player's ship is trying to move.</figcaption>
|
||||||
|
</figure>
|
||||||
|
<p>
|
||||||
|
Handling mouse events works almost the same way, but instead we check what button of the mouse was pressed.
|
||||||
|
</p>
|
||||||
|
<figure>
|
||||||
|
<pre><code class="language-java">
|
||||||
|
@Override
|
||||||
|
public void mouseClicked(MouseEvent e) {
|
||||||
|
int b = e.getButton();
|
||||||
|
if (b == MouseEvent.BUTTON1) {
|
||||||
|
System.out.printf("Player clicked at x=%d, y=%d\n", e.getX(), e.getY());
|
||||||
|
player.getShip().firingBlaster = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</code></pre>
|
||||||
|
<figcaption>In this example, I show both checking which button was pressed, and also checking where the button was pressed. <code>getX()</code> and <code>getY()</code> return the x- and y-coordinates relative to the <em>source component</em>, which will most often be the JFrame or JPanel that the user clicked on.</figcaption>
|
||||||
|
</figure>
|
||||||
|
</section>
|
||||||
|
<a href="../blog.html">Back to Dev Discussions</a>
|
||||||
|
</article>
|
||||||
|
<script src="../../js/prism.js"></script>
|
||||||
|
<script src="../../js/themes.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,81 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Spring's Open-Session-in-View & Transactions</title>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta name="description" content="Some explanation about Spring Boot's open-session-in-view setting and how to get around it."/>
|
||||||
|
<link rel="stylesheet" href="../../css/style.css" type="text/css"/>
|
||||||
|
<link rel="stylesheet" href="../../css/prism.css" type="text/css"/>
|
||||||
|
<link rel="stylesheet" href="../../css/blog-article.css" type="text/css"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<article>
|
||||||
|
<header>
|
||||||
|
<h1>Spring's Open-Session-in-View & Transactions</h1>
|
||||||
|
<p>
|
||||||
|
<em>Written on <time>May 9, 2021</time>, by Andrew Lalis.</em>
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
<section>
|
||||||
|
<p>
|
||||||
|
If you're reading this article, there's a good chance you've already heard about Spring's controversial <code>OpenSessionInViewFilter</code>, which allows lazy loading of entity relations during the rendering of a web view. In case you haven't, I suggest reading this excellent <a href="https://vladmihalcea.com/the-open-session-in-view-anti-pattern/">article by Vlad Mihalcea</a> which goes into much more detail on why one should avoid using the filter entirely.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
This article will focus on the practical way to structure your application logic so that you can still take advantage of Hibernate's lazy loading, without using the OSIV anti-pattern.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h3>Don't Send Entities to Your View</h3>
|
||||||
|
<p>
|
||||||
|
At first, many of the simple tutorials you'll read online will have you passing your entity models to Thymeleaf or JSP templates to render a web page for your application. This convenience is what leads developers to use OSIV in the first place, because they start fetching attributes of their entity which were not initially loaded. This is fine for the most simple "Hello World" starter projects, but as soon as you add any real complexity, this "convenience" breaks down into an unorganized mess, where it is not clear at a glance where data will be fetched from the database.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
One solution, which beginners often turn to, is to simply eager-fetch all the attributes they'll need using a their data-access object (<em>usually a repository with a custom JPQL query</em>). This works well enough, and is often the preferred solution since it's about as optimized as you can get. The issue with this approach is that it's just not scalable to maintain a huge library of queries that are each tailor-made for a particular use case, and tends to sidestep Hibernate's powerful lazy-fetching features.
|
||||||
|
</p>
|
||||||
|
<figure>
|
||||||
|
<pre><code class="language-java">
|
||||||
|
@Query("SELECT a FROM Assignment a " +
|
||||||
|
"LEFT JOIN FETCH a.gradings " +
|
||||||
|
"LEFT JOIN FETCH a.zippedFile " +
|
||||||
|
"WHERE a.id = :id")
|
||||||
|
Optional<Assignment> findByIdWithAllRelations(Long id);
|
||||||
|
</code></pre>
|
||||||
|
<figcaption>This is an example of the the sort of JPQL queries that will become common if you try to always fetch exactly what you need. One or two of these methods is fine, but if the <code>Assignment</code> entity is used in 150 different places in our code, each with different requirements for which attribute need to be fetched, then this becomes unsustainable.</figcaption>
|
||||||
|
</figure>
|
||||||
|
<p>
|
||||||
|
Eager-fetching the right attributes for an entity still leaves you with the issue of passing your actual entity object to the view for rendering, which means that an unsuspecting developer will eventually try to read a lazy-loaded attribute, which will throw a LazyInitializationException. The solution to that issue, is to not pass your entity to the view at all, but instead pass a data-transfer object (DTO) containing exactly the data that the view requires, and nothing more. And now, your service layer is responsible for producing this DTO, and it <em>can</em> take full advantage of Hibernate's lazy-loading functionality.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h3>Transactions</h3>
|
||||||
|
<p>
|
||||||
|
The Spring framework allows you to annotate a service's method as <code>@Transactional</code>, which means that at runtime, when your method is called, a proxy method is wrapped around it which starts a transaction (and thus a Hibernate session to go with it) that persists for the duration of the method. Inside this method, you are free to fetch lazy-loaded attributes from an entity, without having to deal with LazyInitializationExceptions.
|
||||||
|
</p>
|
||||||
|
<figure>
|
||||||
|
<pre><code class="language-java">
|
||||||
|
@Transactional
|
||||||
|
public boolean isPersonAdmin(long personId) {
|
||||||
|
Person p = this.personRepo.findById(personId).orElseThrow();
|
||||||
|
for (Role r : p.getRoles()) {
|
||||||
|
if (r.getName().equals("ADMIN")) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
</code></pre>
|
||||||
|
<figcaption>In this example, we have a service method <code>isPersonAdmin</code>, which looks to see if a person has the "ADMIN" role. This will work even if the person's list of roles is lazy-loaded, because it all happens in the context of the transaction.</figcaption>
|
||||||
|
</figure>
|
||||||
|
<p>
|
||||||
|
One caveat to be aware of is the fact that entity object should not be passed as arguments to a transactional method, since the Hibernate session that is dedicated to the transaction has no idea what that entity is, and thus can't fetch lazy attributes from it.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
However, the flexibility of being able to use lazy-loaded attributes the easy way more than makes up for the strict encapsulation rules, and by structuring your project around service methods as black-box operations, you'll be able to build huge projects while keeping your sanity mostly intact.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<a href="../blog.html">Back to Dev Discussions</a>
|
||||||
|
</article>
|
||||||
|
<script src="../../js/prism.js"></script>
|
||||||
|
<script src="../../js/themes.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,124 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>{{TEMPLATE_TITLE}}</title>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta name="description" content="{{TEMPLATE_DESCRIPTION}}"/>
|
||||||
|
<link rel="stylesheet" href="../styles/font.css" type="text/css">
|
||||||
|
|
||||||
|
<!-- CSS class to hide elements if JS is not enabled. -->
|
||||||
|
<noscript><style>.jsonly{display: none !important;}</style></noscript>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// An inline script that manages the site's color theme.
|
||||||
|
const THEMES = ["light", "dark"];
|
||||||
|
|
||||||
|
function getPreferredTheme() {
|
||||||
|
const storedTheme = localStorage.getItem("theme");
|
||||||
|
if (storedTheme !== null && THEMES.includes(storedTheme)) return storedTheme;
|
||||||
|
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||||
|
return "dark";
|
||||||
|
}
|
||||||
|
return "light";
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPreferredTheme(theme) {
|
||||||
|
document.documentElement.className = theme;
|
||||||
|
localStorage.setItem("theme", theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
setPreferredTheme(getPreferredTheme());
|
||||||
|
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", event => {
|
||||||
|
const newTheme = event.matches ? "dark" : "light";
|
||||||
|
setPreferredTheme(newTheme);
|
||||||
|
});
|
||||||
|
document.addEventListener("DOMContentLoaded", event => {
|
||||||
|
const themeToggleButton = document.getElementById("themeToggleButton");
|
||||||
|
themeToggleButton.onclick = clickEvent => {
|
||||||
|
const currentTheme = getPreferredTheme();
|
||||||
|
const idx = THEMES.indexOf(currentTheme);
|
||||||
|
const nextIdx = idx === THEMES.length - 1 ? 0 : idx + 1;
|
||||||
|
setPreferredTheme(THEMES[nextIdx]);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="../styles/style.css" type="text/css"/>
|
||||||
|
<link rel="stylesheet" href="../styles/article.css" type="text/css"/>
|
||||||
|
<link rel="stylesheet" href="../vendor/prism.css" type="text/css"/>
|
||||||
|
<script src="../vendor/prism.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header class="page-header">
|
||||||
|
<h1>{{TEMPLATE_TITLE}}</h1>
|
||||||
|
<nav>
|
||||||
|
<div>
|
||||||
|
<a href="../index.html">Home</a>
|
||||||
|
<a class="page-header-selected" href="../articles.html">Articles</a>
|
||||||
|
<a href="../projects.html">Projects</a>
|
||||||
|
<a href="../training.html">Training</a>
|
||||||
|
<a href="../contact.html">Contact</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="https://github.com/andrewlalis">GitHub</a>
|
||||||
|
<a href="https://www.linkedin.com/in/andrew-lalis/">LinkedIn</a>
|
||||||
|
<a href="https://www.youtube.com/channel/UC9X4mx6-ObPUB6-ud2IGAFQ">YouTube</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<button id="themeToggleButton" class="jsonly">Change Color Theme</button>
|
||||||
|
<hr>
|
||||||
|
<p>
|
||||||
|
<em>Written on <time>{{TEMPLATE_TIME}}</time>, by Andrew Lalis.</em>
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<article>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<p>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi dignissim feugiat tortor. Donec non gravida massa. Interdum et malesuada fames ac ante ipsum primis in faucibus. Aliquam eleifend nunc placerat ante suscipit consequat. Nulla tempor pellentesque mollis. Nam malesuada risus urna, ac fermentum dui egestas sit amet. Ut vel tellus eros. Phasellus massa erat, dignissim ac arcu non, vestibulum rhoncus diam. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec pharetra pretium purus, et tempus elit tristique maximus. Donec imperdiet, enim et consequat pharetra, purus augue congue nulla, a hendrerit nulla est id augue. Aliquam erat volutpat. Fusce eu lorem purus.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Curabitur lobortis convallis nunc interdum mattis. Suspendisse tincidunt nec mi in elementum. Vestibulum eu sagittis felis, at bibendum odio. Quisque tortor mi, convallis ut nulla id, scelerisque scelerisque eros. Duis convallis enim lorem, sit amet congue ipsum eleifend et. Phasellus finibus elit nibh, nec sodales nisi vehicula quis. Quisque a elit nec turpis maximus posuere id ac turpis. Aliquam efficitur accumsan tortor, sit amet hendrerit eros faucibus eget. Mauris non est in lectus hendrerit varius pellentesque at ante. Morbi sit amet suscipit ligula, vitae condimentum quam. Etiam diam nunc, vehicula a fermentum nec, varius nec diam. Etiam elit tellus, mattis eu libero quis, faucibus varius est. <a href="https://google.com">Here is a link to Google.com.</a>
|
||||||
|
</p>
|
||||||
|
<figure>
|
||||||
|
<pre><code class="language-java line-numbers">
|
||||||
|
/**
|
||||||
|
* Shortcut for getting a property.
|
||||||
|
* @param key The property's key.
|
||||||
|
* @return The value for that property.
|
||||||
|
*/
|
||||||
|
public static String getProp(String key) {
|
||||||
|
return getInstance().getProperties().getProperty(key);
|
||||||
|
}
|
||||||
|
</code></pre>
|
||||||
|
<figcaption>This is a code snippet with line numbers.</figcaption>
|
||||||
|
</figure>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h3>Subsection</h3>
|
||||||
|
<p>
|
||||||
|
Praesent elementum quam in odio sodales, vel suscipit massa dignissim. Aenean id molestie lectus. Vestibulum eu magna scelerisque dui elementum molestie. Vestibulum vitae interdum felis. In rutrum turpis at posuere fermentum. Interdum et malesuada fames ac ante ipsum primis in faucibus. Duis porta blandit blandit. Morbi ut vehicula metus, tristique ultrices justo. Vivamus ullamcorper convallis tempus. In et laoreet quam. Maecenas lobortis, enim ut tincidunt ultricies, dui mauris dictum magna, vel facilisis nunc nisi at ligula.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h3>Second Subsection</h3>
|
||||||
|
<p>
|
||||||
|
Some small text.
|
||||||
|
</p>
|
||||||
|
<figure>
|
||||||
|
<pre><code class="language-c">
|
||||||
|
void main(int argc, char** argv) {
|
||||||
|
printf("Hello world!\n");
|
||||||
|
}
|
||||||
|
</code></pre>
|
||||||
|
</figure>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -54,7 +54,7 @@
|
||||||
<nav>
|
<nav>
|
||||||
<div>
|
<div>
|
||||||
<a href="index.html">Home</a>
|
<a href="index.html">Home</a>
|
||||||
<a href="blog.html">Articles</a>
|
<a href="articles.html">Articles</a>
|
||||||
<a href="projects.html">Projects</a>
|
<a href="projects.html">Projects</a>
|
||||||
<a href="training.html">Training</a>
|
<a href="training.html">Training</a>
|
||||||
<a class="page-header-selected" href="contact.html">Contact</a>
|
<a class="page-header-selected" href="contact.html">Contact</a>
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -54,7 +54,7 @@
|
||||||
<nav>
|
<nav>
|
||||||
<div>
|
<div>
|
||||||
<a class="page-header-selected" href="index.html">Home</a>
|
<a class="page-header-selected" href="index.html">Home</a>
|
||||||
<a href="blog.html">Articles</a>
|
<a href="articles.html">Articles</a>
|
||||||
<a href="projects.html">Projects</a>
|
<a href="projects.html">Projects</a>
|
||||||
<a href="training.html">Training</a>
|
<a href="training.html">Training</a>
|
||||||
<a href="contact.html">Contact</a>
|
<a href="contact.html">Contact</a>
|
||||||
|
@ -84,6 +84,9 @@
|
||||||
<p>
|
<p>
|
||||||
As you can see, I'm a fan of the <em>"plain HTML is responsive"</em> school of thought. This website is intentionally very simple in its structure, styling, and interactivity. This is because I believe that the vast majority of the internet's most popular websites sacrifice usability and accessibility to make a flashier product. By using a simpler design, we can help to make the internet more accessible to all, regardless of their physical ability or device's processing power.
|
As you can see, I'm a fan of the <em>"plain HTML is responsive"</em> school of thought. This website is intentionally very simple in its structure, styling, and interactivity. This is because I believe that the vast majority of the internet's most popular websites sacrifice usability and accessibility to make a flashier product. By using a simpler design, we can help to make the internet more accessible to all, regardless of their physical ability or device's processing power.
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
This website doesn't use any third-party scripts or cookies to track you, and barely uses any javascript at all! It uses a script to allow you to change the site's color theme, but this is entirely optional.
|
||||||
|
</p>
|
||||||
</article>
|
</article>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -57,7 +57,7 @@
|
||||||
<nav>
|
<nav>
|
||||||
<div>
|
<div>
|
||||||
<a href="index.html">Home</a>
|
<a href="index.html">Home</a>
|
||||||
<a href="blog.html">Articles</a>
|
<a href="articles.html">Articles</a>
|
||||||
<a class="page-header-selected" href="projects.html">Projects</a>
|
<a class="page-header-selected" href="projects.html">Projects</a>
|
||||||
<a href="training.html">Training</a>
|
<a href="training.html">Training</a>
|
||||||
<a href="contact.html">Contact</a>
|
<a href="contact.html">Contact</a>
|
||||||
|
@ -77,7 +77,14 @@
|
||||||
<p>
|
<p>
|
||||||
A top-down 2D shooter game inspired by <em>Ace of Spades</em>, and was made as a submission the 2021 Java Discord server's Java Jam. Includes a dedicated server, client, and server registry application.
|
A top-down 2D shooter game inspired by <em>Ace of Spades</em>, and was made as a submission the 2021 Java Discord server's Java Jam. Includes a dedicated server, client, and server registry application.
|
||||||
</p>
|
</p>
|
||||||
<video src="videos/aos-latest-2023-04-06_19.39.53.mp4" type="video/mp4" style="width: 100%" controls></video>
|
<iframe
|
||||||
|
src="https://www.youtube.com/embed/LHeMhCN6O2Y"
|
||||||
|
style="width: 100%; min-height: 250px;"
|
||||||
|
title="YouTube video player"
|
||||||
|
frameborder="0"
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||||
|
allowfullscreen>
|
||||||
|
</iframe>
|
||||||
<img src="images/logo_java.svg" class="lang-icon" alt="Java programming language logo">
|
<img src="images/logo_java.svg" class="lang-icon" alt="Java programming language logo">
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
@ -86,7 +93,6 @@
|
||||||
<p>
|
<p>
|
||||||
A 3D first-person shooter designed as the successor to <em>Ace of Shades</em>. This was again made as a 2022 Java Discord Java Jam submission, but I continued to develop it well beyond that.
|
A 3D first-person shooter designed as the successor to <em>Ace of Shades</em>. This was again made as a 2022 Java Discord Java Jam submission, but I continued to develop it well beyond that.
|
||||||
</p>
|
</p>
|
||||||
<video src="videos/aos2-2022-07-29_16.25.08.mp4" type="video/mp4" style="width: 100%" controls></video>
|
|
||||||
<img src="images/logo_java.svg" class="lang-icon" alt="Java programming language logo">
|
<img src="images/logo_java.svg" class="lang-icon" alt="Java programming language logo">
|
||||||
<img src="images/logo_open_gl.png" class="lang-icon" alt="OpenGL API logo">
|
<img src="images/logo_open_gl.png" class="lang-icon" alt="OpenGL API logo">
|
||||||
</article>
|
</article>
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
article {
|
||||||
|
background-color: var(--background-color-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
figure {
|
||||||
|
margin: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-size: x-small;
|
||||||
|
}
|
|
@ -1,41 +1,36 @@
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "IBM Plex Sans";
|
font-family: "IBM Plex Sans";
|
||||||
src: url(../fonts/IBMPlexSans-Regular.woff2);
|
src: url(../fonts/IBMPlexSans/IBMPlexSans-Regular.woff2);
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "IBM Plex Sans";
|
font-family: "IBM Plex Sans";
|
||||||
src: url(../fonts/IBMPlexSans-Italic.woff2);
|
src: url(../fonts/IBMPlexSans/IBMPlexSans-Italic.woff2);
|
||||||
font-weight: normal;
|
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "IBM Plex Sans";
|
font-family: "IBM Plex Sans";
|
||||||
src: url(../fonts/IBMPlexSans-Bold.woff2);
|
src: url(../fonts/IBMPlexSans/IBMPlexSans-Bold.woff2);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-style: normal;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "IBM Plex Sans";
|
font-family: "IBM Plex Sans";
|
||||||
src: url(../fonts/IBMPlexSans-BoldItalic.woff2);
|
src: url(../fonts/IBMPlexSans/IBMPlexSans-BoldItalic.woff2);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "IBM Plex Sans";
|
font-family: "IBM Plex Sans";
|
||||||
src: url(../fonts/IBMPlexSans-Light.woff2);
|
src: url(../fonts/IBMPlexSans/IBMPlexSans-Light.woff2);
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
font-style: normal;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "IBM Plex Sans";
|
font-family: "IBM Plex Sans";
|
||||||
src: url(../fonts/IBMPlexSans-LightItalic.woff2);
|
src: url(../fonts/IBMPlexSans/IBMPlexSans-LightItalic.woff2);
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
@ -43,7 +38,41 @@
|
||||||
/* Monospace variant(s) */
|
/* Monospace variant(s) */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "IBM Plex Mono";
|
font-family: "IBM Plex Mono";
|
||||||
src: url(../fonts/IBMPlexMono-Regular.ttf);
|
src: url(../fonts/IBMPlexMono/IBMPlexMono-Regular.ttf);
|
||||||
font-weight: normal;
|
}
|
||||||
font-style: normal;
|
|
||||||
|
/* serif font */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Cormorant Garamond";
|
||||||
|
src: url(../fonts/CormorantGaramond/CormorantGaramond-Regular.ttf);
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Cormorant Garamond";
|
||||||
|
src: url(../fonts/CormorantGaramond/CormorantGaramond-Bold.ttf);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Cormorant Garamond";
|
||||||
|
src: url(../fonts/CormorantGaramond/CormorantGaramond-Italic.ttf);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Cormorant Garamond";
|
||||||
|
src: url(../fonts/CormorantGaramond/CormorantGaramond-Light.ttf);
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Cormorant Garamond";
|
||||||
|
src: url(../fonts/CormorantGaramond/CormorantGaramond-Medium.ttf);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Cormorant Garamond";
|
||||||
|
src: url(../fonts/CormorantGaramond/CormorantGaramond-SemiBold.ttf);
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,11 @@ body {
|
||||||
padding-bottom: 1em;
|
padding-bottom: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
font-family: "Cormorant Garamond";
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--link-color);
|
color: var(--link-color);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
@ -33,10 +38,6 @@ hr {
|
||||||
max-width: 60ch;
|
max-width: 60ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-weight: 300;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
code {
|
||||||
font-family: "IBM Plex Mono", monospace;
|
font-family: "IBM Plex Mono", monospace;
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,7 +54,7 @@
|
||||||
<nav>
|
<nav>
|
||||||
<div>
|
<div>
|
||||||
<a href="index.html">Home</a>
|
<a href="index.html">Home</a>
|
||||||
<a href="blog.html">Articles</a>
|
<a href="articles.html">Articles</a>
|
||||||
<a href="projects.html">Projects</a>
|
<a href="projects.html">Projects</a>
|
||||||
<a class="page-header-selected" href="training.html">Training</a>
|
<a class="page-header-selected" href="training.html">Training</a>
|
||||||
<a href="contact.html">Contact</a>
|
<a href="contact.html">Contact</a>
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue