Monday 29 September 2014

Review the Ancients: Treasure Island Dizzy - CodeMasters 1988

Treasure Island Dizzy

Written in 1988
Coded by The Oliver Twins
Graphics: Neil Adamson
Music: David Whittaker
Published by CodeMasters

http://www.worldofspectrum.org/infoseekid.cgi?id=0009333
Play it online http://torinak.com/qaop#!dizzy2

The Egg Who Returned

Back in 1988 we saw the return of The Egg who wears boxing gloves. Dizzy.
His initial game came out the year before but Treasure Island Dizzy turned everything up to 11.
It had a proper masked sprite system as opposed to the XOR rendering of the original and it had a 128k soundtrack by the ever wonderful David Whittaker. He included a 48k soundtrack for those with older spectrums but the 128k version was the best by far. It also gave us in-game music. Music who's pleasant jaunty tones concealed any malevolence within.



Treasure Island Dizzy was much in the same vein as it's predecessor, a flip screen adventure game with inventory puzzles and platforming. Very simple game-play. Move, Jump and collect/use.

There were changes though.

Original Dizzy only allowed the player to carry a single item, which must be dropped should the player want another item whereas Treasure Island Dizzy allowed for three items.. a design decision which would alter the gameplay and difficulty in ways they probably didn't foresee (..or did they?).

Another decision they took was to remove the lives system used in the original whereas the player had 3 lives to start with but could collect more as the player progressed. This was removed and was replaced with Permadeath.

They also removed the random flying enemies (see God Damned Bats trope ) from the original and replaced them with enemies which followed a specific and predictable path - if they moved at all. This made things a little easier. a little.

Treasure Island Dizzy is very hard.

I mean seriously really very hard. Don't underestimate this game.
Kids today are not prepared for how soul crushingly hard this game is. Seriously!

It's not hard in a dark souls way where you have to memorize attack patterns and weaknesses to succeed - no, Treasure Island Dizzy is harder than that. You see, in Dark Souls, should you die (and you will.. a lot) you don't have to start from the beginning again.

With Treasure Island Dizzy, should you make any mistake - like fail to avoid one of the few traps, get hit by a creature, burned by fire, fall in water without a snorkel, or drop your snorkel while underwater while trying to pick up another item - Then it's back to the start of the game.

No matter how far you have progressed.

And it wasn't a small game either. Here is the map for your viewing pleasure:


As you can see, there are items to collect, hidden coins which you need and the same puzzles to solve over and over and over again.

It sounds terrible right? So why does this game invoke such fond memories with it's players?

Well, for me it was initially the cuteness of the thing. The happy egg waving along with the jaunty tune, jumping and rolling. Dizzy was innocent. He was happy. Every time Dizzy died and I had to restart the game from scratch, it was my fault. Each and every time. There were no random actions, no traps that couldn't be avoided. With each death was a failure - My Failure.

I don't like to fail so I persevered. I learned where items were. I made sure that my snorkel was always the most recent item in my inventory so that when I picked up an item under the water, I wouldn't drown.

I timed the jumps, the rolls and even memorized the most optimal route through the story to get things done.

After a lot of work, trial and error and a home made map, I finally got to the end screen.
I had collected all 30 of the coins (some were hidden in evil places)  and I completed the game.

I DID IT!

To be greeted by a message.

CONGRATULATIONS! You have successfully solved all the puzzles and truely earned your freedom. Good luck Dizzy.

I nearly had a psychotic breakdown.. after all of the work and retries the game was over with a simple message - then it was right back to the title screen again.

"Welcome to Treasure Island Dizzy!"

I sat in shocked silence watching the title screen for several seconds before I calmly put the tape back in its box and reset the computer.

I never tried to complete it again. That was it.. I was done.

But did I have fun? Yes. Absolutely. The feeling of solving a seemingly difficult problem and making progress was amazing. Making the jump to land on the top of the mine, jumping the crab and drowning the Sinclair Abuser magazine.

Collecting some items and solving puzzles opened up new and interesting areas which really helped to sell the mystery of the island. Because there were no enemies trying to kill you generally, you were free to explore - just be careful.


In modern terms, 

I do wonder whether it would be more fun if the permadeath was removed. Would the game still be fun if instead of a "Restart from the beginning" or "several lives" approach that a restart from a checkpoint system would be better. It would certainly be more forgiving.

However restart-checkpoints didn't really come into existence until Sonic the Hedgehog and even Sonic had lives.

This was the 80's/early 90's we had what we had and we had Dizzy and that was alright.

Finally

I would recommend that anyone interested in game design and gameplay gives this little old gem a shot. It's got a lot of heart and is worth the time. Whether you want to devote enough time to complete the game will depend on several factors, one of which being your sanity.

If anyone complains about how hard a modern game is, if you've played Treasure Island Dizzy, you can scoff at their ineptitude. Pah! You don't know hard until you've played Treasure Island Dizzy!


I've placed a link at the top of the review so you can play it on-line. CodeMasters have not yet given permissions to reproduce any of their games online which is a shame because they'll be lost in time in a few years unless they're allowed to hosted by fans.


For my mind, The Oliver Twins made the games which made CodeMasters a household name.
I owned a great number of their games and enjoyed them all. They still make games as Blitz Games Studios - They have a simply staggering collection of titles to their name. To call them prolific would be an understatement.

Gentlemen, I salute you!




Sunday 28 September 2014

Box2D Raycasting with Category Masking with MOAI

What is Ray Casting?

Ray Casting is a technique used to determine whether something intersects a line drawn between one point and another. Normally Box2D uses circles, rectangles and polygons to determine collisions and resolve the forces and positions appropriately. However sometimes you just want to check for collisions along an imaginary line.

Imagine a Laser Pointer

One example for this is if you imagine a laser pointer.
Its beam is emitted from the end of the laser pen and it continues until it hits the wall at the far end of the room and leaves a pleasant red dot.

Should something break the beam, the red dot shows on the object breaking the beam and no longer on the wall.

Before MOAI 1.5

For a long time, MOAI did not support the ray casting features of Box 2D. Version 1.5 supports the closest raycast implementation where the closest object on the path of the ray is returned.

The Closest raycast feature is very much like the laser pen example. A ray is cast and the first thing which intersects the ray is returned.

A lot of the time, this is all you need.

It will find the closest Box2D fixture intersecting the ray no matter what it is.

Category Masking?

I needed something a little extra. Box2D supports categories. This is so you can designate an object as a particular type of thing so it can be included or excluded in specific collisions.

For my game I have several categories for objects.

  • FLOOR = 1
  • PLAYER = 2
  • OBJECT = 4
  • MONSTER =8
  • SWITCH = 16
  • POWERUP = 32

My laser beams should only be stopped by Players, Monsters and the Floor. Objects, Powerups and Switches should not get in the way - let alone be blown up by lasers.

I need a mask to filter only the categories I'm interested in.
FLOOR + PLAYER + MONSTER = 11

By performing a bitwise AND against the category for the object intersecting the ray and the mask, we can check to see if we're interested in it.

Here's a little binary refresher so you can see why the mask works.

Floor   = 00000001
Mask    = 00001011
Yup

Player  = 00000010
Mask    = 00001011
Yarp

Monster = 00001000
Mask    = 00001011
Yessir

Switch  = 00010000
Mask    = 00001011
Nope

However... 

Box2D doesn't support category masking for RayCasts, even though it supports categories and masks for all other types of collision detection.

This meant that even with the implementation in MOAI release 5, I couldn't use the Box2D ray casting. In fact, it meant I couldn't use Box2D* at all without making some changes.

*I'm using Box2D v 2.2.1 within MOAI - the most recent appears to be 2.3.0 (as of time of writing) however it too does not support masking.

So I modified Box2D to suit.

UDiff for Box2D 2.2.1


 3rdparty/box2d-2.2.1/Box2D/Collision/b2Collision.h     |  1 +
 3rdparty/box2d-2.2.1/Box2D/Collision/b2DynamicTree.h   |  1 +
 3rdparty/box2d-2.2.1/Box2D/Dynamics/b2World.cpp        | 18 ++++++++++++++++--
 3rdparty/box2d-2.2.1/Box2D/Dynamics/b2World.h          |  3 +++
 3rdparty/box2d-2.2.1/Box2D/Dynamics/b2WorldCallbacks.h |  2 +-
 5 files changed, 22 insertions(+), 3 deletions(-)

diff --git a/3rdparty/box2d-2.2.1/Box2D/Collision/b2Collision.h b/3rdparty/box2d-2.2.1/Box2D/Collision/b2Collision.h
index 8bb316c..62c7ee8 100644
--- a/3rdparty/box2d-2.2.1/Box2D/Collision/b2Collision.h
+++ b/3rdparty/box2d-2.2.1/Box2D/Collision/b2Collision.h
@@ -147,6 +147,7 @@ struct b2RayCastInput
 {
  b2Vec2 p1, p2;
  float32 maxFraction;
+ uint16 maskBits;
 };

 /// Ray-cast output data. The ray hits at p1 + fraction * (p2 - p1), where p1 and p2
diff --git a/3rdparty/box2d-2.2.1/Box2D/Collision/b2DynamicTree.h b/3rdparty/box2d-2.2.1/Box2D/Collision/b2DynamicTree.h
index 0787785..ed4afce 100644
--- a/3rdparty/box2d-2.2.1/Box2D/Collision/b2DynamicTree.h
+++ b/3rdparty/box2d-2.2.1/Box2D/Collision/b2DynamicTree.h
@@ -254,6 +254,7 @@ inline void b2DynamicTree::RayCast(T* callback, const b2RayCastInput& input) con
  b2RayCastInput subInput;
  subInput.p1 = input.p1;
  subInput.p2 = input.p2;
+ subInput.maskBits = input.maskBits;
  subInput.maxFraction = maxFraction;

  float32 value = callback->RayCastCallback(subInput, nodeId);
diff --git a/3rdparty/box2d-2.2.1/Box2D/Dynamics/b2World.cpp b/3rdparty/box2d-2.2.1/Box2D/Dynamics/b2World.cpp
index d77e80a..b3497e1 100644
--- a/3rdparty/box2d-2.2.1/Box2D/Dynamics/b2World.cpp
+++ b/3rdparty/box2d-2.2.1/Box2D/Dynamics/b2World.cpp
@@ -1013,7 +1013,7 @@ struct b2WorldRayCastWrapper
  {
  float32 fraction = output.fraction;
  b2Vec2 point = (1.0f - fraction) * input.p1 + fraction * input.p2;
- return callback->ReportFixture(fixture, point, output.normal, fraction);
+ return callback->ReportFixture(fixture, point, output.normal, fraction, input.maskBits);
  }

  return input.maxFraction;
@@ -1023,7 +1023,7 @@ struct b2WorldRayCastWrapper
  b2RayCastCallback* callback;
 };

-void b2World::RayCast(b2RayCastCallback* callback, const b2Vec2& point1, const b2Vec2& point2) const
+void b2World::RayCast(b2RayCastCallback* callback, const b2Vec2& point1, const b2Vec2& point2 ) const
 {
  b2WorldRayCastWrapper wrapper;
  wrapper.broadPhase = &m_contactManager.m_broadPhase;
@@ -1032,6 +1032,20 @@ void b2World::RayCast(b2RayCastCallback* callback, const b2Vec2& point1, const b
  input.maxFraction = 1.0f;
  input.p1 = point1;
  input.p2 = point2;
+ input.maskBits = 0;
+ m_contactManager.m_broadPhase.RayCast(&wrapper, input);
+}
+
+void b2World::RayCast2(b2RayCastCallback* callback, const b2Vec2& point1, const b2Vec2& point2, const uint16 maskBits ) const
+{
+ b2WorldRayCastWrapper wrapper;
+ wrapper.broadPhase = &m_contactManager.m_broadPhase;
+ wrapper.callback = callback;
+ b2RayCastInput input;
+ input.maxFraction = 1.0f;
+ input.p1 = point1;
+ input.p2 = point2;
+ input.maskBits = maskBits;
  m_contactManager.m_broadPhase.RayCast(&wrapper, input);
 }

diff --git a/3rdparty/box2d-2.2.1/Box2D/Dynamics/b2World.h b/3rdparty/box2d-2.2.1/Box2D/Dynamics/b2World.h
index 965a366..d910e23 100644
--- a/3rdparty/box2d-2.2.1/Box2D/Dynamics/b2World.h
+++ b/3rdparty/box2d-2.2.1/Box2D/Dynamics/b2World.h
@@ -121,6 +121,9 @@ public:
  /// @param point2 the ray ending point
  void RayCast(b2RayCastCallback* callback, const b2Vec2& point1, const b2Vec2& point2) const;

+ void RayCast2(b2RayCastCallback* callback, const b2Vec2& point1, const b2Vec2& point2, const uint16 maskBits ) const;
+
+
  /// Get the world body list. With the returned body, use b2Body::GetNext to get
  /// the next body in the world list. A NULL body indicates the end of the list.
  /// @return the head of the world body list.
diff --git a/3rdparty/box2d-2.2.1/Box2D/Dynamics/b2WorldCallbacks.h b/3rdparty/box2d-2.2.1/Box2D/Dynamics/b2WorldCallbacks.h
index 82ffc02..a485d8c 100644
--- a/3rdparty/box2d-2.2.1/Box2D/Dynamics/b2WorldCallbacks.h
+++ b/3rdparty/box2d-2.2.1/Box2D/Dynamics/b2WorldCallbacks.h
@@ -149,7 +149,7 @@ public:
  /// @return -1 to filter, 0 to terminate, fraction to clip the ray for
  /// closest hit, 1 to continue
  virtual float32 ReportFixture( b2Fixture* fixture, const b2Vec2& point,
- const b2Vec2& normal, float32 fraction) = 0;
+ const b2Vec2& normal, float32 fraction, uint16 maskBits) = 0;
 };

 #endif


As you can see, the changes merely extend Box2D's raycast feature to include a request for a specific set of maskBits.  Box2D doesn't perform any filtering internally with this change.

Category checking against the mask will be done within MOAI's implementation coming up next.

Note also that I create a function called RayCast2 which can be called with the maskBits parameter. This is just in case the Original RayCast function is used internally by Box2D and to ensure that other programs using it won't be broken by this change.


Initially it felt like overkill to modify Box2D to just pass a number through and back again. However this is required because the raycast function uses a callback to notify the program that it found something - this callback doesn't have any context as to how it's being called really to know what type of fixture you're looking for.

So apart from hard coding it to ignore sensor type fixtures, you're stuck just returning the closest object of any category unless you pass the mask through to the callback function.

Basically, Box2D doesn't know to inform the callback function that you're really only interested in UFOs and it can ignore Clouds.

Here's the UDiff for the MOAI code 

(Note patch is for 1.4r2 not for MOAI v1.5 - although the change is simple to apply to 1.5 )

 src/moaicore/MOAIBox2DWorld.cpp                  | 104 +++++++++++++++++++++++
 src/moaicore/MOAIBox2DWorld.h                    |   1 +

 2 files changed, 105 insertions(+)

diff --git a/src/lua-headers/run.sh b/src/lua-headers/run.sh
old mode 100755
new mode 100644
diff --git a/src/moaicore/MOAIBox2DWorld.cpp b/src/moaicore/MOAIBox2DWorld.cpp
index 943cbf0..20b39fe 100644
--- a/src/moaicore/MOAIBox2DWorld.cpp
+++ b/src/moaicore/MOAIBox2DWorld.cpp
@@ -46,6 +46,8 @@ MOAIBox2DPrim::MOAIBox2DPrim () :
  mDestroyNext ( 0 ) {
 }


+
 //================================================================//
 // local
 //================================================================//
@@ -600,6 +602,107 @@ int MOAIBox2DWorld::_getAutoClearForces ( lua_State* L ) {
  return 1;
 }

+// local
+ //================================================================//

+// RayCast modification from: http://getmoai.com/forums/post2723.html?hilit=raycast#p2723
+// Additional box2d modification to return fixtureIndex as well.
+class RayCastCallback : public b2RayCastCallback
+{
+public:
+    RayCastCallback()
+    {
+        m_fixture = NULL;
+ m_point.SetZero();
+ m_normal.SetZero();
+    }
+
+    float32 ReportFixture(b2Fixture* fixture, const b2Vec2& point, const b2Vec2& normal, float32 fraction, uint16 maskBits)
+    {
+        
+ b2Filter filter = fixture->GetFilterData ();
+

+ //MOAILog ( state,MOAILogMessages::MOAIRayCast_MaskBits, maskBits );
+ if( (filter.categoryBits & maskBits) != filter.categoryBits )
+ {
+ return -1; 
+ }
+ /*if( ( MOAIBox2DFixture* )fixture->IsSensor()){
+ return -1; //filter
+ }*/
+
+ m_fixture = fixture;
+        m_point = point;
+        m_normal = normal;
+
+        return fraction;
+    }
+
+    b2Fixture* m_fixture;
+    b2Vec2 m_point;
+    b2Vec2 m_normal;
+};
+
+//----------------------------------------------------------------//
+/**     @name   getRayCast
+        @text   return RayCast 1st point hit
+
+        @in     MOAIBox2DWorld self
+        @in     number p1x
+        @in     number p1y
+        @in     number p2x
+        @in     number p2y
+        @out    bool true if hit, false otherwise
+        @out    number  hitpoint.x or nil
+        @out    number  hitpoint.y or nil
+ @out MOAIBox2DFixture fixture that was hit, or nil
+*/
+
+int MOAIBox2DWorld::_getRayCast ( lua_State* L ) {
+ MOAI_LUA_SETUP ( MOAIBox2DWorld, "U" )
+ float p1x=state.GetValue < float >( 2, 0 ) * self->mUnitsToMeters;
+ float p1y=state.GetValue < float >( 3, 0 ) * self->mUnitsToMeters;
+ float p2x=state.GetValue < float >( 4, 0 ) * self->mUnitsToMeters;
+ float p2y=state.GetValue < float >( 5, 0 ) * self->mUnitsToMeters;
+ uint16 maskBits = ( uint16 ) state.GetValue < u32 >( 6, 0 );
+
+ b2Vec2 p1(p1x,p1y);
+ b2Vec2 p2(p2x,p2y);
+
+ RayCastCallback callback;
+ self->mWorld->RayCast2(&callback, p1, p2, maskBits);
+
+ if (NULL != callback.m_fixture)
+ {
+ b2Vec2 hitpoint = callback.m_point;
+
+ lua_pushboolean( state, true );
+ lua_pushnumber ( state, hitpoint.x / self->mUnitsToMeters );
+ lua_pushnumber ( state, hitpoint.y / self->mUnitsToMeters );
+
+ // Raycast hit a fixture
+ MOAIBox2DFixture* moaiFixture = ( MOAIBox2DFixture* )callback.m_fixture->GetUserData ();
+ if ( moaiFixture )
+ {
+ moaiFixture->PushLuaUserdata ( state );
+
+ return 4;
+ }
+ else
+ {
+ return 3;
+ }
+ }
+ else
+ {
+ // Raycast did not hit a fixture
+ lua_pushboolean( state, false );
+
+ return 1;
+ }
+}
+
 //----------------------------------------------------------------//
 /** @name getGravity
  @text See Box2D documentation.
@@ -998,6 +1101,7 @@ void MOAIBox2DWorld::RegisterLuaFuncs ( MOAILuaState& state ) {
  { "setGravity", _setGravity },
  { "setIterations", _setIterations },
  { "setLinearSleepTolerance", _setLinearSleepTolerance },
+ { "getRayCast", _getRayCast },
  { "setTimeToSleep", _setTimeToSleep },
  { "setUnitsToMeters", _setUnitsToMeters },
  { NULL, NULL }
diff --git a/src/moaicore/MOAIBox2DWorld.h b/src/moaicore/MOAIBox2DWorld.h
index 61a6641..7d0880c 100644
--- a/src/moaicore/MOAIBox2DWorld.h
+++ b/src/moaicore/MOAIBox2DWorld.h
@@ -98,6 +98,7 @@ private:
  static int _getAutoClearForces ( lua_State* L );
  static int _getGravity ( lua_State* L );
  static int _getLinearSleepTolerance ( lua_State* L );
+ static int _getRayCast ( lua_State* L );
  static int _getTimeToSleep ( lua_State* L );
  static int _setAngularSleepTolerance ( lua_State* L );
  static int _setAutoClearForces ( lua_State* L );

So there you go. Raycasts with masking.
If I only want to shoot UFOs with my lasers, not the clouds in the sky, now I can.

So how do we use this in game?

Somewhere in your game code you'll have something which creates your MOAIBox2DWorld.
You'll be calling the getRayCast method on this object.

gameworld = MOAIBox2DWorld.new ()

Then somewhere you'll need a function to actually cast the ray and return the data back.
The function returns several parameters (typical for Lua).

  • Whether the ray hit something or not, 
  • the X co-ordinate of the hit,
  • the Y co-ordinate of the hit 
  • the object with a category matching the maskBits that the ray hit.


return gameworld:getRayCast( x1,y1,x2,y2, maskBits)



Sorry for the ugly formatting - I'm sure there's a nicer way to present a UDiff.

Have fun.


Special thanks to FiveVoltHigh.com for getting me started with their initial tutorial on patching MOAI with support for RayCasts.

http://www.fivevolthigh.com/2013/10/moai-exposing-box2d-world-ray-casting-to-moai/