Thanks for tuning in to part 2, and hopefully the final part in the series. So, once we accept that the other files are a given, the working example is quite functional for only ~500 lines. That's right, 500. Here is a video of the example:
Again, if you'd just like to take it for a spin and see what it's all about, just download the binary for Windows. Source has been uploaded to github.
Okay, so as is, the level design would be horrible, but it does get the point across. As you can see, there are several types of platforms that the player (well, the white square...) interacts with. If you took a good look at the header file for the collision system from part 1 of this series, you may have noticed the enumeration for platform types, if so, good for you - you knew what was coming ahead of time. That said, there are static platforms that the player interacts with that you just collide with - no big whoop. In fact, this worked out-of-the-box from Alex's original codebase. There are also some bonus platforms to get things a little more interesting, including jump-through platforms, which only detect collisions from the top, a moving platform (which is self-evident in the video), falling platforms, which fall when the player touches them, and vanishing platforms. I don't know about you, but I remember vanishing platforms the most from the Megaman series. It was so frustrating trying to memorize the patterns in which they would appear and disappear - now you get to pass that frustration along to others. There is also a conveyor belt, hard to tell immediately by watching the video, but easy to feel if you are controlling the square. Moving forward, the code for the working example is as follows:
#include <iostream>
#include <SFML/Graphics.hpp>
#include <CollisionSystem.hpp>
#include <dynamicBody.hpp>
#include <platformBody.hpp>
#include <tile.hpp>
#include <interpolate.hpp>
int main(void){
const sf::Vector2f tileSize = sf::Vector2f(32, 32);
const int NUM_PLATFORM_OBJECTS = 24;
float GRAVITY = 0.f;//variable gravity
sf::RenderWindow window(sf::VideoMode(800, 600), "Collision Detection", sf::Style::Default);
window.setKeyRepeatEnabled(false);
window.setVerticalSyncEnabled(true);
window.setFramerateLimit(60);
sf::Event e;
//set up the view
sf::View mainView(sf::FloatRect(0.f, 0.f, 1200.f, 800.f));
window.setView(mainView);
//instantiate the collision system object
phys::collisionSystem collisionSys;
//set up some platforms - all normal platforms by default
phys::platformBody bodies[NUM_PLATFORM_OBJECTS];
for (int i = 0; i < NUM_PLATFORM_OBJECTS;i++){
bodies[i].m_id = i;//assign each platform an id
bodies[i].m_width = tileSize.x*4;
bodies[i].m_height = tileSize.y;
bodies[i].m_type = phys::bodyType::platform;
}
//place them on the screen somewhere...
bodies[0].m_position = sf::Vector2f(0.f,window.getSize().y-32.f);
bodies[1].m_position = sf::Vector2f(0.f, 356.f);
bodies[2].m_position = sf::Vector2f(480.f, window.getSize().y - 192.f);
bodies[3].m_position = sf::Vector2f(0.f, 96.f);
bodies[4].m_position = sf::Vector2f(640.f, 224.f);
bodies[5].m_position = sf::Vector2f(480.f, window.getSize().y - 32.f);
bodies[6].m_position = sf::Vector2f(672.f, window.getSize().y - 260.f);
bodies[7].m_position = sf::Vector2f(96.f, window.getSize().y - 128.f);
bodies[8].m_position = sf::Vector2f(640, window.getSize().y - 160.f);
bodies[9].m_position = sf::Vector2f(640.f, 64.f);
bodies[10].m_position = sf::Vector2f(292, window.getSize().y - 128.f);
//conveyor belt
bodies[11].m_position = sf::Vector2f(window.getSize().x / 4.f, window.getSize().y / 2.f);
bodies[11].m_width = tileSize.x*10;
bodies[11].m_height = tileSize.y;
bodies[11].m_type = phys::bodyType::conveyorBelt;
bodies[11].m_surfaceVelocity = 12.f;
//moving platform
bodies[12].m_height = tileSize.y/8;
bodies[12].m_width = tileSize.x*5;
bodies[12].m_position = sf::Vector2f(window.getSize().x/4,window.getSize().y/4);
bodies[12].m_type = phys::bodyType::moving;
int platformDir = 1;
float platformVelocity = 0.f;
//jump through platforms
bodies[13].m_width = tileSize.x;
bodies[13].m_height = tileSize.y/8.f;
bodies[13].m_position = sf::Vector2f(160.f,window.getSize().y - 224.f);
bodies[13].m_type = phys::bodyType::jumpthrough;
bodies[14].m_width = tileSize.x;
bodies[14].m_height = tileSize.y/8.f;
bodies[14].m_position = sf::Vector2f(576, window.getSize().y - 288.f);
bodies[14].m_type = phys::bodyType::jumpthrough;
//falling platforms
bodies[15].m_width = tileSize.x;
bodies[15].m_height = tileSize.y;
bodies[15].m_position = sf::Vector2f(192, 64.f);
bodies[15].m_type = phys::bodyType::falling;
bodies[16].m_width = tileSize.x;
bodies[16].m_height = tileSize.y;
bodies[16].m_position = sf::Vector2f(256.f, 64.f);
bodies[16].m_type = phys::bodyType::falling;
bodies[17].m_width = tileSize.x;
bodies[17].m_height = tileSize.y;
bodies[17].m_position = sf::Vector2f(320.f, 64.f);
bodies[17].m_type = phys::bodyType::falling;
bodies[18].m_width = tileSize.x;
bodies[18].m_height = tileSize.y;
bodies[18].m_position = sf::Vector2f(384.f, 64.f);
bodies[18].m_type = phys::bodyType::falling;
bodies[19].m_width = tileSize.x;
bodies[19].m_height = tileSize.y;
bodies[19].m_position = sf::Vector2f(448.f, 64.f);
bodies[19].m_type = phys::bodyType::falling;
//vanishing platforms
bodies[20].m_width = tileSize.x;
bodies[20].m_height = tileSize.y;
bodies[20].m_position = sf::Vector2f(192.f, -32.f);
bodies[20].m_type = phys::bodyType::vanishing;
bodies[21].m_width = tileSize.x;
bodies[21].m_height = tileSize.y;
bodies[21].m_position = sf::Vector2f(256.f, -32.f);
bodies[21].m_type = phys::bodyType::vanishing;
bodies[22].m_width = tileSize.x;
bodies[22].m_height = tileSize.y;
bodies[22].m_position = sf::Vector2f(320.f, -32.f);
bodies[22].m_type = phys::bodyType::vanishing;
bodies[23].m_width = tileSize.x;
bodies[23].m_height = tileSize.y;
bodies[23].m_position = sf::Vector2f(384.f, -32.f);
bodies[23].m_type = phys::bodyType::vanishing;
tile tiles[NUM_PLATFORM_OBJECTS];
for (int i = 0; i < NUM_PLATFORM_OBJECTS; ++i){
tiles[i].setPosition(bodies[i].m_position);
tiles[i].setSize(sf::Vector2f(bodies[i].m_width, bodies[i].m_height));
switch(bodies[i].m_type){
case phys::bodyType::platform:
tiles[i].setFillColor(sf::Color(255, 0, 0, 255));
break;
case phys::bodyType::conveyorBelt:
tiles[i].setFillColor(sf::Color(255, 100, 0, 255));
break;
case phys::bodyType::moving:
tiles[i].setFillColor(sf::Color(0, 255, 0, 255));
break;
case phys::bodyType::jumpthrough:
tiles[i].setFillColor(sf::Color(0, 255, 255, 255));
break;
case phys::bodyType::falling:
tiles[i].setFillColor(sf::Color(255, 255, 0, 255));
break;
case phys::bodyType::vanishing:
tiles[i].setFillColor(sf::Color(255, 0, 255, 255));
break;
default:
break;
}
}
//set up for the player
phys::dynamicBody playerBody;
playerBody.m_position = sf::Vector2f(window.getSize().x/2.f, window.getSize().y/2.f);
playerBody.m_width = tileSize.x;
playerBody.m_height = tileSize.y;
playerBody.m_velocity = sf::Vector2f(0,0);
playerBody.m_lastPosition = sf::Vector2f(window.getSize().x/2.f, window.getSize().y/2.f);
playerBody.m_moveX = 0;
playerBody.m_moveY = 0;
sf::RectangleShape player;
player.setFillColor(sf::Color(255, 255, 255, 255));
player.setSize(sf::Vector2f(tileSize.x, tileSize.y));
player.setPosition(playerBody.m_position);
//time variables
sf::Clock tickClock;
sf::Time timeSinceLastUpdate = sf::Time::Zero;
sf::Time duration = sf::Time::Zero;
sf::Time TimePerFrame = sf::seconds(1.f / 60.f);
sf::Time jumpTime = sf::Time::Zero;
sf::Time vanishingTime = sf::Time::Zero;
int oddEven = 1;
float alpha = 0.f;
//get information about the joystick (available in SFML 2.2 - comment out if using older version)
sf::Joystick::Identification id = sf::Joystick::getIdentification(0);
std::cout << "\nVendor ID: " << id.vendorId << "\nProduct ID: " << id.productId << std::endl;
sf::String controller("Joystick Use: " + id.name);
window.setTitle(controller);//easily tells us what controller is connected
//query joystick for settings if it's plugged in...
if (sf::Joystick::isConnected(0)){
// check how many buttons joystick number 0 has
unsigned int buttonCount = sf::Joystick::getButtonCount(0);
// check if joystick number 0 has a Z axis
bool hasZ = sf::Joystick::hasAxis(0, sf::Joystick::Z);
std::cout << "Button count: " << buttonCount << std::endl;
std::cout << "Has a z-axis: " << hasZ << std::endl;
}
int turbo = 1;//indicate whether player is using button for turbo speed ;)
//for movement
sf::Vector2f speed = sf::Vector2f(0.f,0.f);
//collision info for insight on handling player movement
bool intersection = false;
unsigned int type = phys::bodyType::none;
bool collisionTop = false;
bool collisionBottom = false;
bool collisionLeft = false;
bool collisionRight = false;
bool canJump = false;
bool jumped = false;
unsigned int jumpCount = 0;
//for debug info
int debug = -1;
bool running = true;
while (running){
while (window.pollEvent(e)){
if (e.type == sf::Event::Closed){
window.close();
return 0;
}
if (e.type == sf::Event::KeyPressed){
switch (e.key.code){
case sf::Keyboard::Escape:
{
window.close();
return 0;
}
break;
case sf::Keyboard::D:
{
debug *= -1;
}
break;
default:
break;
}
}
}
sf::Time elapsedTime = tickClock.restart();
timeSinceLastUpdate += elapsedTime;
while (timeSinceLastUpdate > TimePerFrame)
{
timeSinceLastUpdate -= TimePerFrame;
//////////////////////////////////////
//get joystick input inside fixed
//time step - pollEvent() seems to be
//missing joystick events for some
//reason (sure it's my fault!)
//////////////////////////////////////
//check state of joystick
speed = sf::Vector2f(sf::Joystick::getAxisPosition(0, sf::Joystick::X), sf::Joystick::getAxisPosition(0, sf::Joystick::Y));
if (sf::Joystick::isButtonPressed(0, 2)){//"X" button on the XBox 360 controller
turbo = 2;
}
if (!sf::Joystick::isButtonPressed(0, 2)){
turbo = 1;
}
if(sf::Joystick::isButtonPressed(0,0)){//"A" button on the XBox 360 controller
jumpCount++;
jumped = true;
}
if(!sf::Joystick::isButtonPressed(0,0) && jumpCount > 1){
jumped = false;
jumpCount = 0;
}
if(sf::Joystick::isButtonPressed(0,1)){//"B" button on the XBox 360 controller
window.close();
return 0;
}
//accumulator for moving platform
if(duration.asSeconds() >= 4.f){
platformDir *= -1;
duration = sf::Time::Zero;
}
else
duration += TimePerFrame;
//moving platform updates
if(duration.asSeconds() <= 3.f)
platformVelocity = TimePerFrame.asSeconds()*(platformDir * 700.f * math::interpolate::quadraticEaseInOut(duration.asSeconds(), 0.f, 1.f, 3.f));
else
platformVelocity = 0.f;
//update the position of the moving platforms (bodies and geometry)
//this needs to occur before player updates
bodies[12].m_position.x += platformVelocity;
tiles[12].setPosition(bodies[12].m_position);
//find bodies that are falling and handle them
//while simultaneously setting the fall flag for sprite
for(int i=0; i<NUM_PLATFORM_OBJECTS; ++i){
//establish what a valid collision is for falling platform
bool collided = bodies[i].m_falling;
//conditions for falling platforms to fall
//based on a half second delay
if(collided && tiles[i].m_fallTime.asSeconds() > .5f){
tiles[i].m_fallTime = sf::Time::Zero;
}else{
tiles[i].m_fallTime += TimePerFrame;
}
if(collided && tiles[i].m_fallTime.asSeconds() > .5f){
tiles[i].m_falling = true;
}
}
//update the position of the falling platform sprite and move the bodies off-screen
for(int i=0; i<NUM_PLATFORM_OBJECTS; ++i){
if(tiles[i].m_falling && bodies[i].m_falling){
bodies[i].m_position = sf::Vector2f(-9999,0);
tiles[i].move(0.f, TimePerFrame.asSeconds()*1000.f);
}
}
//compute time and resets for vanishing platforms
if(vanishingTime.asSeconds() > 2.f){
vanishingTime = sf::Time::Zero;
oddEven *= -1;//occilate between odd and even platforms
//std::cout<<"oddEven: "<<oddEven<<std::endl;
}else{
vanishingTime += TimePerFrame;
}
//make platforms vanish and reappear
//this applies to platforms in the index [20,23]
if(oddEven == 1){
for(int i=20; i<24; ++i){
if(i%2 == 0){//even numbered platforms vanish...
if(vanishingTime.asSeconds() < 1.f){
alpha = math::interpolate::sineEaseIn(vanishingTime.asSeconds(),0.f,255.f, 1.f);
tiles[i].setFillColor(sf::Color(255,0,255,255-(unsigned int)alpha));
}
else{
tiles[i].setFillColor(sf::Color(255,0,255,0));
}
if(tiles[i].getFillColor().a <= 0)//friendlier to the player to wait until the platform completely disappears before moving the physics body off-screen
bodies[i].m_position = sf::Vector2f(-9999,0);
}else{//odd numbered platforms reappear...
if(vanishingTime.asSeconds() < 1.f){
alpha = math::interpolate::sineEaseIn(vanishingTime.asSeconds(),0.f,255.f, 1.f);
tiles[i].setFillColor(sf::Color(255,0,255,(unsigned int)alpha));
}
else{
tiles[i].setFillColor(sf::Color(255,0,255,255));
}
bodies[i].m_position = tiles[i].getPosition();
}
}
}else{
for(int i=20; i<24; ++i){
if(i%2 == 0){//even numbered platforms reappear...
if(vanishingTime.asSeconds() < 1.f){
alpha = math::interpolate::sineEaseIn(vanishingTime.asSeconds(),0.f,255.f, 1.f);
tiles[i].setFillColor(sf::Color(255,0,255,(unsigned int)alpha));
}
else{
tiles[i].setFillColor(sf::Color(255,0,255,255));
}
bodies[i].m_position = tiles[i].getPosition();
}else{//odd numbered platforms vanish...
if(vanishingTime.asSeconds() < 1.f){
alpha = math::interpolate::sineEaseIn(vanishingTime.asSeconds(),0.f,255.f, 1.f);
tiles[i].setFillColor(sf::Color(255,0,255,255-(unsigned int)alpha));
}
else{
tiles[i].setFillColor(sf::Color(255,0,255,0));
}
if(tiles[i].getFillColor().a <= 0)//friendlier to the player to wait until the platform completely disappears before moving the physics body off-screen
bodies[i].m_position = sf::Vector2f(-9999,0);
}
}
}
//check for springboard activity and handle the motion of the springboard
//do "can jump" routine
//when the jump count is greater than 1
//the timer resets
if(jumpCount > 1){
jumpTime = sf::Time::Zero;
}
else
jumpTime += TimePerFrame;
//if jump is pressed
//and the timer is less than .5 seconds
//then the player can jump
canJump = jumped && jumpTime.asSeconds() < 0.4f ? true : false;
//do updates
//get intersection information
intersection = playerBody.m_position.y + playerBody.m_height >= collisionSys.getBodyInfo().m_position.y - 2.f
&& playerBody.m_position.y + playerBody.m_height <= collisionSys.getBodyInfo().m_position.y + collisionSys.getBodyInfo().m_height
&& playerBody.m_position.x + playerBody.m_width >= collisionSys.getBodyInfo().m_position.x
&& playerBody.m_position.x + playerBody.m_width <= collisionSys.getBodyInfo().m_position.x + collisionSys.getBodyInfo().m_width + playerBody.m_width;
//get platform type information
type = collisionSys.getBodyInfo().m_type;
//debug info
if(intersection && debug == 1){
std::cout<<"Top Collision: "<<collisionSys.getCollisionInfo().m_collisionTop<<std::endl;
std::cout<<"Bottom Collision: "<<collisionSys.getCollisionInfo().m_collisionBottom<<std::endl;
std::cout<<"Left Collision: "<<collisionSys.getCollisionInfo().m_collisionLeft<<std::endl;
std::cout<<"Right Collision: "<<collisionSys.getCollisionInfo().m_collisionRight<<std::endl;
std::cout<<"type: "<<collisionSys.getBodyInfo().m_type<<std::endl;
std::cout<<"id: "<<collisionSys.getBodyInfo().m_id<<std::endl;
std::cout<<"Gravity: "<<GRAVITY<<std::endl;
}
//get collision information
collisionTop = collisionSys.getCollisionInfo().m_collisionTop;
collisionBottom = collisionSys.getCollisionInfo().m_collisionBottom;
collisionLeft = collisionSys.getCollisionInfo().m_collisionLeft;
collisionRight = collisionSys.getCollisionInfo().m_collisionRight;
//update the position of the square according to input from joystick
//CHECK DEAD ZONES - OTHERWISE INPUT WILL RESULT IN JITTERY MOVEMENTS WHEN NO INPUT IS PROVIDED
//INPUT RANGES FROM -100 TO 100
//A 15% DEAD ZONE SEEMS TO WORK FOR ME - GIVE THAT A SHOT
if ((speed.x > 15.f && !collisionLeft) || (speed.x < -15.f && !collisionRight)){
playerBody.m_position.x += turbo*speed.x*TimePerFrame.asSeconds();
}
else
speed.x = 0.f;
if(canJump){
if(jumpTime.asSeconds() < .2f && !collisionTop){
playerBody.m_position.y += -800.f*TimePerFrame.asSeconds() + GRAVITY;
}
else if(jumpTime.asSeconds() > .2f && !collisionTop){
playerBody.m_position.y += GRAVITY - 13.9;//black magic...makes jumps parabolic instead of triangular
}
}
if(collisionBottom){//reduces stickiness
playerBody.m_position.y = playerBody.m_lastPosition.y;
playerBody.m_position.x += turbo*speed.x*TimePerFrame.asSeconds();
}
//check for contact with static platforms
if(type == phys::bodyType::platform && collisionBottom){
GRAVITY = 0.f;
playerBody.m_position.x += turbo*speed.x*TimePerFrame.asSeconds();
}
//sticky ceilings problem solved
else if(collisionTop && (!collisionLeft || !collisionRight)){
playerBody.m_position.y = playerBody.m_lastPosition.y + .5f;
playerBody.m_position.x += turbo*speed.x*TimePerFrame.asSeconds();
}
//sticky walls problem solved
else if(!collisionBottom && (collisionLeft || collisionRight)){
playerBody.m_position.y += GRAVITY;
playerBody.m_position.x = playerBody.m_lastPosition.x;
}
//check for contact with the jump-through platforms
else if(type == phys::bodyType::jumpthrough && collisionBottom){
GRAVITY = 0.f;
playerBody.m_position.x += turbo*speed.x*TimePerFrame.asSeconds();
}
//check for contact with the conveyor belt
else if (type == phys::bodyType::conveyorBelt && collisionBottom){
GRAVITY = 0.f;
playerBody.m_position.x += turbo*speed.x*TimePerFrame.asSeconds() - bodies[11].m_surfaceVelocity;
}
//check for contact with the moving platform
else if(type == phys::bodyType::moving && collisionBottom){
GRAVITY = 0.f;
playerBody.m_position.x += turbo*speed.x*TimePerFrame.asSeconds() + (2.f*platformVelocity);
}
//check for contact with the falling platform
else if(collisionBottom && intersection && type == phys::bodyType::falling){
bodies[collisionSys.getBodyInfo().m_id].m_falling = true;
GRAVITY = 0.f;
playerBody.m_position.x += turbo*speed.x*TimePerFrame.asSeconds();
}
//check for contact with the vanishing platforms
else if(type == phys::bodyType::vanishing && collisionBottom){
GRAVITY = 0.f;
playerBody.m_position.x += turbo*speed.x*TimePerFrame.asSeconds();
}
else{
if(GRAVITY < 20.f)
GRAVITY += .4f;
else
GRAVITY = 20.f;
playerBody.m_position.y += GRAVITY;
playerBody.m_position.x += turbo*speed.x*TimePerFrame.asSeconds();
}
collisionSys.setCollisionInfo(false, false, false, false);//reset collision info every frame
//handle collisions - 1 call works, but 3 iterations seems to work well and reduce jittering
for(int i=0; i<3; ++i)
collisionSys.resolveCollisions(&playerBody, bodies, NUM_PLATFORM_OBJECTS);
playerBody.m_lastPosition = playerBody.m_position;
player.setPosition(playerBody.m_position);//attach player geometry to player body
}
//good enough for now - but will make you dizzy!!!
mainView.setCenter(player.getPosition());
window.clear();
window.setView(mainView);
for (auto i : tiles){
window.draw(i);
}
window.draw(player);
window.display();
}
return 0;
}
More to come, but as it is the weekend, I have beers to drink, and games to play. Until then, cheers!