The first task of this week was to create a puzzle. I had to make puzzle peices that were 12x12 to match the cardboard. Then I had to put mmy blank cardboard into the printer so it could print my picture before using the laser cutter to create my puzle. I made a small mistake during the laser cutting portion. I measured out the length of the cardboard instead of the photo itsself so I have some white blank space, but its still easy to put the puzzle together.
The fitkit was really hard because to get the peices to stay together, the cardboard had to be measured.The diffculty was using CorelDraw to make the slits in the shapes big or small enough to aactual work. The actual making of the shapes was the easy part because the this point I had been using the laser cutter almost every class.
The box was one of the funner projects I think becausde it was so simple. To get the design for my box I used Makercase files that Mr.Gerber linked our claass to. Then I uploaded it to CorelDraw and inserted a picture of my first intal. Then I had to make sure to make the C on my box not hiarline because I wanted it to be cut into. Then I used cardboard, and but it all into the laser cutter. Then when it was finsihed I decided to super glue the sides together because for some reason they wouldn't stay.
Out of the many choices we had for the particular assignment, I decided to do a coaster, a bookmark, aand a design on glass. The coaster was a little tricker because it's a circle, so I had to measure it as though it was a square. And I was a littl eoff so the deisgn is a little too high becuase I didn't want it to be too low.
 I wanted the oppurtinty to learn the different of the lazer cutting machine. because Making a design on glass was a really new process because I had use the laser cutter before but on only on cardboard. First I had upload my file into CorelDraw and make it so it would brint horazinatlly. Then I had to learn from Mr.Gerber that theres an extra piece for the lazer cutter to cut on glass. This piece roattes it. And you have make sure the measurments are't off. After inserting it into the piece I then had to make sure that it was level enough. And becuase my design was basically an outline I didn't have to worry about my glass breaking.
For the final part of my required assignments, we had were challanged to engrave on a bookamrk. Though it was easy I still had to learn that the settings for the engraving are different for every type of material.


Survery
Please help up improve by answering a few questions by following the link to the survery https://www.research.net/r/BPJSGJC
{{ partial "meta.html"}}
Please help up improve by answering a few questions by following the link to the survery https://www.research.net/r/BPJSGJC


We are using the freshest ingredients, and make our cupcakes fresh everyday! Here are a few of our favorite choices!
Peanut Butter!
Chocolate!
Birthday Cake!
Strawberry!
Sprinkles!
Vanilla!
Our customers favorite cupcake is the Zoo Buddies!


|
muSE
r716
|
Functions | |
| muse_cell | fn_sort_inplace (muse_env *env, void *context, muse_cell args) |
| muse_cell | fn_sort (muse_env *env, void *context, muse_cell args) |
| Just like fn_sort_inplace(), except that the original list is not modified. More... | |
| muse_cell | fn_reverse (muse_env *env, void *context, muse_cell args) |
| muse_cell | fn_reverse_inplace (muse_env *env, void *context, muse_cell args) |
Sorts a list in place according to an optional property function. The first argument is the list of objects to sort. The cells in the list are replaced with the same objects in sorted order. The usual object ordering is used, which will sort a numeric list in ascending order.
A propertyFn can be specified to control the sorting order. The ordering of two objects in the list is determined by the ordering of the objects resulting from the application of the property function to the list objects.
This method is not as flexible as giving a comparator function, but it is good enough for most purposes in a scripting language and is more efficient than giving a comparator function. For example, if you want to sort a numeric list in descending order instead of ascending order, you can write -
instead of
If the list consists of lists, then you can use the propertyFn to select an element of the list entries to sort by.
References fn_sort_inplace(), muse_add_recent_item(), and MUSE_NIL.
Referenced by fn_sort_inplace().
Just like fn_sort_inplace(), except that the original list is not modified.
A new list of the same objects in sorted order is returned.
References fn_sort(), muse_add_recent_item(), and MUSE_NIL.
Referenced by fn_sort().
Evaluates to a list with items in the reverse order of the given list. The given list is not modified.
References fn_reverse(), muse_add_recent_item(), and MUSE_NIL.
Referenced by fn_reverse().
Evaluates to a list with items in the reverse order of the given list. The given list is reversed in-place to conserve memory, therefore the cell given as input is left pointing to the last element of the list. Use
if you want to modify a variable containing a list to its reversed version.
References fn_reverse_inplace(), muse_add_recent_item(), MUSE_NIL, and muse_tail().
Referenced by fn_reverse_inplace().


| Beer Image | Name | Description | Price |
|---|---|---|---|
| Corona Extra 330ml | Corona extra is a pale lager produced by .... | $1.90 | |
| Stella Artois 330ml | Stella Artois is a pale lager produced by .... | $1.80 | |
| Carlsberg 330ml | Carlsberg is a pale lager produced by .... | $1.60 | |
| Heineken 330ml | Heineken is a pale lager produced by .... | $1.80 |


| SDG 6:
Ensure availability and sustainable management of water and sanitation for all
|
Fresh water resource management, safe drinking water, sanitation facilities, water scarcity, flooding, wastewater management and efficiency of water use. |
| SDG 13:
Take urgent action to combat climate change and its impacts
|
Strengthen resilience and adaptive capacity to climate-related hazards, mitigate risks where possible, lower greenhouse gas emissions, improve education, awareness-raising and human and institutional capacity, and implement early warning systems. |
| SDG 14:
Conserve and sustainably use the oceans, seas and marine resources for sustainable development
|
Sustainable use and conservation of oceans, overfishing, ocean acidification, expansion of protected areas for marine biodiversity, intensification of research capacity to preserve marine resources and reduce marine pollution. |


Resepsi Mewah Citra Kirana & Rezky Aditya, Ginanjar Lamar Gadis Belia 09 Dec 2019, 17:00 by Uploader Related Videos


This experience is meant for diabetics. As a diabetic, I thought it would be a good idea to know what effects drugs will have on my before I take them. However, I couldn't find any information. So I took it and now I have experience to share. So this one is for all those curious, reckless diabetics.
I've taken DXM three times now. Each time I have increased the dosage; first at about 250 mg, next at about 340, and the last time, up to 370 or so. I'm a diabetic, and have therefore been very careful (relatively speaking of course-I'm a dysfunctional human being after all) each time I've taken it. My experience deals not so much with the effects of the drug on my mind as on my body. As a diabetic, I've wondered what effects new drugs will have on my blood sugar, and metabolism as a whole. DXM, like other psychodelic drugs, seems to shut down my metabolism for a short time.
Even after eating sugar, in starburst form, my blood sugar doesn't necessarily rise within a half hour as it normally does. My simple, but maybe wrong, reasoning accounts for this: DXM is a poison, and perhaps my body stops metabolising 'food' once the poison has entered into my metabolic equation. I'm pretty sure that it digests the food, it's just a matter of whether or not the sugar makes it to my blood stream, and consequently my brain and body. In any case, my experience is that taking food doesn't have it's normal effect of increasing my blood sugar.
For this reason I like to walk into the trip with my blood sugar on the high side. Although this has bad long term effects on my body, I deem the short-term safety to be worth it. Once the DXM has been metabolised, sugar that I'd previously eaten does raise my blood sugar; it just seems to be delayed. So there you have it.
Experience Reports are the writings and opinions of the individual authors who submit them.
Some of the activities described are dangerous and/or illegal and none are recommended by Erowid.


User Contribution Search
This tool will search through a page's history and return edits made by a particular user on that page.
Expand the "Help & Tips" section at the bottom of this page for help with using this tool effectively.
Help & Tips ↓
- Don't resolve redirects — Normally, the tool will try to resolve redirects, so if you use "WP:ANI" for the page name, it will resolve it to "Wikipedia:Administrators' noticeboard/Incidents". However, in the rare case that you want to list a user's contributions to a page that is currently a redirect, check this checkbox.
- Allow wildcards in page title — To search through a range of pages, use asterisks in the page name for wildcards. Note that wildcards do not work in the namespace, so "User*:Snottywong" won't return edits from "User:Snottywong" and "User talk:Snottywong". Use wildcards only after the colon.
- Page title is case sensitive — When wildcards are enabled, the pages search is not case sensitive unless this checkbox is checked. This option has no effect when wildcards are not enabled.


|
Serge J. BelongieAssistant ProfessorDepartment of Computer Science and Engineering EBU3B Room 4118 University of California, San Diego
Fax:<PHONE_NUMBER>
Office Hours (wi07): Monday 5-6pm, Thursday 4-5pm.
|
| COGS 87C | Computation and Vision in Film [Winter '06] |
| CSE 190A | Projects in Vision & Learning [Winter '07] [Winter '06] |
| CSE 166 | Image Processing [Fall '06] [Fall '05] [Fall '04] [Fall '03] [Fall '02] [Winter '02] |
| CSE 252 | Computer Vision [Spring '03] [Spring '02] |
| CSE 252B | Computer Vision II [Spring '06] [Spring '05] [Spring '04] |
| CSE 252C | Selected Topics in Vision & Learning [Fall '06] [Fall '05] [Fall '04] [Fall '03] [Fall '02] [Fall '01] |
| ENG 100L | Team Engineering Laboratory [Winter '07] |
PUBLICATIONS, RESEARCH, ADVISEES, BRIEF BIOGRAPHY, CURRICULUM VITAE
CONTACT & TRAVEL INFORMATION, INFORMATION FOR PROSPECTIVE STUDENTS
Last updated January 14, 2007.


After a couple of months getting into ruby on rails I got back to .NET development, as well as back to NBlogr and back to blogging.
Now for replies I saw on some blogs the format < replied 1 month, 5 days, 2 hours and 13 minutes ago. I wanted to find out how they did that and my first reaction was.. yay easy hello timespan.
Turns out that timespan only goes up to days, no months or anything. A bit deeper hidden for a c# programmer there is the function datediff in vb.net But that function doesn’t handle years properly for example the difference between today 01/01/2007 15:15 and 31/12/2007 18:00 gives a difference of 1 year instead of 1 day. So I decided to write my own class for it, turns out that the solution is quite simple.
To use the class below one would do :
NBlogr.Common.TimeSpan ts = new NBlogr.Common.TimeSpan(someStartDate, someEndDate);
string humantime = ts.ToString() + ” ago”;
The class :
/*
* Created by: Ivan Porto Carrero
* Created: Thursday, 12 April 2007
*/
using System;
using System.IO;
using System.Text;
namespace NBlogr.Common
{
public class TimeSpan
{
private DateTime startDate;
private DateTime endDate;
private int startDay;
private int endDay;
private int startMonth;
private int endMonth;
private int startYear;
private int endYear;
private int startHour;
private int endHour;
private int startMinute;
private int endMinute;
private int startSecond;
private int endSecond;
private int diffMonth;
private int diffDays;
private int diffHours;
private int diffMinutes;
private int diffYear;
private int diffSeconds;
private bool hasCalculated;
public TimeSpan()
{
hasCalculated = false;
}
public TimeSpan(DateTime startDate, DateTime endDate)
{
StartDate = startDate;
EndDate = endDate;
}
private void initialize()
{
startDay = StartDate.Day;
endDay = EndDate.Day;
startMonth = StartDate.Month;
endMonth = EndDate.Month;
startYear = StartDate.Year;
endYear = EndDate.Year;
startHour = StartDate.Hour;
endHour = EndDate.Hour;
startMinute = StartDate.Minute;
endMinute = EndDate.Minute;
startSecond = StartDate.Second;
endSecond = EndDate.Second;
}
public override string ToString()
{
StringBuilder sb = new StringBuilder();
if (Years > 0)
sb.Append(pluralise(“year”, Years));
if (Months > 0 && Years > 0)
sb.Append(“, “);
if (Months > 0)
sb.Append(pluralise(“month”, Months));
if ((Months > 0 || Years > 0) && Days > 0)
sb.Append(“, “);
if (Days > 0)
sb.Append(pluralise(“day”, Days));
if ((Months > 0 || Years > 0 || Days > 0) && Hours > 0)
sb.Append(“, “);
if (Hours > 0)
sb.Append(pluralise(“hour”, Hours));
if ((Months > 0 || Years > 0 || Days > 0 || Hours > 0) && Minutes > 0)
sb.Append(“, “);
if (Minutes > 0)
sb.Append(pluralise(“minute”, Minutes));
if ((Months > 0 || Years > 0 || Days > 0 || Hours > 0 || Minutes > 0) && Seconds > 0)
sb.Append(“, “);
if (Seconds > 0)
sb.Append(pluralise(“second”, Seconds));
return sb.ToString();
}
private void calculate()
{
initialize();
diffYear = endYear - startYear;
calculateMonths();
calculateDays();
calculateHours();
calculateMinutes();
calculateSeconds();
hasCalculated = true;
}
private static string pluralise(string singular, int quantity)
{
return quantity != 1 ? string.Format(“{0} {1}”, quantity, singular + “s”) : string.Format(“{0} {1}”, quantity, singular);
}
private void calculateMonths()
{
if (endMonth >= startMonth)
diffMonth = endMonth - startMonth;
else
{
diffYear–;
diffMonth = (12 - startMonth) + endMonth;
}
}
private void calculateDays()
{
if (endDay >= startDay)
diffDays = endDay - startDay;
else
{
diffMonth–;
diffDays = (DateTime.DaysInMonth(startYear, startMonth) - startDay) + endDay;
}
}
private void calculateHours()
{
if (endHour >= startHour)
diffHours = endHour - startHour;
else
{
diffDays–;
diffHours = (24 - startHour) + endHour;
}
}
private void calculateMinutes()
{
if (endMinute >= startMinute)
diffMinutes = endMinute - startMinute;
else
{
diffHours–;
diffMinutes = 60 - startMinute + endMinute;
}
}
private void calculateSeconds()
{
if (endSecond >= startSecond)
diffSeconds = endSecond - startSecond;
else
{
diffMinutes–;
diffSeconds = 60 - startSecond + endSecond;
}
}
public int Months
{
get
{
if (!hasCalculated)
calculate();
return diffMonth;
}
set { diffMonth = value; }
}
public int Days
{
get
{
if (!hasCalculated)
calculate();
return diffDays;
}
set { diffDays = value; }
}
public int Hours
{
get
{
if (!hasCalculated)
calculate();
return diffHours;
}
set { diffHours = value; }
}
public int Minutes
{
get
{
if (!hasCalculated)
calculate();
return diffMinutes;
}
set { diffMinutes = value; }
}
public int Years
{
get
{
if(!hasCalculated)
calculate();
return diffYear;
}
set { diffYear = value; }
}
public DateTime StartDate
{
get
{
if (startDate == null)
throw new InvalidDataException(“You need to specify a start date”);
return startDate;
}
set
{
hasCalculated = false;
startDate = value;
}
}
public DateTime EndDate
{
get
{
if (endDate == null)
throw new InvalidDataException(“You need to specify a end date”);
return endDate;
}
set
{
hasCalculated = false;
endDate = value;
}
}
public int Seconds
{
get { return diffSeconds; }
set { diffSeconds = value; }
}
}
}


Football Shirts Ireland
Home
Shop
(current)
Contact
About Us
Login
Checkout
LOGIN
The username or password you entered is incorrect.
Forgot your password?
Email address
Password
Need to Register?


Once at the second step
Once at the second step
Once at the second step
Once at the second step
ZuoJinSong
He is a great man ,he is very cool and clever
ZuoJinSong
He is a great man ,he is very cool and clever
ZuoJinSong
He is a great man ,he is very cool and clever
ZuoJinSong
He is a great man ,he is very cool and clever
ZuoJinSong
He is a great man ,he is very cool and clever
10
Once at the second step
Once at the second step
99
Once at the second step
50
Once at the second step


Esperance Shire recommends approving Castletown holiday home
by Jake DietschDespite neighbours being divided, Shire of Esperance staff have recommended council approve an application for a holiday house in Castletown.
Gregory Hill has applied for a development application to change a 'dwelling' to a 'holiday house' at 4, Cavanagh Court.
Shire manager of strategic planning Richard Hindley advised council to approve the application subject to conditions.
There are five properties adjoining the address, with two under the same ownership. Two neighbours expressed support and two objected.
At a council briefing session on Tuesday, November 19, Mr Hill's neighbour, Jen Pusey, spoke against the proposal.
Mrs Pusey said she had learned, from past experiences, short term renting was an "unsupervised business" and "rules were wonderful on paper but often ignored".
These experiences were not related to Mr Hill.
Mrs Pusey was also concerned the holiday home would cause additional parking that would obstruct traffic.
Another neighbour, Heather Creedon, told council the area had many elderly residents.
"Everyone with a holiday let earns good money, but it can be a bane for those that live around and beside them. It has been a nightmare in the past living next door to one," Mrs Creedon said.
In a written objection she said, "when greed and making money becomes the focus in this sort of enterprise, other people around them suffer and are not financially compensated for the aggravation and stress short term stays in quiet streets present".
Mr Hill told council he brought the property four to five months ago.
He admitted he lived in Perth, but said he had businesses in Esperance, spent half of his time here and had nominated his niece to be the primary contact.
Mr Hill said the house would only be used 30 per cent of the time by holiday makers. He said he and his family would use the house the rest of the time.
According to the applicant, at least five cars can park on his property, so obstructing traffic was not an issue.
Mr Hill said Mrs Creedon, lived on a farm and the neighbourhood wasn't her primary residence.
"Heather, I believe this must be your holiday home," he said.
Mr Hill said he wanted to reconnect to Castletown Beach, where he'd grown up.
"It's not about income. There's better ways of making money," he said.
Mr Hill has promised to limit the number of guests, vet them and impose 'quiet hours' .
The applicant also said he would not use sites such as Airbnb. Mr Hill told council respect was "utmost" to him.
Mr Hindley said there were more than 100 holiday homes in the shire and a new policy would address concerns raised.
He said the proposal was compatible with the surrounding area as long as it was adequately managed and conditions were adhered to.
Council is set to vote on the matter on Tuesday, November 26.


Penalty charge cancelled
You do not have to pay any charges
Michelle Doe
Penalty charge notice:<PHONE_NUMBER>
Save a copyThank you, we’ve found your exemption using new details you have given us today.
Why did I receive a Penalty Charge Notice?
The details on your exemption certificate do not match those taken from your prescription. Checks for exemptions are done automatically using these details from your prescription:
- Name
- Address
- Date of birth
One or more of the above details on your certificate may be different to the details on your prescription.
-
NHS number
Your NHS number may have been missing from your prescription.
Make sure these are correct on your NHS exemption certificate, and at your GP surgery, dental practice, and any benefits organisations. If they’re not, you might get another Penalty Charge Notice.
How to check these details are correct on your NHS exemption
For medical and maternity exemption certificates, and prescription prepayment certificates
- Call us on 0300 330 1341
For NHS tax credit exemption certificates
- Call us on 0300 330 1347
For HC2 and HC3 certificates
- Call us on 0300 330 1343
Lines are available Monday to Friday from 8am to 6pm and Saturday from 9am to 3pm (Your call will be charged at a local rate).
Feedback
Leaving feedback only takes 5 minutes. Your comments are extremely helpful and will help us to provide a better service.
{{hasmail}}


unshadow
Utility provided by the John the Ripper project to obtain the traditional Unix password file if the system uses shadow passwords.
More information: home page
- Combine the `/etc/shadow` and `/etc/passwd` of the current system:
sudo unshadow /etc/passwd /etc/shadow
- Combine two arbitrary shadow and password files:
sudo unshadow path/to/passwd path/to/shadow


Single expansion panel
Multiple expansion panel
The Golden Retriever is a large-sized breed of dog bred as gun dogs to retrieve shot waterfowl such as ducks and upland game birds during hunting and shooting parties, and were named 'retriever' because of their ability to retrieve shot game undamaged. Golden Retrievers have an instinctive love of water, and are easy to train to basic or advanced obedience standards.
The Beagle is a breed of small hound, similar in appearance to the much larger foxhound. The beagle is a scent hound, developed primarily for hunting hare.
The dachshund is a short-legged, long-bodied, hound-type dog breed. The standard size dachshund was developed to scent, chase, and flush out badgers and other burrow-dwelling animals, while the miniature ...


Data doesn’t make for a very good tradable commodity. Technological changes that allow us to connect and share data more easily are disrupting established business models. Therefore the public sector has a critical role to intervene in the data market and the private sector must adapt to ensure long-term sustainability.
The Market Isn’t Always Perfect
Economists use this term to describe situations where the allocation of goods and services by the free market is inefficient (i.e. someone can be made better off without making another worse off). In some extreme cases the good or service would not be available without intervention by government. The market is only effective if the good or service:
- has a price which reflects all costs and benefits
- can be withheld from people at reasonable expense
- cannot be used by more than one person at the same time
These characteristics don’t apply in the case of open data…
Open Data: The Gift That Keeps On Giving
An externality, or spillover effect, is a cost or benefit that is incurred by someone who is not involved in the trade. The price will not reflect these indirect social impacts. Pollution is an example of how external costs can lead to excessive production.
Network externalities are a form of external benefit. Within a network the value of an adding an extra node or edge (e.g. person or friendship) is felt by every other part of the network. The Metrolink extension, for example, is not only of benefit to people in the new areas that can now use trams, but also to people in the existing areas that can now access new areas.
Network externalities occur in the case of open data because data is only informative when it is interpreted. The value of each datum increases with the volume of other data that it may be connected with as the context and range of analysis that are possible increases. Thus, data operates as a network. In deciding whether or not to open their data, private organisations will not consider the indirect benefits that their data will have to improve the quality of every other dataset and so will release less data than is socially optimal.
Data As A Public Good
A public good is one which is non-excludable (impossible to prevent people from using it) and non-rival (one person’s use doesn’t not reduce availability to another). The problem with these goods is that consumers can take advantage without contributing sufficiently to their creation. If too many people decide to free-ride then the private revenues won’t meet the private costs and the incentive to provide the good through the market will disappear.
Data is typically non-excludable. Although legislation may prohibit the buyer from sharing data it is, in many cases, practically unenforcable (as is apparent with music sharing, for example). Data is also non-rival. Your reading of this post does not prevent another person from reading it – indeed I don’t loose the ideas when I write them down – we can all enjoy them at the same time. Where data may be characterised as a public good, subject to the free-rider problem, there won’t be sufficient private incentives for production and distribution.
Implications For Business & Government
So what does this economic analysis tell us?
- Technological change (particularly the increasing capacity for connecting and sharing data) will disrupt certain markets by undermining established closed-data business models;
- Businesses that attempt to defend outdated business models by keeping data closed won’t be able to compete with an open alternatives (alternatives that they are encouraging by keeping their data closed);
- It’s probably better for such businesses to open their data before a competitor does – they shouldn’t waste their current position of dominance;
- In some cases, where the market fails completely, public intervention to fund the creation or provision of information will help to improve economic growth and social welfare;
- In other cases there will be considerable opportunities for private firms to innovate and capture the benefits of open data.


{% extends "bank/base.html" %} {% load crispy_forms_tags %} {% block body %}
Deleting a bank account cannot be undone.


Simple Random Search function for the 'smoof' and 'ParamHelpers' ecosystem with termination criteria and parallelization.
| Version: | 0.1.0 |
| Depends: | ParamHelpers (≥ 1.10), smoof (≥ 1.5.1) |
| Imports: | checkmate (≥ 1.8.2), parallelMap (≥ 1.3), fs |
| Suggests: | emoa, rmarkdown, testthat, knitr, roxygen2 |
| Published: | 2018-07-01 |
| Author: | Jakob Richter [aut, cre] |
| Maintainer: | Jakob Richter <code at jakob-r.de> |
| BugReports: | https://github.com/jakob-r/randomsearch/issues |
| License: | BSD_2_clause + file LICENSE |
| URL: | https://github.com/jakob-r/randomsearch |
| NeedsCompilation: | no |
| CRAN checks: | randomsearch results |
| Reference manual: | randomsearch.pdf |
| Vignettes: |
randomsearch: A brief introduction |
| Package source: | randomsearch_0.1.0.tar.gz |
| Windows binaries: | r-devel: randomsearch_0.1.0.zip, r-release: randomsearch_0.1.0.zip, r-oldrel: randomsearch_0.1.0.zip |
| OS X binaries: | r-release: randomsearch_0.1.0.tgz, r-oldrel: randomsearch_0.1.0.tgz |
Please use the canonical form https://CRAN.R-project.org/package=randomsearch to link to this page.


Abode’s DIY home security system is now compatible with HomeKit
HomeKit is available on the Iota today and will arrive on Gen 1 and Gen 2 soon.
by Christine FisherAbode's Iota smart home security device is now compatible with Apple HomeKit. If you're an iOS users, that means you'll be able to control your Iota through the Home app on your iPhone or iPad. You'll also be able to control any Abode-branded motion, door and window sensors through the Home app if they're connected to the Iota gateway.
The Iota gateway has a built-in 1080-pixel camera, which you can stream through the Home app. It has two-way voice and support for Z-Wave, Zigbee, Alexa and Google Home, making it one of the most flexible smart-home hubs to date.
HomeKit is available on the Iota through an update that's rolling out today, and Abode says the Gen 1 and Gen 2 security systems will add HomeKit compatibility "in the near future."


jQuery
Pour one for the homie...
A few haikus for you...


Many people want to write games using three.js. This article will hopefully give you some ideas on how to start.
At least at the time I'm writing this article it's probably going to be the longest article on this site. It's possible the code here is massively over engineered but as I wrote each new feature I'd run into a problem that needed a solution I'm used to from other games I've written. In other words each new solution seemed important so I'll try to show why. Of course the smaller your game the less you might need some of the solutions shown here but this is a pretty small game and yet with the complexities of 3D characters many things take more organization than they might with 2D characters.
As an example if you're making PacMan in 2D, when PacMan turns a corner that happens instantly at 90 degrees. There is no in-between step. But in a 3D game often we need the character to rotate over several frames. That simple change can add a bunch of complexity and require different solutions.
The majority of the code here will not really be three.js and that's important to note, three.js is not a game engine. Three.js is a 3D library. It provides a scene graph and features for displaying 3D objects added to that scene graph but it does not provide all the other things needed to make a game. No collisions, no physics, no input systems, no path finding, etc, etc... So, we'll have to provide those things ourselves.
I ended up writing quite a bit of code to make this simple unfinished game like thing and again, it's certainly possible I over engineered and there are simpler solutions but I feel like I actually didn't write enough code and hopefully I can explain what I think is missing.
Many of the ideas here are heavily influenced by Unity. If you're not familiar with Unity that probably does not matter. I only bring it up as 10s of 1000s of games have shipped using these ideas.
Let's start with the three.js parts. We need to load models for our game.
At opengameart.org I found this animated knight model by quaternius
quaternius also made these animated animals.
These seem like good models to start with so the first thing we need to do is load them.
We covered loading glTF files before. The difference this time is we need to load multiple models and we can't start the game until all the models are loaded.
Fortunately three.js provides the LoadingManager
just for this purpose.
We create a LoadingManager
and pass it to the other loaders. The
LoadingManager
provides both onProgress
and
onLoad
properties we can attach callbacks to.
The onLoad
callback will be called when
all files have been loaded. The onProgress
callback
as called after each individual file arrives to give as a chance to show
loading progress.
Starting with the code from loading a glTF file I removed all the code related to framing the scene and added this code to load all models.
const manager = new THREE.LoadingManager(); manager.onLoad = init; const models = { pig: { url: 'resources/models/animals/Pig.gltf' }, cow: { url: 'resources/models/animals/Cow.gltf' }, llama: { url: 'resources/models/animals/Llama.gltf' }, pug: { url: 'resources/models/animals/Pug.gltf' }, sheep: { url: 'resources/models/animals/Sheep.gltf' }, zebra: { url: 'resources/models/animals/Zebra.gltf' }, horse: { url: 'resources/models/animals/Horse.gltf' }, knight: { url: 'resources/models/knight/KnightCharacter.gltf' }, }; { const gltfLoader = new GLTFLoader(manager); for (const model of Object.values(models)) { gltfLoader.load(model.url, (gltf) => { model.gltf = gltf; }); } } function init() { // TBD }
This code will load all the models above and the LoadingManager
will call
init
when done. We'll use the models
object later to let us access the
loaded models so the GLTFLoader
callback for each individual model attaches
the loaded data to that model's info.
All the models with all their animation are currently about 6.6meg. That's a pretty big download. Assuming your server supports compression (the server this site runs on does) it's able to compress them to around 1.4meg. That's definitely better than 6.6meg bit it's still not a tiny amount of data. It would probably be good if we added a progress bar so the user has some idea how much longer they have to wait.
So, let's add an onProgress
callback. It will be
called with 3 arguments, the url
of the last loaded object and then the number
of items loaded so far as well as the total number of items.
Let's setup some HTML for a loading bar
 <canvas id="c"></canvas> +
 +
 +
...loading...
 +

 +
 +

We'll look up the #progressbar
div and we can set the width from 0% to 100%
to show our progress. All we need to do is set that in our callback.
const manager = new THREE.LoadingManager(); manager.onLoad = init; +const progressbarElem = document.querySelector('#progressbar'); +manager.onProgress = (url, itemsLoaded, itemsTotal) => { + progressbarElem.style.width = `${itemsLoaded / itemsTotal * 100 | 0}%`; +};
We already setup init
to be called when all the models are loaded so
we can turn off the progress bar by hiding the #loading
element.
function init() { + // hide the loading bar + const loadingElem = document.querySelector('#loading'); + loadingElem.style.display = 'none'; }
Here's a bunch of CSS for styling the bar. The CSS makes the #loading

the full size of the page and centers its children. The CSS makes a .progress
area to contain the progress bar. The CSS also gives the progress bar
a CSS animation of diagonal stripes.
#loading { position: absolute; left: 0; top: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; text-align: center; font-size: xx-large; font-family: sans-serif; } #loading>div>div { padding: 2px; } .progress { width: 50vw; border: 1px solid black; } #progressbar { width: 0; transition: width ease-out .5s; height: 1em; background-color: #888; background-image: linear-gradient( -45deg, rgba(255, 255, 255, .5) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .5) 50%, rgba(255, 255, 255, .5) 75%, transparent 75%, transparent ); background-size: 50px 50px; animation: progressanim 2s linear infinite; } @keyframes progressanim { 0% { background-position: 50px 50px; } 100% { background-position: 0 0; } }
Now that we have a progress bar let's deal with the models. These models
have animations and we want to be able to access those animations.
Animations are stored in an array by default be we'd like to be able to
easily access them by name so let's setup an animations
property for
each model to do that. Note of course this means animations must have unique names.
+function prepModelsAndAnimations() { + Object.values(models).forEach(model => { + const animsByName = {}; + model.gltf.animations.forEach((clip) => { + animsByName[clip.name] = clip; + }); + model.animations = animsByName; + }); +} function init() { // hide the loading bar const loadingElem = document.querySelector('#loading'); loadingElem.style.display = 'none'; + prepModelsAndAnimations(); }
Let's display the animated models.
Unlike the previous example of loading a glTF file
This time we probably want to be able to display more than one instance
of each model. To do this, instead of adding
the loaded gltf scene directly like we did in the article on loading a glTF,
we instead want to clone the scene and in particular we want to clone
it for skinned animated characters. Fortunately there's a utility function,
SkeletonUtils.clone
we can use to do this. So, first we need to include
the utils.
import * as THREE from 'three'; import {OrbitControls} from 'three/addons/controls/OrbitControls.js'; import {GLTFLoader} from 'three/addons/loaders/GLTFLoader.js'; +import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils.js';
Then we can clone the models we just loaded
function init() { // hide the loading bar const loadingElem = document.querySelector('#loading'); loadingElem.style.display = 'none'; prepModelsAndAnimations(); + Object.values(models).forEach((model, ndx) => { + const clonedScene = SkeletonUtils.clone(model.gltf.scene); + const root = new THREE.Object3D(); + root.add(clonedScene); + scene.add(root); + root.position.x = (ndx - 3) * 3; + }); }
Above, for each model, we clone the gltf.scene
we loaded and we parent that
to a new Object3D
. We need to parent it to another object because when
we play animations the animation will apply animated positions to the nodes
in the loaded scene which means we won't have control over those positions.
To play the animations each model we clone needs an AnimationMixer
.
An AnimationMixer
contains 1 or more AnimationAction
s. An
AnimationAction
references an AnimationClip
. AnimationAction
s
have all kinds of settings for playing then chaining to another
action or cross fading between actions. Let's just get the first
AnimationClip
and create an action for it. The default is for
an action to play its clip in a loop forever.
+const mixers = []; function init() { // hide the loading bar const loadingElem = document.querySelector('#loading'); loadingElem.style.display = 'none'; prepModelsAndAnimations(); Object.values(models).forEach((model, ndx) => { const clonedScene = SkeletonUtils.clone(model.gltf.scene); const root = new THREE.Object3D(); root.add(clonedScene); scene.add(root); root.position.x = (ndx - 3) * 3; + const mixer = new THREE.AnimationMixer(clonedScene); + const firstClip = Object.values(model.animations)[0]; + const action = mixer.clipAction(firstClip); + action.play(); + mixers.push(mixer); }); }
We called play
to start the action and stored
off all the AnimationMixers
in an array called mixers
. Finally
we need to update each AnimationMixer
in our render loop by computing
the time since the last frame and passing that to AnimationMixer.update
.
+let then = 0; function render(now) { + now *= 0.001; // convert to seconds + const deltaTime = now - then; + then = now; if (resizeRendererToDisplaySize(renderer)) { const canvas = renderer.domElement; camera.aspect = canvas.clientWidth / canvas.clientHeight; camera.updateProjectionMatrix(); } + for (const mixer of mixers) { + mixer.update(deltaTime); + } renderer.render(scene, camera); requestAnimationFrame(render); }
And with that we should get each model loaded and playing its first animation.
Let's make it so we can check all of the animations. We'll add all of the clips as actions and then enable just one at a time.
-const mixers = []; +const mixerInfos = []; function init() { // hide the loading bar const loadingElem = document.querySelector('#loading'); loadingElem.style.display = 'none'; prepModelsAndAnimations(); Object.values(models).forEach((model, ndx) => { const clonedScene = SkeletonUtils.clone(model.gltf.scene); const root = new THREE.Object3D(); root.add(clonedScene); scene.add(root); root.position.x = (ndx - 3) * 3; const mixer = new THREE.AnimationMixer(clonedScene); - const firstClip = Object.values(model.animations)[0]; - const action = mixer.clipAction(firstClip); - action.play(); - mixers.push(mixer); + const actions = Object.values(model.animations).map((clip) => { + return mixer.clipAction(clip); + }); + const mixerInfo = { + mixer, + actions, + actionNdx: -1, + }; + mixerInfos.push(mixerInfo); + playNextAction(mixerInfo); }); } +function playNextAction(mixerInfo) { + const {actions, actionNdx} = mixerInfo; + const nextActionNdx = (actionNdx + 1) % actions.length; + mixerInfo.actionNdx = nextActionNdx; + actions.forEach((action, ndx) => { + const enabled = ndx === nextActionNdx; + action.enabled = enabled; + if (enabled) { + action.play(); + } + }); +}
The code above makes an array of AnimationAction
s,
one for each AnimationClip
. It makes an array of objects, mixerInfos
,
with references to the AnimationMixer
and all the AnimationAction
s
for each model. It then calls playNextAction
which sets enabled
on
all but one action for that mixer.
We need to update the render loop for the new array
-for (const mixer of mixers) { +for (const {mixer} of mixerInfos) { mixer.update(deltaTime); }
Let's make it so pressing a key 1 to 8 will play the next animation for each model
window.addEventListener('keydown', (e) => { const mixerInfo = mixerInfos[e.keyCode - 49]; if (!mixerInfo) { return; } playNextAction(mixerInfo); });
Now you should be able to click on the example and then press keys 1 through 8 to cycle each of the models through their available animations.
So that is arguably the sum-total of the three.js portion of this
article. We covered loading multiple files, cloning skinned models,
and playing animations on them. In a real game you'd have to do a
ton more manipulation of AnimationAction
objects.
Let's start making a game infrastructure
A common pattern for making a modern game is to use an Entity Component System. In an Entity Component System an object in a game is called an entity that consists of a bunch of components. You build up entities by deciding which components to attach to them. So, let's make an Entity Component System.
We'll call our entities GameObject
. It's effectively just a collection
of components and a three.js Object3D
.
function removeArrayElement(array, element) { const ndx = array.indexOf(element); if (ndx >= 0) { array.splice(ndx, 1); } } class GameObject { constructor(parent, name) { this.name = name; this.components = []; this.transform = new THREE.Object3D(); parent.add(this.transform); } addComponent(ComponentType, ...args) { const component = new ComponentType(this, ...args); this.components.push(component); return component; } removeComponent(component) { removeArrayElement(this.components, component); } getComponent(ComponentType) { return this.components.find(c => c instanceof ComponentType); } update() { for (const component of this.components) { component.update(); } } }
Calling GameObject.update
calls update
on all the components.
I included a name only to help in debugging so if I look at a GameObject
in the debugger I can see a name to help identify it.
Some things that might seem a little strange:
GameObject.addComponent
is used to create components. Whether or not
this a good idea or a bad idea I'm not sure. My thinking was it makes
no sense for a component to exist outside of a gameobject so I thought
it might be good if creating a component automatically added that component
to the gameobject and passed the gameobject to the component's constructor.
In other words to add a component you do this
const gameObject = new GameObject(scene, 'foo'); gameObject.addComponent(TypeOfComponent);
If I didn't do it this way you'd instead do something like this
const gameObject = new GameObject(scene, 'foo'); const component = new TypeOfComponent(gameObject); gameObject.addComponent(component);
Is it better that the first way is shorter and more automated or is it worse because it looks out of the ordinary? I don't know.
GameObject.getComponent
looks up components by type. That has
the implication that you can not have 2 components of the same
type on a single game object or at least if you do you can only
look up the first one without adding some other API.
It's common for one component to look up another and when looking them up they have to match by type otherwise you might get the wrong one. We could instead give each component a name and you could look them up by name. That would be more flexible in that you could have more than one component of the same type but it would also be more tedious. Again, I'm not sure which is better.
On to the components themselves. Here is their base class.
// Base for all components class Component { constructor(gameObject) { this.gameObject = gameObject; } update() { } }
Do components need a base class? JavaScript is not like most strictly typed languages so effectively we could have no base class and just leave it up to each component to do whatever it wants in its constructor knowing that the first argument is always the component's gameobject. If it doesn't care about gameobject it wouldn't store it. I kind of feel like this common base is good though. It means if you have a reference to a component you know you can find its parent gameobject always and from its parent you can easily look up other components as well as look at its transform.
To manage the gameobjects we probably need some kind of gameobject manager. You might think we could just keep an array of gameobjects but in a real game the components of a gameobject might add and remove other gameobjects at runtime. For example a gun gameobject might add a bullet gameobject every time the gun fires. A monster gameobject might remove itself if it has been killed. We then would have an issue that we might have code like this
for (const gameObject of globalArrayOfGameObjects) { gameObject.update(); }
The loop above would fail or do un-expected things if
gameobjects are added or removed from globalArrayOfGameObjects
in the middle of the loop in some component's update
function.
To try to prevent that problem we need something a little safer. Here's one attempt.
class SafeArray { constructor() { this.array = []; this.addQueue = []; this.removeQueue = new Set(); } get isEmpty() { return this.addQueue.length + this.array.length > 0; } add(element) { this.addQueue.push(element); } remove(element) { this.removeQueue.add(element); } forEach(fn) { this._addQueued(); this._removeQueued(); for (const element of this.array) { if (this.removeQueue.has(element)) { continue; } fn(element); } this._removeQueued(); } _addQueued() { if (this.addQueue.length) { this.array.splice(this.array.length, 0, ...this.addQueue); this.addQueue = []; } } _removeQueued() { if (this.removeQueue.size) { this.array = this.array.filter(element => !this.removeQueue.has(element)); this.removeQueue.clear(); } } }
The class above lets you add or remove elements from the SafeArray
but won't mess with the array itself while it's being iterated over. Instead
new elements get added to addQueue
and removed elements to the removeQueue
and then added or removed outside of the loop.
Using that here is our class to manage gameobjects.
class GameObjectManager { constructor() { this.gameObjects = new SafeArray(); } createGameObject(parent, name) { const gameObject = new GameObject(parent, name); this.gameObjects.add(gameObject); return gameObject; } removeGameObject(gameObject) { this.gameObjects.remove(gameObject); } update() { this.gameObjects.forEach(gameObject => gameObject.update()); } }
With all that now let's make our first component. This component
will just manage a skinned three.js object like the ones we just created.
To keep it simple it will just have one method, setAnimation
that
takes the name of the animation to play and plays it.
class SkinInstance extends Component { constructor(gameObject, model) { super(gameObject); this.model = model; this.animRoot = SkeletonUtils.clone(this.model.gltf.scene); this.mixer = new THREE.AnimationMixer(this.animRoot); gameObject.transform.add(this.animRoot); this.actions = {}; } setAnimation(animName) { const clip = this.model.animations[animName]; // turn off all current actions for (const action of Object.values(this.actions)) { action.enabled = false; } // get or create existing action for clip const action = this.mixer.clipAction(clip); action.enabled = true; action.reset(); action.play(); this.actions[animName] = action; } update() { this.mixer.update(globals.deltaTime); } }
You can see it's basically the code we had before that clones the scene we loaded,
then sets up an AnimationMixer
. setAnimation
adds a AnimationAction
for a
particular AnimationClip
if one does not already exist and disables all
existing actions.
The code references globals.deltaTime
. Let's make a globals object
const globals = { time: 0, deltaTime: 0, };
And update it in the render loop
let then = 0; function render(now) { // convert to seconds globals.time = now * 0.001; // make sure delta time isn't too big. globals.deltaTime = Math.min(globals.time - then, 1 / 20); then = globals.time;
The check above for making sure deltaTime
is not more than 1/20th
of a second is because otherwise we'd get a huge value for deltaTime
if we hide the tab. We might hide it for seconds or minutes and then
when our tab was brought to the front deltaTime
would be huge
and might teleport characters across our game world if we had code like
position += velocity * deltaTime;
By limiting the maximum deltaTime
that issue is prevented.
Now let's make a component for the player.
class Player extends Component { constructor(gameObject) { super(gameObject); const model = models.knight; this.skinInstance = gameObject.addComponent(SkinInstance, model); this.skinInstance.setAnimation('Run'); } }
The player calls setAnimation
with 'Run'
. To know which animations
are available I modified our previous example to print out the names of
the animations
function prepModelsAndAnimations() { Object.values(models).forEach(model => { + console.log('------->:', model.url); const animsByName = {}; model.gltf.animations.forEach((clip) => { animsByName[clip.name] = clip; + console.log(' ', clip.name); }); model.animations = animsByName; }); }
And running it got this list in the JavaScript console.
------->: resources/models/animals/Pig.gltf Idle Death WalkSlow Jump Walk ------->: resources/models/animals/Cow.gltf Walk Jump WalkSlow Death Idle ------->: resources/models/animals/Llama.gltf Jump Idle Walk Death WalkSlow ------->: resources/models/animals/Pug.gltf Jump Walk Idle WalkSlow Death ------->: resources/models/animals/Sheep.gltf WalkSlow Death Jump Walk Idle ------->: resources/models/animals/Zebra.gltf Jump Walk Death WalkSlow Idle ------->: resources/models/animals/Horse.gltf Jump WalkSlow Death Walk Idle ------->: resources/models/knight/KnightCharacter.gltf Run_swordRight Run Idle_swordLeft Roll_sword Idle Run_swordAttack
Fortunately the names of the animations for all the animals match
which will come in handy later. For now we only care the that the
player has an animation called Run
.
Let's use these components. Here's the updated init function.
All it does is create a GameObject
and add a Player
component to it.
const globals = { time: 0, deltaTime: 0, }; +const gameObjectManager = new GameObjectManager(); function init() { // hide the loading bar const loadingElem = document.querySelector('#loading'); loadingElem.style.display = 'none'; prepModelsAndAnimations(); + { + const gameObject = gameObjectManager.createGameObject(scene, 'player'); + gameObject.addComponent(Player); + } }
And we need to call gameObjectManager.update
in our render loop
let then = 0; function render(now) { // convert to seconds globals.time = now * 0.001; // make sure delta time isn't too big. globals.deltaTime = Math.min(globals.time - then, 1 / 20); then = globals.time; if (resizeRendererToDisplaySize(renderer)) { const canvas = renderer.domElement; camera.aspect = canvas.clientWidth / canvas.clientHeight; camera.updateProjectionMatrix(); } - for (const {mixer} of mixerInfos) { - mixer.update(deltaTime); - } + gameObjectManager.update(); renderer.render(scene, camera); requestAnimationFrame(render); }
and if we run that we get a single player.
That was a lot of code just for an entity component system but it's infrastructure that most games need.
Let's add an input system. Rather than read keys directly we'll
make a class that other parts of the code can check left
or right
.
That way we can assign multiple ways to input left
or right
etc..
We'll start with just keys
// Keeps the state of keys/buttons // // You can check // // inputManager.keys.left.down // // to see if the left key is currently held down // and you can check // // inputManager.keys.left.justPressed // // To see if the left key was pressed this frame // // Keys are 'left', 'right', 'a', 'b', 'up', 'down' class InputManager { constructor() { this.keys = {}; const keyMap = new Map(); const setKey = (keyName, pressed) => { const keyState = this.keys[keyName]; keyState.justPressed = pressed && !keyState.down; keyState.down = pressed; }; const addKey = (keyCode, name) => { this.keys[name] = { down: false, justPressed: false }; keyMap.set(keyCode, name); }; const setKeyFromKeyCode = (keyCode, pressed) => { const keyName = keyMap.get(keyCode); if (!keyName) { return; } setKey(keyName, pressed); }; addKey(37, 'left'); addKey(39, 'right'); addKey(38, 'up'); addKey(40, 'down'); addKey(90, 'a'); addKey(88, 'b'); window.addEventListener('keydown', (e) => { setKeyFromKeyCode(e.keyCode, true); }); window.addEventListener('keyup', (e) => { setKeyFromKeyCode(e.keyCode, false); }); } update() { for (const keyState of Object.values(this.keys)) { if (keyState.justPressed) { keyState.justPressed = false; } } } }
The code above tracks whether keys are up or down and you can check
if a key is currently pressed by checking for example
inputManager.keys.left.down
. It also has a justPressed
property
for each key so that you can check the user just pressed the key.
For example a jump key you don't want to know if the button is being
held down, you want to know did the user press it now.
Let's create an instance of InputManager
const globals = { time: 0, deltaTime: 0, }; const gameObjectManager = new GameObjectManager(); +const inputManager = new InputManager();
and update it in our render loop
function render(now) { ... gameObjectManager.update(); + inputManager.update(); ... }
It needs to be called after gameObjectManager.update
otherwise
justPressed
would never be true inside a component's update
function.
Let's use it in the Player
component
+const kForward = new THREE.Vector3(0, 0, 1); const globals = { time: 0, deltaTime: 0, + moveSpeed: 16, }; class Player extends Component { constructor(gameObject) { super(gameObject); const model = models.knight; this.skinInstance = gameObject.addComponent(SkinInstance, model); this.skinInstance.setAnimation('Run'); + this.turnSpeed = globals.moveSpeed / 4; } + update() { + const {deltaTime, moveSpeed} = globals; + const {transform} = this.gameObject; + const delta = (inputManager.keys.left.down ? 1 : 0) + + (inputManager.keys.right.down ? -1 : 0); + transform.rotation.y += this.turnSpeed * delta * deltaTime; + transform.translateOnAxis(kForward, moveSpeed * deltaTime); + } }
The code above uses Object3D.transformOnAxis
to move the player
forward. Object3D.transformOnAxis
works in local space so it only
works if the object in question is at the root of the scene, not if it's
parented to something else 1
We also added a global moveSpeed
and based a turnSpeed
on the move speed.
The turn speed is based on the move speed to try to make sure a character
can turn sharply enough to meet its target. If turnSpeed
so too small
a character will turn around and around circling its target but never
hitting it. I didn't bother to do the math to calculate the required
turn speed for a given move speed. I just guessed.
The code so far would work but if the player runs off the screen there's no
way to find out where they are. Let's make it so if they are offscreen
for more than a certain time they get teleported back to the origin.
We can do that by using the three.js Frustum
class to check if a point
is inside the camera's view frustum.
We need to build a frustum from the camera. We could do this in the Player component but other objects might want to use this too so let's add another gameobject with a component to manage a frustum.
class CameraInfo extends Component { constructor(gameObject) { super(gameObject); this.projScreenMatrix = new THREE.Matrix4(); this.frustum = new THREE.Frustum(); } update() { const {camera} = globals; this.projScreenMatrix.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse); this.frustum.setFromProjectionMatrix(this.projScreenMatrix); } }
Then let's setup another gameobject at init time.
function init() { // hide the loading bar const loadingElem = document.querySelector('#loading'); loadingElem.style.display = 'none'; prepModelsAndAnimations(); + { + const gameObject = gameObjectManager.createGameObject(camera, 'camera'); + globals.cameraInfo = gameObject.addComponent(CameraInfo); + } { const gameObject = gameObjectManager.createGameObject(scene, 'player'); gameObject.addComponent(Player); } }
and now we can use it in the Player
component.
class Player extends Component { constructor(gameObject) { super(gameObject); const model = models.knight; this.skinInstance = gameObject.addComponent(SkinInstance, model); this.skinInstance.setAnimation('Run'); this.turnSpeed = globals.moveSpeed / 4; + this.offscreenTimer = 0; + this.maxTimeOffScreen = 3; } update() { - const {deltaTime, moveSpeed} = globals; + const {deltaTime, moveSpeed, cameraInfo} = globals; const {transform} = this.gameObject; const delta = (inputManager.keys.left.down ? 1 : 0) + (inputManager.keys.right.down ? -1 : 0); transform.rotation.y += this.turnSpeed * delta * deltaTime; transform.translateOnAxis(kForward, moveSpeed * deltaTime); + const {frustum} = cameraInfo; + if (frustum.containsPoint(transform.position)) { + this.offscreenTimer = 0; + } else { + this.offscreenTimer += deltaTime; + if (this.offscreenTimer >= this.maxTimeOffScreen) { + transform.position.set(0, 0, 0); + } + } } }
One more thing before we try it out, let's add touchscreen support for mobile. First let's add some HTML to touch
 <canvas id="c"></canvas> +
 +

 +

 +

 +

...loading...

and some CSS to style it
#ui { position: absolute; left: 0; top: 0; width: 100%; height: 100%; display: flex; justify-items: center; align-content: stretch; } #ui>div { display: flex; align-items: flex-end; flex: 1 1 auto; } .bright { filter: brightness(2); } #left { justify-content: flex-end; } #right { justify-content: flex-start; } #ui img { padding: 10px; width: 80px; height: 80px; display: block; }
The idea here is there is one div, #ui
, that
covers the entire page. Inside will be 2 divs, #left
and #right
both of which are almost half the page wide and the entire screen tall.
In between there is a 40px separator. If the user slides their finger
over the left or right side then we need up update keys.left
and keys.right
in the InputManager
. This makes the entire screen sensitive to being touched
which seemed better than just small arrows.
class InputManager { constructor() { this.keys = {}; const keyMap = new Map(); const setKey = (keyName, pressed) => { const keyState = this.keys[keyName]; keyState.justPressed = pressed && !keyState.down; keyState.down = pressed; }; const addKey = (keyCode, name) => { this.keys[name] = { down: false, justPressed: false }; keyMap.set(keyCode, name); }; const setKeyFromKeyCode = (keyCode, pressed) => { const keyName = keyMap.get(keyCode); if (!keyName) { return; } setKey(keyName, pressed); }; addKey(37, 'left'); addKey(39, 'right'); addKey(38, 'up'); addKey(40, 'down'); addKey(90, 'a'); addKey(88, 'b'); window.addEventListener('keydown', (e) => { setKeyFromKeyCode(e.keyCode, true); }); window.addEventListener('keyup', (e) => { setKeyFromKeyCode(e.keyCode, false); }); + const sides = [ + { elem: document.querySelector('#left'), key: 'left' }, + { elem: document.querySelector('#right'), key: 'right' }, + ]; + + const clearKeys = () => { + for (const {key} of sides) { + setKey(key, false); + } + }; + + const handleMouseMove = (e) => { + e.preventDefault(); + // this is needed because we call preventDefault(); + // we also gave the canvas a tabindex so it can + // become the focus + canvas.focus(); + window.addEventListener('pointermove', handleMouseMove); + window.addEventListener('pointerup', handleMouseUp); + + for (const {elem, key} of sides) { + let pressed = false; + const rect = elem.getBoundingClientRect(); + const x = e.clientX; + const y = e.clientY; + const inRect = x >= rect.left && x < rect.right && + y >= rect.top && y < rect.bottom; + if (inRect) { + pressed = true; + } + setKey(key, pressed); + } + }; + + function handleMouseUp() { + clearKeys(); + window.removeEventListener('pointermove', handleMouseMove, {passive: false}); + window.removeEventListener('pointerup', handleMouseUp); + } + + const uiElem = document.querySelector('#ui'); + uiElem.addEventListener('pointerdown', handleMouseMove, {passive: false}); + + uiElem.addEventListener('touchstart', (e) => { + // prevent scrolling + e.preventDefault(); + }, {passive: false}); } update() { for (const keyState of Object.values(this.keys)) { if (keyState.justPressed) { keyState.justPressed = false; } } } }
And now we should be able to control the character with the left and right cursor keys or with our fingers on a touchscreen
Ideally we'd do something else if the player went off the screen like move the camera or maybe offscreen = death but this article is already going to be too long so for now teleporting to the middle was the simplest thing.
Lets add some animals. We can start it off similar to the Player
by making
an Animal
component.
class Animal extends Component { constructor(gameObject, model) { super(gameObject); const skinInstance = gameObject.addComponent(SkinInstance, model); skinInstance.mixer.timeScale = globals.moveSpeed / 4; skinInstance.setAnimation('Idle'); } }
The code above sets the AnimationMixer.timeScale
to set the playback
speed of the animations relative to the move speed. This way if we
adjust the move speed the animation will speed up or slow down as well.
To start we could setup one of each type of animal
function init() { // hide the loading bar const loadingElem = document.querySelector('#loading'); loadingElem.style.display = 'none'; prepModelsAndAnimations(); { const gameObject = gameObjectManager.createGameObject(camera, 'camera'); globals.cameraInfo = gameObject.addComponent(CameraInfo); } { const gameObject = gameObjectManager.createGameObject(scene, 'player'); globals.player = gameObject.addComponent(Player); globals.congaLine = [gameObject]; } + const animalModelNames = [ + 'pig', + 'cow', + 'llama', + 'pug', + 'sheep', + 'zebra', + 'horse', + ]; + animalModelNames.forEach((name, ndx) => { + const gameObject = gameObjectManager.createGameObject(scene, name); + gameObject.addComponent(Animal, models[name]); + gameObject.transform.position.x = (ndx + 1) * 5; + }); }
And that would get us animals standing on the screen but we want them to do something.
Let's make them follow the player in a conga line but only if the player gets near enough. To do this we need several states.
Idle:
Animal is waiting for player to get close
Wait for End of Line:
Animal was tagged by player but now needs to wait for the animal at the end of the line to come by so they can join the end of the line.
Go to Last:
Animal needs to walk to where the animal they are following was, at the same time recording a history of where the animal they are following is currently.
Follow
Animal needs to keep recording a history of where the animal they are following is while moving to where the animal they are following was before.
There are many ways to handle different states like this. A common one is to use a Finite State Machine and to build some class to help us manage the state.
So, let's do that.
class FiniteStateMachine { constructor(states, initialState) { this.states = states; this.transition(initialState); } get state() { return this.currentState; } transition(state) { const oldState = this.states[this.currentState]; if (oldState && oldState.exit) { oldState.exit.call(this); } this.currentState = state; const newState = this.states[state]; if (newState.enter) { newState.enter.call(this); } } update() { const state = this.states[this.currentState]; if (state.update) { state.update.call(this); } } }
Here's a simple class.
Each state as 3 optional functions, enter
, update
, and exit
.
To switch states we call FiniteStateMachine.transition
and pass it
the name of the new state. If the current state has an exit
function
it's called. Then if the new state has an enter
function it's called.
Finally each frame FiniteStateMachine.update
calls the update
function
of the current state.
Let's use it to manage the states of the animals.
// Returns true of obj1 and obj2 are close function isClose(obj1, obj1Radius, obj2, obj2Radius) { const minDist = obj1Radius + obj2Radius; const dist = obj1.position.distanceTo(obj2.position); return dist < minDist; } // keeps v between -min and +min function minMagnitude(v, min) { return Math.abs(v) > min ? min * Math.sign(v) : v; } const aimTowardAndGetDistance = function() { const delta = new THREE.Vector3(); return function aimTowardAndGetDistance(source, targetPos, maxTurn) { delta.subVectors(targetPos, source.position); // compute the direction we want to be facing const targetRot = Math.atan2(delta.x, delta.z) + Math.PI * 1.5; // rotate in the shortest direction const deltaRot = (targetRot - source.rotation.y + Math.PI * 1.5) % (Math.PI * 2) - Math.PI; // make sure we don't turn faster than maxTurn const deltaRotation = minMagnitude(deltaRot, maxTurn); // keep rotation between 0 and Math.PI * 2 source.rotation.y = THREE.MathUtils.euclideanModulo( source.rotation.y + deltaRotation, Math.PI * 2); // return the distance to the target return delta.length(); }; }(); class Animal extends Component { constructor(gameObject, model) { super(gameObject); + const hitRadius = model.size / 2; const skinInstance = gameObject.addComponent(SkinInstance, model); skinInstance.mixer.timeScale = globals.moveSpeed / 4; + const transform = gameObject.transform; + const playerTransform = globals.player.gameObject.transform; + const maxTurnSpeed = Math.PI * (globals.moveSpeed / 4); + const targetHistory = []; + let targetNdx = 0; + + function addHistory() { + const targetGO = globals.congaLine[targetNdx]; + const newTargetPos = new THREE.Vector3(); + newTargetPos.copy(targetGO.transform.position); + targetHistory.push(newTargetPos); + } + + this.fsm = new FiniteStateMachine({ + idle: { + enter: () => { + skinInstance.setAnimation('Idle'); + }, + update: () => { + // check if player is near + if (isClose(transform, hitRadius, playerTransform, globals.playerRadius)) { + this.fsm.transition('waitForEnd'); + } + }, + }, + waitForEnd: { + enter: () => { + skinInstance.setAnimation('Jump'); + }, + update: () => { + // get the gameObject at the end of the conga line + const lastGO = globals.congaLine[globals.congaLine.length - 1]; + const deltaTurnSpeed = maxTurnSpeed * globals.deltaTime; + const targetPos = lastGO.transform.position; + aimTowardAndGetDistance(transform, targetPos, deltaTurnSpeed); + // check if last thing in conga line is near + if (isClose(transform, hitRadius, lastGO.transform, globals.playerRadius)) { + this.fsm.transition('goToLast'); + } + }, + }, + goToLast: { + enter: () => { + // remember who we're following + targetNdx = globals.congaLine.length - 1; + // add ourselves to the conga line + globals.congaLine.push(gameObject); + skinInstance.setAnimation('Walk'); + }, + update: () => { + addHistory(); + // walk to the oldest point in the history + const targetPos = targetHistory[0]; + const maxVelocity = globals.moveSpeed * globals.deltaTime; + const deltaTurnSpeed = maxTurnSpeed * globals.deltaTime; + const distance = aimTowardAndGetDistance(transform, targetPos, deltaTurnSpeed); + const velocity = distance; + transform.translateOnAxis(kForward, Math.min(velocity, maxVelocity)); + if (distance <= maxVelocity) { + this.fsm.transition('follow'); + } + }, + }, + follow: { + update: () => { + addHistory(); + // remove the oldest history and just put ourselves there. + const targetPos = targetHistory.shift(); + transform.position.copy(targetPos); + const deltaTurnSpeed = maxTurnSpeed * globals.deltaTime; + aimTowardAndGetDistance(transform, targetHistory[0], deltaTurnSpeed); + }, + }, + }, 'idle'); + } + update() { + this.fsm.update(); + } }
That was big chunk of code but it does what was described above. Hopefully of you walk through each state it will be clear.
A few things we need to add. We need the player to add itself
to the globals so the animals can find it and we need to start the
conga line with the player's GameObject
.
function init() { ... { const gameObject = gameObjectManager.createGameObject(scene, 'player'); + globals.player = gameObject.addComponent(Player); + globals.congaLine = [gameObject]; } }
We also need to compute a size for each model
function prepModelsAndAnimations() { + const box = new THREE.Box3(); + const size = new THREE.Vector3(); Object.values(models).forEach(model => { + box.setFromObject(model.gltf.scene); + box.getSize(size); + model.size = size.length(); const animsByName = {}; model.gltf.animations.forEach((clip) => { animsByName[clip.name] = clip; // Should really fix this in .blend file if (clip.name === 'Walk') { clip.duration /= 2; } }); model.animations = animsByName; }); }
And we need the player to record their size
class Player extends Component { constructor(gameObject) { super(gameObject); const model = models.knight; + globals.playerRadius = model.size / 2;
Thinking about it now it would probably have been smarter for the animals to just target the head of the conga line instead of the player specifically. Maybe I'll come back and change that later.
When I first started this I used just one radius for all animals
but of course that was no good as the pug is much smaller than the horse.
So I added the difference sizes but I wanted to be able to visualize
things. To do that I made a StatusDisplayHelper
component.
I uses a PolarGridHelper
to draw a circle around each character
and it uses html elements to let each character show some status using
the techniques covered in the article on aligning html elements to 3D.
First we need to add some HTML to host these elements
 <canvas id="c"></canvas>

...loading...

 +

And add some CSS for them
#labels { position: absolute; /* let us position ourself inside the container */ left: 0; /* make our position the top left of the container */ top: 0; color: white; width: 100%; height: 100%; overflow: hidden; pointer-events: none; } #labels>div { position: absolute; /* let us position them inside the container */ left: 0; /* make their default position the top left of the container */ top: 0; font-size: large; font-family: monospace; user-select: none; /* don't let the text get selected */ text-shadow: /* create a black outline */ -1px -1px 0 #000, 0 -1px 0 #000, 1px -1px 0 #000, 1px 0 0 #000, 1px 1px 0 #000, 0 1px 0 #000, -1px 1px 0 #000, -1px 0 0 #000; }
Then here's the component
const labelContainerElem = document.querySelector('#labels'); class StateDisplayHelper extends Component { constructor(gameObject, size) { super(gameObject); this.elem = document.createElement('div'); labelContainerElem.appendChild(this.elem); this.pos = new THREE.Vector3(); this.helper = new THREE.PolarGridHelper(size / 2, 1, 1, 16); gameObject.transform.add(this.helper); } setState(s) { this.elem.textContent = s; } setColor(cssColor) { this.elem.style.color = cssColor; this.helper.material.color.set(cssColor); } update() { const {pos} = this; const {transform} = this.gameObject; const {canvas} = globals; pos.copy(transform.position); // get the normalized screen coordinate of that position // x and y will be in the -1 to +1 range with x = -1 being // on the left and y = -1 being on the bottom pos.project(globals.camera); // convert the normalized position to CSS coordinates const x = (pos.x * .5 + .5) * canvas.clientWidth; const y = (pos.y * -.5 + .5) * canvas.clientHeight; // move the elem to that position this.elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`; } }
And we can then add them to the animals like this
class Animal extends Component { constructor(gameObject, model) { super(gameObject); + this.helper = gameObject.addComponent(StateDisplayHelper, model.size); ... } update() { this.fsm.update(); + const dir = THREE.MathUtils.radToDeg(this.gameObject.transform.rotation.y); + this.helper.setState(`${this.fsm.state}:${dir.toFixed(0)}`); } }
While we're at it lets make it so we can turn them on/off using lil-gui like we've used else where
import * as THREE from 'three'; import {OrbitControls} from 'three/addons/controls/OrbitControls.js'; import {GLTFLoader} from 'three/addons/loaders/GLTFLoader.js'; import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils.js'; +import {GUI} from 'three/addons/libs/lil-gui.module.min.js';
+const gui = new GUI(); +gui.add(globals, 'debug').onChange(showHideDebugInfo); +showHideDebugInfo(); const labelContainerElem = document.querySelector('#labels'); +function showHideDebugInfo() { + labelContainerElem.style.display = globals.debug ? '' : 'none'; +} +showHideDebugInfo(); class StateDisplayHelper extends Component { ... update() { + this.helper.visible = globals.debug; + if (!globals.debug) { + return; + } ... } }
And with that we get the kind of start of a game
Originally I set out to make a snake game where as you add animals to your line it gets harder because you need to avoid crashing into them. I'd also have put some obstacles in the scene and maybe a fence or some barrier around the perimeter.
Unfortunately the animals are long and thin. From above here's the zebra.
The code so far is using circle collisions which means if we had obstacles like a fence then this would be considered a collision
That's no good. Even animal to animal we'd have the same issue
I thought about writing a 2D rectangle to rectangle collision system but I quickly realized it could really be a lot of code. Checking that 2 arbitrarily oriented boxes overlap is not too much code and for our game with just a few objects it might work but looking into it after a few objects you quickly start needing to optimize the collision checking. First you might go through all objects that can possibly collide with each other and check their bounding spheres or bounding circles or their axially aligned bounding boxes. Once you know which objects might be colliding then you need to do more work to check if they are actually colliding. Often even checking the bounding spheres is too much work and you need some kind of better spacial structure for the objects so you can more quickly only check objects possibly near each other.
Then, once you write the code to check if 2 objects collide you generally want to make a collision system rather than manually asking "do I collide with these objects". A collision system emits events or calls callbacks in relation to things colliding. The advantage is it can check all the collisions at once so no objects get checked more than once where as if you manually call some "am I colliding" function often objects will be checked more than once wasting time.
Making that collision system would probably not be more than 100-300 lines of code for just checking arbitrarily oriented rectangles but it's still a ton more code so it seemed best to leave it out.
Another solution would have been to try to find other characters that are mostly circular from the top. Other humanoid characters for example instead of animals in which case the circle checking might work animal to animal. It would not work animal to fence, well we'd have to add circle to rectangle checking. I thought about making the fence a fence of bushes or poles, something circular but then I'd need probably 120 to 200 of them to surround the play area which would run into the optimization issues mentioned above.
These are reasons many games use an existing solution. Often these solutions are part of a physics library. The physical library needs to know if objects collide with each other so on top of providing physics they can also be used to detect collision.
If you're looking for a solution some of the three.js examples use ammo.js so that might be one.
One other solution might have been to place the obstacles on a grid and try to make it so each animal and the player just need to look at the grid. While that would be performant I felt that's best left as an exercise for the reader 😜
One more thing, many game systems have something called coroutines. Coroutines are routines that can pause while running and continue later.
Let's make the main character emit musical notes like they are leading the line by singing. There are many ways we could implement this but for now let's do it using coroutines.
First, here's a class to manage coroutines
function* waitSeconds(duration) { while (duration > 0) { duration -= globals.deltaTime; yield; } } class CoroutineRunner { constructor() { this.generatorStacks = []; this.addQueue = []; this.removeQueue = new Set(); } isBusy() { return this.addQueue.length + this.generatorStacks.length > 0; } add(generator, delay = 0) { const genStack = [generator]; if (delay) { genStack.push(waitSeconds(delay)); } this.addQueue.push(genStack); } remove(generator) { this.removeQueue.add(generator); } update() { this._addQueued(); this._removeQueued(); for (const genStack of this.generatorStacks) { const main = genStack[0]; // Handle if one coroutine removes another if (this.removeQueue.has(main)) { continue; } while (genStack.length) { const topGen = genStack[genStack.length - 1]; const {value, done} = topGen.next(); if (done) { if (genStack.length === 1) { this.removeQueue.add(topGen); break; } genStack.pop(); } else if (value) { genStack.push(value); } else { break; } } } this._removeQueued(); } _addQueued() { if (this.addQueue.length) { this.generatorStacks.splice(this.generatorStacks.length, 0, ...this.addQueue); this.addQueue = []; } } _removeQueued() { if (this.removeQueue.size) { this.generatorStacks = this.generatorStacks.filter(genStack => !this.removeQueue.has(genStack[0])); this.removeQueue.clear(); } } }
It does things similar to SafeArray
to make sure that it's safe to add or remove
coroutines while other coroutines are running. It also handles nested coroutines.
To make a coroutine you make a JavaScript generator function.
A generator function is preceded by the keyword function*
(the asterisk is important!)
Generator functions can yield
. For example
function* countOTo9() { for (let i = 0; i < 10; ++i) { console.log(i); yield; } }
If we added this function to the CoroutineRunner
above it would print
out each number, 0 to 9, once per frame or rather once per time we called runner.update
.
const runner = new CoroutineRunner(); runner.add(count0To9); while(runner.isBusy()) { runner.update(); }
Coroutines are removed automatically when they are finished. To remove a coroutine early, before it reaches the end you need to keep a reference to its generator like this
const gen = count0To9(); runner.add(gen); // sometime later runner.remove(gen);
In any case, in the player let's use a coroutine to emit a note every half second to 1 second
class Player extends Component { constructor(gameObject) { ... + this.runner = new CoroutineRunner(); + + function* emitNotes() { + for (;;) { + yield waitSeconds(rand(0.5, 1)); + const noteGO = gameObjectManager.createGameObject(scene, 'note'); + noteGO.transform.position.copy(gameObject.transform.position); + noteGO.transform.position.y += 5; + noteGO.addComponent(Note); + } + } + + this.runner.add(emitNotes()); } update() { + this.runner.update(); ... } } function rand(min, max) { if (max === undefined) { max = min; min = 0; } return Math.random() * (max - min) + min; }
You can see we make a CoroutineRunner
and we add an emitNotes
coroutine.
That function will run forever, waiting 0.5 to 1 seconds and then creating a game object
with a Note
component.
For the Note
component first lets make a texture with a note on it and
instead of loading a note image let's make one using a canvas like we covered in the article on canvas textures.
function makeTextTexture(str) { const ctx = document.createElement('canvas').getContext('2d'); ctx.canvas.width = 64; ctx.canvas.height = 64; ctx.font = '60px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = '#FFF'; ctx.fillText(str, ctx.canvas.width / 2, ctx.canvas.height / 2); return new THREE.CanvasTexture(ctx.canvas); } const noteTexture = makeTextTexture('♪');
The texture we create above is white each means when we use it we can set the material's color and get a note of any color.
Now that we have a noteTexture here's the Note
component.
It uses SpriteMaterial
and a Sprite
like we covered in
the article on billboards
class Note extends Component { constructor(gameObject) { super(gameObject); const {transform} = gameObject; const noteMaterial = new THREE.SpriteMaterial({ color: new THREE.Color().setHSL(rand(1), 1, 0.5), map: noteTexture, side: THREE.DoubleSide, transparent: true, }); const note = new THREE.Sprite(noteMaterial); note.scale.setScalar(3); transform.add(note); this.runner = new CoroutineRunner(); const direction = new THREE.Vector3(rand(-0.2, 0.2), 1, rand(-0.2, 0.2)); function* moveAndRemove() { for (let i = 0; i < 60; ++i) { transform.translateOnAxis(direction, globals.deltaTime * 10); noteMaterial.opacity = 1 - (i / 60); yield; } transform.parent.remove(transform); gameObjectManager.removeGameObject(gameObject); } this.runner.add(moveAndRemove()); } update() { this.runner.update(); } }
All it does is setup a Sprite
, then pick a random velocity and move
the transform at that velocity for 60 frames while fading out the note
by setting the material's opacity
.
After the loop it the removes the transform
from the scene and the note itself from active gameobjects.
One last thing, let's add a few more animals
function init() { ... const animalModelNames = [ 'pig', 'cow', 'llama', 'pug', 'sheep', 'zebra', 'horse', ]; + const base = new THREE.Object3D(); + const offset = new THREE.Object3D(); + base.add(offset); + + // position animals in a spiral. + const numAnimals = 28; + const arc = 10; + const b = 10 / (2 * Math.PI); + let r = 10; + let phi = r / b; + for (let i = 0; i < numAnimals; ++i) { + const name = animalModelNames[rand(animalModelNames.length) | 0]; const gameObject = gameObjectManager.createGameObject(scene, name); gameObject.addComponent(Animal, models[name]); + base.rotation.y = phi; + offset.position.x = r; + offset.updateWorldMatrix(true, false); + offset.getWorldPosition(gameObject.transform.position); + phi += arc / r; + r = b * phi; }
You might be asking, why not use setTimeout
? The problem with setTimeout
is it's not related to the game clock. For example above we made the maximum
amount of time allowed to elapse between frames to be 1/20th of a second.
Our coroutine system will respect that limit but setTimeout
would not.
Of course we could have made a simple timer ourselves
class Player ... { update() { this.noteTimer -= globals.deltaTime; if (this.noteTimer <= 0) { // reset timer this.noteTimer = rand(0.5, 1); // create a gameobject with a note component } }
And for this particular case that might have been better but as you add more and things you'll get more and more variables added to your classes where as with coroutines you can often just fire and forget.
Given our animal's simple states we could also have implemented them with a coroutine in the form of
// pseudo code! function* animalCoroutine() { setAnimation('Idle'); while(playerIsTooFar()) { yield; } const target = endOfLine; setAnimation('Jump'); while(targetIsTooFar()) { aimAt(target); yield; } setAnimation('Walk') while(notAtOldestPositionOfTarget()) { addHistory(); aimAt(target); yield; } for(;;) { addHistory(); const pos = history.unshift(); transform.position.copy(pos); aimAt(history[0]); yield; } }
This would have worked but of course as soon as our states were not so linear
we'd have had to switch to a FiniteStateMachine
.
It also wasn't clear to me if coroutines should run independently of their
components. We could have made a global CoroutineRunner
and put all
coroutines on it. That would make cleaning them up harder. As it is now
if the gameobject is removed all of its components are removed and
therefore the coroutine runners created are no longer called and it will
all get garbage collected. If we had global runner then it would be
the responsibility of each component to remove any coroutines it added
or else some other mechanism of registering coroutines with a particular
component or gameobject would be needed so that removing one removes the
others.
There are lots more issues a normal game engine would deal with. As it is there is no order to how gameobjects or their components are run. They are just run in the order added. Many game systems add a priority so the order can be set or changed.
Another issue we ran into is the Note
removing its gameobject's transform from the scene.
That seems like something that should happen in GameObject
since it was GameObject
that added the transform in the first place. Maybe GameObject
should have
a dispose
method that is called by GameObjectManager.removeGameObject
?
Yet another is how we're manually calling gameObjectManager.update
and inputManager.update
.
Maybe there should be a SystemManager
which these global services can add themselves
and each service will have its update
function called. In this way if we added a new
service like CollisionManager
we could just add it to the system manager and not
have to edit the render loop.
I'll leave those kinds of issues up to you. I hope this article has given you some ideas for your own game engine.
Maybe I should promote a game jam. If you click the jsfiddle or codepen buttons above the last example they'll open in those sites ready to edit.  Use the knight's rolling animation as a bowling ball and make an animal bowling game. Make an animal relay race. If you make a cool game post a link in the comments below.


...
Port Angeles, WA
he/him
<EMAIL_ADDRESS>key:
general
website
software
updated: sept 10, 2022
If I knew, I would speak.
I know nothing. I guess much.
-Ged (the wizard, not
my cat)


Dare’s new DEEL for Nigeria’s youth - The Nation Newspaper
ON Tuesday, November 12, Nigeria joined other African countries to celebrate the Africa Youth Month. The focus was on the African Union theme of “1 million by 2021, Count Me In.” But there was more in the kitty for the Youth as you will find out in the takeaways from the Ministers speech as he outlined the new tactical approach to youth engagement.
How is Nigeria going to keep its millions and millions of Youth productively engaged?
That was one of the questions on the minds of the attendees of the 2019 Africa Youth Day celebration as they filed up to register and gained entrance into the hall. Unlike most events organised by government bodies, this event had a different feel to it. For example, the banners had cartoons on them. Second, the air was light and the music was contemporary. Even though the event started behind schedule, once it did, it ran like clock work. Everyone participated. Sporting icons were there. Notable CSOs spoke their minds without fear. People got registered to get jobs on the spot. Everyone had a blast.
So, here are the 10 key takeaways from the speech read by the Minister of Youth and Sports Development, Mr Sunday Dare:
- Africa is wisely rethinking its strategy on the Youth: “In the past, many African governments in their quest for sustainable development, have invested substantially in infrastructure and economic reforms while paying little attention to the development of their human capital. Decades of these investments still leaves us in the search for more effective ways to attain our development goals. For me, I believe the way to go is to tilt the scale.”
- Ignoring the Youth will be too expensive for the Black Continent: “The cost implication to increase investment in youth is minimal when compared with the enormous resources most countries in Africa expend to mitigate the impact of poor investments in youth on peace, security and economies of our nations. Therefore, many experts propound that the youth bulge if not adequately harnessed, will become a demographic bomb rather than revenue, because a large mass of frustrated youth is likely to become a potential source of social and political instability.”
- In Nigeria, the Youth are in charge: “For us in Nigeria, is a double celebration as we also use the occasion to launch the Pocket Size National Youth Policy (NYP) as well as the National Youth Policy APP. The choice to have variations of the National Youth Policy in both formats is informed by our critical audience – youth, to whom the policy belong.”
- As far as the President is concerned, Mr. Dare only has one job: “When Mr. President appointed me as Minister, he gave me a clear mandate: ‘Keep Nigerian youth productively busy!’ And by God, this we will do. Together!”
- However, the job is for all: “Our strategy is simple: – each person contributing from their little space: government, private sector, civil society and development partners; The most important actors being the youth themselves; not mere standby spectators.”
- Cooperation is key: “Going forward, we at the Ministry will work towards sustaining an ecosystem of State and non-state actors in each thematic area of the National Youth Policy, which will evolve into the National Youth Policy Implementation Network.”
- Let’s DEEL!: “Mr. President’s has a new DEEL for the youth; and it starts today! Whether your interest is Digital Skills, Entrepreneurship, Employment or you are passionate about leadership, governance and inclusion, there is something for everyone in the new DEEL.”
- D – Digital Skill Acquisition
- DY.NG to train 200,000 youth on digital skills across the country using Youth Development Centres
E – Entrepreneurship
- Empowerment Programme
- CBN Enterprise Centres
E – Employability
- WEP – Work Experience Placement
L – Leadership Training
- National Youth Council of Nigeria
- Nigerian Youth Parliament
- Citizens and Leadership Training Centre
- NOYA is the next big thing: NOYA is the Nigerian Online Youth Assembly, an initiative of the ministry. It is an aggregator website to be launched early December and will connect the youth to Opportunities, Employment, Scholarships, Internships, Networking with peers and Leadership Trainings amongst others.
- Being an underdog has its perks: “Developing countries remain a massive part of the untapped market for the digital economy. They have population in hundreds of millions of youth offering a wide pool of talents that can be trained to utilize digital platforms, develop digital resources and drive technological advancement. What is more, they have the opportunity to learn from the experiences of other developed countries.”
- Olorunpomi is a digital media strategist and youth leader.


Writing
How Real-Time Collaborative Editors work? [Operational Transformation]
Recently, I’ve been diving deep on how real-time collaborative editing works. I have been reading various papers on both the methods of implementing collaborative editing : Operational Transformation, which goes back to 1989, when it was first implemented by the GROVE (GRoup Outtie Viewing Editor) system (this algorithm is quite old, and Google uses this algorithm for collaborative editing for Google Docs, Google Slides, Wave, etc) and Conflict-Free Replicated Data Types, which is a much newer approach to real-time editing.
Real challenge of collaborative editing
If you know how real-time collaborative editing works, then you may know that handling concurrent editing in multi user environment gracefully is very challenging. However, a few simple concepts can simplify this problem. The main challenge, as mentioned, with collaborative editing is the concurrency control [concurrent edits] to the document are not commutative. This needs to be causally ordered before applying either by undoing history, or by transforming the operations [operational transformation] before applying them to make them seem commutative.
Bringing Latency into action
Introducing latency between the client and server is where the problems arise. Latency in a collaborative editor introduces the possibility of version conflicts. Here, where, I said Operational Transformation will come into action.
Let’s take an example :
Starting Client’s state :
ABCD
Starting Server’s state :
ABCD
Now, let’s say, Client enters X
in between C
and D
, the operation would look something like this :
insert(X,3) //where 3 is the position where x is going to be added (0=A, 1=B, 2=C ..)
And at the same time, Server deletes b
, the operation would be :
delete(B,1)
What actually should happen is that the client and server should both end with ACXD
but in reality, client ends with ACXD
Starting Document State -> ABCD
"Insert 'X'" operation at offset 3 [local] -> ABCXD
"Delete 'B'" operation at offset 1 [remote] -> ACXD
but the server ends with ACDX
.
Starting Document State -> ABCD
"Delete 'B'" operation at offset 1 [local] -> ACD
"Insert 'X'" operation at offset 3 [remote] -> ACDX
Ofcourse, ACXD != ACDX
and the document which is shared now is in wrong state.
Here is where the Operational Transformation algorithm comes to the rescue.
Operational Transformation Algorithm
Operational Transformation (OT) is an algorithm/technique for the transformation of operations such that they can be applied to documents whose states have diverged, bringing them both back to the same state.
How does Operational Transformation work?
A short overview of how OT works :
-
Every change (insertion or deletion) is represented as an operation. An operation can be applied to the current document which results into a new document state.
-
To handle concurrent operations, we use the tranform function that takes two operations that have been applied to the same document state (but on different clients) and computes a new operation that can be applied after the second operation and that preserves the first operation’s intended change.
Let’s apply Operational Transformation in the original example.
If we apply OT, Client will see :
Starting Document State -> ABCD
"Insert 'X'" operation at offset 3 [local] -> ABCXD
"Delete 'B'" operation at offset 1 [transformed] -> ACXD
and Server will see :
Starting Document State -> ABCD
"Delete 'B'" operation at offset 1 [local] -> ACD
"Insert 'X'" operation at offset 2 [transformed] -> ACXD //Transform function would add add it in the new (3 - 1 = 2) position
Client - Server [OT] Approach to Collaborative Editing
The transform function is used to build a client-server protocol that can handle collaboration between any number of clients.
Choosing a Client-Server architecture will allow scouting a large number of clients without actually complicating the environment. Also, there will be a single system which holds the source of truth i.e. the server, so even if the clients crash/go offline for a long time, we can go back to the server and fetch the document easily.
This source of truth also forces the client to wait for the server to acknowledge the operation that the client has just sent which would mean that the client always stays on the server’s OT path. This would help in keeping a single history of operations without actually having to keep a mirror of the state for each client that is connected. That would eventually mean the number of clients that are connected to the server would have only one single copy of the document on the server.
Conclusion
Operational Transformation is a very powerful tool that allows to build great collaborative apps with support for non-blocking concurrent editing. I am looking forward to dive deep into the algorithm (and ofcourse extending this blogpost) and possibly working on an editor which supports it as well.
Resources
I’ve read the following papers and articles to learn about Operational Transformation.
-
High Latency, Low-Bandwidth Windowing in the Jupiter Collaboration System Authored by David A. Nichols, Pavel Curtis, Michael Dixon, and John Lamping.
-
Operational Transformation in Real-Time Group Editors: Issues, Algorithms, and Achievements Authored by Chengzheng Sun and Clarence (Skip) Ellis.
-
Concurrency Control in Groupware Systems Authored by C.A. Ellis, S.J. Gibbs
-
Evaluating CRDTs for Real-time Document Editing Authored by Mehdi Ahmed-Nacer, Claudia-Lavinia Ignat, G´erald Oster, Hyun-Gul Roh, Pascal Urso
-
Merging OT and CRDT Algorithms Authored by Mehdi Ahmed-Nacer, Pascal Urso, Valter Balegas, Nuno Pregui¸ca
-
Wikipedia on Operational Transformation One of the most informative articles, I have found on wikipedia, suprisingly.


A helper class for proxy objects to remote APIs.
Bases: object
A helper class for rpc clients.
This class is a wrapper around the RPC client API. It allows you to specify the topic and API version in a single place. This is intended to be used as a base class for a class that implements the client side of an rpc API.
rpc.call() a remote method.
| Parameters: |
|
|---|---|
| Returns: | The return value from the remote method. |
rpc.cast() a remote method.
| Parameters: |
|
|---|---|
| Returns: | None. rpc.cast() does not wait on any return value from the remote method. |
rpc.cast_to_server() a remote method.
| Parameters: |
|
|---|---|
| Returns: | None. rpc.cast_to_server() does not wait on any return values. |
rpc.fanout_cast() a remote method.
| Parameters: |
|
|---|---|
| Returns: | None. rpc.fanout_cast() does not wait on any return value from the remote method. |
rpc.fanout_cast_to_server() a remote method.
| Parameters: |
|
|---|---|
| Returns: | None. rpc.fanout_cast_to_server() does not wait on any return values. |
rpc.multicall() a remote method.
| Parameters: |
|
|---|---|
| Returns: | An iterator that lets you process each of the returned values from the remote method as they arrive. |


From this point on in the lab, you will be editing the table definition file '/var/www/tableDefinitions/ATSLab/atsBaseView.xml' which you saved earlier in page 6 of the '1. Introduction' step of this lab. The name 'table definition' no longer appropriately describes what these files are capable of. They are much more powerful than simply providing a framework to display a table or view. Within the table definition file, you can define countless actions which can be performed on either an entire dataset, or on a row-by-row basis.
Throughout the rest of the lab, the table definition will be transformed from it's raw state to what is available in the menu today. This will be done by:
Remember you can view your work anytime by reloading or navigating to the menu item we created earlier in the lab.
If you do not wish to edit the file by hand, you can copy the XML text displayed to the left at almost any time, and overwrite the file. There are some pages where there are lines marked to be deleted; and as such, you will need to manually remove them if you are coping the text. On these pages there will be a note of this at the top of the information in this panel.
On to the edits...