Welcome, Guest. Please login or register.
Did you miss your activation email?

Login with username, password and session length

 
Advanced search

1395213 Posts in 67244 Topics- by 60309 Members - Latest Member: raghav chauhan

September 17, 2021, 06:20:53 AM

Need hosting? Check out Digital Ocean
(more details in this thread)
TIGSource ForumsCommunityDevLogsDuality
Pages: [1] 2 3 ... 5
Print
Author Topic: Duality  (Read 15602 times)
Adam_
Level 1
*



View Profile WWW
« on: August 27, 2014, 02:31:18 PM »

Hey guys,

after silently enjoying a lot of marvelous devlogs here, I figured it's time to start my own. I cannot hope to rise to the same heights of artistic awesomeness as others already have, because.. you know.. programmer art. But maybe I can aid a little on the technical side Smiley So here it is: My very own game engine. It's called Duality.


Clicking the image will take you to this info page (Edit: Duality is also on itch.io now!), which will give you a quick intro on what this is all about. It won't take you longer than two minutes to read and there are all the relevant facts, links and screenshots, highly compressed for maximum efficiency. The page is for everyone who just wants to have a quick glance. For everyone else, here's the story behind it:
« Last Edit: July 24, 2016, 06:04:34 AM by Adam_ » Logged

Adam_
Level 1
*



View Profile WWW
« Reply #1 on: August 27, 2014, 02:31:33 PM »

Once upon a time

It all started somewhere in 2007 when I started working on a game engine named "ZweiDe". That totally isn't a german word, but when you read it with a german pronunciation, it sounds exactly like the actual german "2D". As you may have guessed, it was focused on 2D games, and it was fun.


I used it for a handful of simulations, game prototypes and techdemos. I even built a complete UI framework! It was called "Fenster", which totally is a german word, and it means as much as "window".


It was a lot of work, like, really a lot of work without having a solid roadmap or an actual userbase. By the time I finished it in 2009, I decided it was time to finally do something with it - an actual game. I ended up working a year on a space-themed action game with a strong focus on atmosphere. It was called "Nullpunkt", which is kind of a german word, and it can refer to the origin of a scale, zero degree Kelvin or a depressingly bad disposition. On the contrary, it definitely had some great stuff: Dynamic lighting, a tutorial, scripted sequences, a nice main menu, a mission and mod editor, heck, it even had voice acting!


But it was only 30 minutes long. In total. That sounds insane, I know - and I must admit that I screwed up on game production, because of this absolutely unreasonable contrast. In case you still want to take a look, here is a trailer and here goes the download1. However, the takeaway is - the project was kind of a train wreck. I don't want to let this slide too much into a post-mortem, but I think part of the problem was that I focused too much on developing the games modding capabilities and level editor. That's right: There was a level editor, and it took easily 50% of that years development time. Only to be thrown away completely after finishing the project, because it was far too hacky specialized to be reusable for any other project.

1The game has a bad habit of setting your screen resolution to a fixed value. Feel free to adjust it in Options.ini before starting.


Maybe a part of me just snapped when I realized this, but I never wanted to do this again. My next game would either be built using some kind of game maker, or wouldn't have a highly specialized toolset, because I would most definitely not spent so much time on something I would throw away afterwards. But being the programmer I am, focusing entirely on gameplay programming or using a game maker wouldn't have made me happy. I figured, I couldn't stay away from some kind of engine programming anyway.. so I might as well do it right.
« Last Edit: August 28, 2014, 04:07:53 AM by Adam_ » Logged

Adam_
Level 1
*



View Profile WWW
« Reply #2 on: August 27, 2014, 02:31:56 PM »

One engine to rule them all

There were a lot of lessons learned from developing that last game of mine. But mostly, I was glad to have finished the project and once again being free to code whatever comes to my mind - without having a bad feeling about not going forward on the real stuff. So I did some prototyping, tried out a few things and just coded carelessly.


Ah, finally! Finally free, and no longer attached to that train wreck of a project. Now that I think about it, that level editor I build for it was a hacky mess. I'm glad to leave that behind.


And ZweiDe is getting a little old, too. So many compatibility issues, and those outdated OpenGL bindings.. phew. Maybe, I'll get rid of that too.


You know, code just gets old after a while. Part of it becomes stale and bitter, and the rest grows organically. Like cancer. Man, what was I thinking. How could I have even used both of them for so long?


Now that I have the time to do whatever the heck I want, maybe I can finally explore some more interesting projects? I've always wanted to get more into procedural generation. That stuff is really interesting. You know, there are people creating art procedurally. Art. That's fascinating! I've always wondered whether it would be possible to create a story procedurally. Or even a whole game.


Yeah, it will almost definitely crash and burn in its first iteration, but where would be the fun without that? Let's do this! Well, I mean.. right after this.


While I'm at it.. wait.. is this..?


No!


Damnit. It's a game engine.
Logged

Adam_
Level 1
*



View Profile WWW
« Reply #3 on: August 27, 2014, 02:32:08 PM »

Meanwhile...

And there goes time. I started working on Duality in 2010, at the end of Nullpunkt. At the time, I didn't even know it was going to be a project like this. I just wanted to experiment with ways in making a game engine as modular and reusable as possible - and see if I could build a generic editor that can serve as a basis for any kind of 2D game. Duality is more or less the product of my pipe dream goal - to do all the things right that went wrong during Nullpunkt.

I'm not sure whether or not I have reached this goal, but I can say for sure that Duality has grown to be a great framework. I've built some little games and prototypes with it. I didn't yet get around to do something bigger - but this time, I'm not the only one. If you want to see in action what is possible using Duality, just head over to this devlog - it's Digital Furnace Games working on their 2D side-scrolling beat'em'up game "Onikira". They are not the only ones using it, but certainly the ones with the most awesome project so far. Grin

So.. I guess that's it for now. Feedback and questions are always appreciated! Until next time.. you'll hear from me as soon as there is news Smiley
« Last Edit: August 27, 2014, 03:16:24 PM by Adam_ » Logged

KevinAristotle
TIGBaby
*


View Profile
« Reply #4 on: August 28, 2014, 07:17:04 AM »

Your work is inspirational! For a while I've been wanting to make a 2D engine using MonoXna, but I've focused more on game development.
Does Duality support multiple platforms? And if so, are you implementing Mono C# in order to do so?
Logged
Adam_
Level 1
*



View Profile WWW
« Reply #5 on: August 29, 2014, 12:45:44 AM »

Your work is inspirational! For a while I've been wanting to make a 2D engine using MonoXna, but I've focused more on game development.
Does Duality support multiple platforms? And if so, are you implementing Mono C# in order to do so?

Duality has been developed with multi-platform support in mind, but right now, there is no official support for any other platform than standard Windows / .Net. It is not at all out of reach though:

  • Duality is based on OpenTK, which has been designed for multi-platform support.
  • The engine core has been written entirely in managed code, including all external dependencies.

My guess would be that it shouldn't be too hard to get it running on Linux or Mac using Mono, but I literally have zero experience with those platforms, so.. this is really just theory. Multiplying all time estimates by Pi is probably a good idea and I'm pretty sure that it won't "just work".
Logged

Adam_
Level 1
*



View Profile WWW
« Reply #6 on: September 01, 2014, 09:33:21 AM »

I've recently been working on completely rewriting the existing cloning system of Duality. It's still a work-in-progress and locked up in a distinct GitHub branch so my changes don't break anything, but since I'm almost done with the actual system, I figured I might as well write a blog post about it now.

As a short introduction: Duality uses a reflection-driven cloning system for applying Prefabs to objects as well as UndoRedo and object duplication support in the editor. Prefabs are one of Dualitys main mechanisms for instantiating objects from a "blueprint". They also support changing the Prefab after objects have been instantiated and updating those object according to the introduced changes. Without automated cloning, a large part of Duality would look quite different.



How to Clone an Object

Making an exact copy of an object isn’t an everyday use case for most programming APIs, but there are certain tasks where the ability to do so is vital. It’s not an easy task per se, and it requires a lot of thought when executed on a larger scale. When designing a modular framework like Duality, where every user can easily add custom classes into the realm of conveniently automated behavior, things get even worse. Fingers crossed, I might finally have found a solution.

Different Concepts

Before we begin, let’s talk about cloning in general. It is a complex topic that deserves some thought. Let’s say you encounter the following code snippet:

Code:
private void DuplicateObject(Foo source)
{
    // Create an exact copy of the source object
    Foo target = Bar.CreateClone(source);
    
    // Do some stuff to it
    // ...
}

Although these few lines seem fairly straightforward, the actual result could vary greatly, depending on the implementation of the clone method in question. Cloning as such isn’t strictly bound to one definition and what exactly it means often depends on use case requirements. In my case, I was searching for a cloning algorithm that satisfies the following definition:

Quote
An clone is a new object that, upon creation, equals the original in data and behavior, without being bound to the context in which it was created or the object it was initialized from.

As it turns out, this goal is surprisingly hard to reach. There are several common methods for implementing cloning algorithms, but unfortunately, most of them break fairly quick even if they looked promising at first.

Method 1: Shallow Copy

The target object can be created by simply creating a new instance of the appropriate type and assigning all of its fields the same values as the source object. This is what’s called a flat, or shallow copy. It is what you get when calling MemberwiseClone, and it is very rarely what you really want. Just imagine the object internally holding a List of strings: After a shallow copy, both source and target refer to the same list and modifying either original or the cloned object now has the potential to affect both.


Method 2: Deep Copy

All target fields could be assigned a value that is equal to the one from the source object, but not the same. This is what’s called a deep copy. It can be achieved using a reflection-driven algorithm, or by exploiting serialization to write an object to memory and reading it again. If the source object is internally holding a List of strings, its clone holds a new List of strings with equal contents, which is a lot better than a shallow copy – but just as likely to break your stuff: If there is an internal reference to a singleton class, there are now two instances of that singleton, and the cloned object has its own, private one.


Method 3: Manual Copy

Examining these options, none of them seems like a suitable candidate for a system that should be able to clone any kind of object. In most cases, it seems the easiest way to clone an object is to let it implement its own custom method. After all, each object knows best how it wants to be handled, right? Let’s call this approach the manual copy. It can just about do anything and you simply have to trust the documentation about its internal mechanisms and public results, but it has by far the highest success rate of all the above methods.


For the longest time, Duality has relied on the manual copy approach, combined with a shallow copy fallback for user-defined classes that handled all ICollection types in a hardcoded deep copy special case. The fallback worked reasonably well and in case something was a little off, the user could always implement an explicit cloning method using the manual copy approach to save the day. The biggest drawback was the impact of maintaining all those copy methods, where each new field had to be copied using some hand-written lines of code. I wasn’t very happy with it, but I accepted the fact that it was the only cloning method that worked reliably.

That was until the day it broke.

"This Should Never Have Worked"

As it turns out, even when doing a manual copy for every single object and completely relying on user-written code, cloning can still break under certain circumstances, and the only practicable way around it is an automated cloning system. In order to explain why, let’s see what happened first:


The image above shows the (simplified) object graph that first exposed problems with the previous system. When cloning the Scene at the root of that graph, its manual copy method will be invoked, and being the owner of its GameObjects, the Scene obviously needs to clone them as well, instead of just assigning their references. Being a good object-oriented fellow, it doesn’t attempt to do so itself, but calls each GameObjects cloning method. The GameObject acts in the same way with its Components, so their manual cloning method will be called as well. So far, so good.

Our problem arises when the Component encounters a reference to a different Component: Since it doesn’t own that other Component, it will choose to simply reference it instead – we don’t want to end up with additional Components after all! However, this reference now points to an original Component, not the cloned one, which is obviously not what we want!


This cannot be fixed from within a custom Clone method: The one provided by Component doesn’t have the required overview to handle problems like this, and the Scenes Clone method doesn’t have the required knowledge about the internals of each Component. To fix this, there needs to be a cloning system keeping track of the operation as a whole. A manual copy approach alone can’t solve this.

That said, the presence of a cloning system isn’t something new in the realms of the Duality framework, but despite certain mechanics to solve problems like the one above, it still failed in the example case. It maintained a mapping between objects from the source graph and objects from the target graph, so each objects Clone method could request the target equivalent of an arbitrary source reference. This is cool so far, and it works in a lot of cases. However, as soon as there is a circular reference like the one between the two Components, one of those is going to be copied first – and at this point, the cloning system simply hasn’t seen the other one yet, or their target equivalent. This problem was only the tip of the iceberg, so I finally decided to completely rewrite the cloning system in Duality, and replace it with a better one.

A Better Cloning System

Over the course of the last months, I’ve spent a lot of thought on how an »ideal« cloning system could look like and came to the conclusion that the question of ownership is central to its implementation: If object A has a field that references object B, does A own B, or does it simply refer to B? The answer to that question makes the difference between deep-cloning a child object and simply referring to it – and in order to find the correct answer, cloning needs to be a context dependent two-step process:

  • Explore the source graph and allocate the target graph.
  • Copy data from source to target using the previously gathered ownership and mapping information.

In the first step, no data will be copied at all. Its purpose is to determine which objects are in a direct or indirect ownership relation to the cloning operations source object, and to provide default-initialized target instances for all of those. Exploring the source graph can be done by using reflection to recursively follow all references of the root object.

By default, when an object references another one, ownership is assumed. Keep in mind that ownership is only relevant to us with regard to the cloning operations root object: If root object R owns object A, which owns object B, then ownership of both A and B will be assumed. Objects that are not assumed to be owned by the root object will not be investigated further, since they are potentially mighty goblin gods and definitely none of the cloning systems business.

Note that without any additional knowledge specified by the programmer, all object references are ownership references and thus, the whole cloning process equal a deep copy operation as described above. The trick is that there is a special CloneBehavior attribute that can be applied to classes and fields in order to change, how certain references are handled. When applied to a class, the attribute is treated like a global setting, and when applied to a field, the attribute is treated like a local override. These local overrides can also target specific Types in order to correctly handle objects stored within collections.

Code:
// I'm owned by default
public class Sandwich : IEdibleDevice {}

// I'm referenced by default
[CloneBehavior(CloneBehavior.Reference)]
public class MightyGoblinGod {}

// Owns sandwiches and refers to a goblin god
public class Goblin
{
private List<Sandwich> myFood;
private MightyGoblinGod allHailTo;

// Well, it doesn't own that specific sandwich. Damnit, Frank.
[CloneBehavior(CloneBehavior.Reference)]
private Sandwich franksSandwich;
}

// A Pantheon owns all its gods, although they are usually referenced.
public class Pantheon
{
[CloneBehavior(typeof(MightyGoblinGod), CloneBehavior.ChildObject)]
private List<MightyGoblinGod> allTheGods;
}

Additionally, there is the concept of weak references: Imagine a tree structure of Nodes, where each Node can contain N child Nodes and 1 parent Node:

Code:
[CloneBehavior(CloneBehavior.Reference)]
public class Node
{
    // Reference to the parent Node
    private Node parent;
 
    // Ownership of all child Nodes
    [CloneBehavior(typeof(Node), CloneBehavior.ChildObject)]
    private List<Node> children;
}

When cloning a certain Node from a Node tree, we expect all of its child Nodes to be cloned as well, but not its parent Node. However, we don’t even want to reference that parent Node, since this would introduce an inconsistency to the hierarchy, where our cloned Node sees itself as child of a (source) object that doesn’t even know it. At the same time, we can’t simply ignore the parent field in general, because the cloned child Nodes should definitely refer back to their cloned parent Node. This is where weak references can help:

Code:
[CloneBehavior(CloneBehavior.Reference)]
public class Node
{
    // Weakly reference the parent Node
    [CloneBehavior(typeof(Node), CloneBehavior.WeakReference)]
    private Node parent;
 
    // Ownership of all child Nodes
    [CloneBehavior(typeof(Node), CloneBehavior.ChildObject)]
    private List<Node> children;
}

A weak reference will behave exactly the same as references, as long as at least one ownership relation to the object in question is detected. If the object isn’t owned by any object in the source graph, weak references will simply be skipped during cloning and affected fields retain their default value. In this case, it means that the parent field of a cloned Node will be null, if the parent isn’t part of the cloned object hierarchy itself.


In addition to these attributes, it is also possible to ignore certain fields completely using a special flag, and to provide a manual implementation of cloning behavior by implementing a certain interface. However, due to the available field and class attributes, custom implementations are rarely necessary.



You can also find this posting on my blog. I hope there were some interesting bits to you Smiley As usual, feel free to ask questions and let me know what you think.
Logged

hammeron-art
Level 0
**



View Profile WWW
« Reply #7 on: September 03, 2014, 12:39:37 PM »

I took a look at it and really liked the framework structure.
A live tool based in 3D navegation is something that lacks in 2D editors.
It's so much easy to build complex parallax and non-grid based levels that way.

I would suggest you to allow rotation and translation in other axis besides Z or a 3D camera view.
For a more technical artist like me the little but fancy effects reachable with that helps a lot in my workflow and in the final product quality.

I also started a project like that in my free time but seems more worthwhile to contribute in this open source.
Great work!  Gentleman
Logged

Adam_
Level 1
*



View Profile WWW
« Reply #8 on: September 04, 2014, 05:36:54 AM »

I took a look at it and really liked the framework structure.
A live tool based in 3D navegation is something that lacks in 2D editors.
It's so much easy to build complex parallax and non-grid based levels that way.

I would suggest you to allow rotation and translation in other axis besides Z or a 3D camera view.
For a more technical artist like me the little but fancy effects reachable with that helps a lot in my workflow and in the final product quality.

I also started a project like that in my free time but seems more worthwhile to contribute in this open source.
Great work!  Gentleman

Thanks! It's also a nice idea to allow rotating the editor camera in arbitrary axes in order to get a better overview. There is already a non-parallax rendering mode, so maybe adding a full-3d diagnostic mode for the editor only wouldn't be too hard to do. Due to the ongoing Cloning changes, I won't get around to it anytime soon, but I will definitely keep that in mind for later experimentation Smiley
« Last Edit: September 04, 2014, 05:43:12 AM by Adam_ » Logged

Adam_
Level 1
*



View Profile WWW
« Reply #9 on: September 09, 2014, 03:42:07 AM »

As it turns out, there are a few more pitfalls when cloning an object mostly automated. Most of them stem from the fact that Duality also requires a second use case: Copying data from a source object to an already existing target object. The catch is, since the target object existed before the operation, there may be references to any part of its object graph all over the place - and these references need to stay intact. So, when copying from a to b, there is the additional requirement not to create any new objects in b, as long as there is an already existing equivalent in place.

Fortunately, I managed to satisfy this requirement as well by introducing a series of API changes and minor improvements to the cloning system, but there was one single issue that I thought was kind of interesting: As described above, the question of ownership is central to the cloning algorithm, and there is a significant amount of work involved in determining it. However, there are actually two kind of ownership: Data ownership and ownership on a conceptual level - and apparently, these two differ in some rare cases.

I have described one of them in a followup blog posting1, specifically C# multicast Delegates, but the topic actually applies to other languages as well when looking at an arbitrary implementation of the Observer Pattern a.k.a. event handlers.

1Since the last one was rather lengthy and turned out to make this thread much less readable, I'll just link to it this time, instead of reposting it.
Logged

Adam_
Level 1
*



View Profile WWW
« Reply #10 on: September 15, 2014, 07:47:42 AM »

After more than two weeks of frantically coding in the realms of reflection, optimization and dynamically emitted IL code, I'm finally getting somewhere with the cloning system's rewrite issue. If all further testing goes well, I might merge my changes back to the master branch and see what happens. Soon after that I'll finally be free to enter new grounds once again. Probably a fresh load of documentation / example projects, with a grain of long term multi-platform research. We'll see Smiley
Logged

Adam_
Level 1
*



View Profile WWW
« Reply #11 on: September 16, 2014, 01:12:13 AM »

I have merged the cloning branch into the master branch. Everyone who isn't in the middle of a big production that shouldn't break short-term, feel free to pull the latest changes from GitHub.


Notable changes:

  • The new system can properly handle delegates and events, including their subscriptions.
  • It can handle any kind of inter-object reference, including Components referencing Components from different objects.
  • You can now modify cloning behavior per-object and per-field using [CloneBehavior(...)] and [CloneField(...)] flags without needing to implement custom cloning.
  • Implementing custom cloning procedures for an object no longer deactivates Duality's reflection-driven fallback, which is ideal if you just need to perform some post-clone actions. If you want to take over handling of a single field, flag that field using the [ManuallyCloned] attribute, if you want to take over everything that is declared within your class, flag the class itself with [ManuallyCloned].
  • Implementing custom cloning procedures now requires two distinct methods: One for discovering ownership by the current object, and one for copying data.
  • There are convenient "HandleXY" methods provided within custom cloning methods to automate a specific part of it.
  • Although the new system is slightly slower as a whole (~1.2x) it can still yield notable performance boosts in a lot of cases, compared to the old one, especially for objects that are relying on 100% automated cloning. Manual cloning is still faster, but the difference is a lot smaller.
  • When copying data from A to B, the new system preserves already existing objects in the target graph, if there is an equivalent in the source graph. References to child objects from B will still be valid after a CopyTo operation in most cases. This was previously only true for Components.

Breaking changes:

  • Manual cloning now uses a different interface.
  • The globally available extension methods for cloning have been renamed.
  • The CloneProvider class has changed dramatically. If you were interacting with it directly, you will need to rewrite that.
  • [NonSerialized] fields are excluded from cloning by default because they're usually irrelevant or temporary. To change this, add [CloneField(CloneFieldFlags.DontSkip)] to the field.

I have written a lot of unit tests and did a lot of manual testing, but there may still be some bugs in the new system I haven't seen yet. Please report anything suspicious, so I can have a look at it. If no severe problems are reported, I will publish a new set of Duality packages soon.
Logged

Adam_
Level 1
*



View Profile WWW
« Reply #12 on: September 20, 2014, 03:06:36 PM »

After finishing 3-4 week long struggles with the cloning issue and wrapping it up in the recent merge, I've begun putting together an extended example project for Duality. I'm aiming to build a simple, playable game that can serve as reference for everyone who has reached beyond the first few "Getting Started" tutorials.

I'm not entirely sure what it will become yet, but maybe something like a twin stick space shooter. In any case, the full source code and (minimalistic) content will be available from the projects GitHub repository and will be published as a sample package later on.

Anyhow, the first steps have been done:


Added shooting.


Player colors, Tweaks, multiple Players.


Let's see where this goes.
Logged

Adam_
Level 1
*



View Profile WWW
« Reply #13 on: September 22, 2014, 03:48:54 AM »

Added usable input management.



There are three input methods supported: Two gamepads and a mouse / keyboard combo. Each player is dynamically assigned the first detected input method that isn't yet assigned to another player. No need for an options menu, just pick up the controller you like and go.
« Last Edit: September 22, 2014, 03:56:30 AM by Adam_ » Logged

AegarPyke
Level 0
**



View Profile WWW
« Reply #14 on: September 22, 2014, 05:29:02 AM »

An example project like that will surely encourage a lot more people to give the engine a try. It's a great idea and the regular devlog updates are interesting. Keep it up!
Logged

The Geartower: Indie game design, development, and deconstruction
InfiniteStateMachine
Level 10
*****



View Profile WWW
« Reply #15 on: September 22, 2014, 07:50:40 AM »

Hey I remember this from the .NetRocks podcast. Great work and great to see this project is still going strong Smiley
Logged

Adam_
Level 1
*



View Profile WWW
« Reply #16 on: September 23, 2014, 02:36:30 AM »

Camera now adapts scale to keep viewport contents ~ the same in any screen resolution.



This also means all game objects should be the same size regardless of monitor DPI or fullscreen resolution, provided the games real-world size on screen is comparable. It always scales X and Y by the same ratio, so no distortion will be introduced.



Hey I remember this from the .NetRocks podcast. Great work and great to see this project is still going strong Smiley

Thanks! Smiley It's been a while since it was on .NetRocks but it still provides some visibility. I'm just really grateful the guys over at BatCat Games dragged it with them into the spotlight.



Here's a link to that podcast for everyone who doesn't know what we're talking about :]

An example project like that will surely encourage a lot more people to give the engine a try. It's a great idea and the regular devlog updates are interesting. Keep it up!

I certainly hope so! Duality could definitely use some more projects going on and I really hope I can lower the initial threshold to overcome for new users to give it a go. If there is anything specific you'd like to see in this example project, just let me know!


Logged

Adam_
Level 1
*



View Profile WWW
« Reply #17 on: September 23, 2014, 06:50:49 AM »

Experimenting with minimalistic art style.

Logged

Adam_
Level 1
*



View Profile WWW
« Reply #18 on: September 25, 2014, 06:58:46 AM »

Working on a first enemy type.



Doesn't do much, but has a neat blinky eye(dle) animation.

Logged

Adam_
Level 1
*



View Profile WWW
« Reply #19 on: September 28, 2014, 04:29:15 AM »

Worked a little more on the first enemy. It will be an explosive that sets off when something touches one of its spikes.



They don't extend spikes that are blocked right away, unless the path has cleared. Prevents blowing themselfs up for no reason.



Same goes for being near to each other. They're a careful bunch.



If you are really careful, you can even push them around without setting them off. Just don't touch the spikes.

Logged

Pages: [1] 2 3 ... 5
Print
Jump to:  

Theme orange-lt created by panic