i've been working on a game project, its a slider platformer and i've been having problem with getting the movement working, the movement is time based instead of being fixed to the frames per secound so the movement is consistent. the movement was working when i was testing out the algorithm lazily in the main file but i've put it all into a class that handles movement and collision detection and now it doesn't want to work, instead of object moving smoothly around the screen it goes flying right off the screen instead.

i've tried to find the root cause of the problem with no luck, with some help from tnbforum i've found that commenting out "Clock.Start()" in the main loop it all works, the problem with doing this is that the framerate is no longer regulated. in a frustrating effort to get to the cause of the problem i've been logging the values of the movement speed and object position which hasn't really helped, the calculations are all working but it seems to be breaking around when drawing the charcter but i'm not sure where abouts it is failing, is it with returning a reference to the collisionbox or is it when updating the collision box position?

i have done a lot of searching with no real luck on results.

source, going to be abit long:
timer.h:

//timer.h
#ifndef TIMER_H
#define TIMER_H

#include "SDL/SDL.h" //for Uint32

class Timer
{
    public:
        Timer();
        ~Timer();

        bool Started();
        bool Paused();

        void Stop();
        void Start();
        void Pause();
        void Continue();

        int GetTime();

    private:
        int ticks;
        int Pticks;

        bool started;
        bool paused;
};

#endif // TIMER_H

timer.cpp:

//timer.cpp
#include "timer/timer.h"
#include "SDL/SDL.h"

Timer::Timer() : ticks(0), Pticks(0), started(false), paused(false)
{
    //if the SDL subsystem timer isn't active then activate it.
    if(SDL_WasInit(SDL_INIT_TIMER) == 0) SDL_InitSubSystem(SDL_INIT_TIMER);
}

Timer::~Timer(){/* */}

bool Timer::Started(){ return started; }
bool Timer::Paused(){ return paused; }

void Timer::Start()
{
    started = true;
    paused = false;
    ticks = SDL_GetTicks();
}

void Timer::Stop()
{
    started = false;
    paused = false;
    ticks = 0;
    Pticks = 0;
}

void Timer::Pause()
{
    if(started == true){
        paused = true;
        Pticks = SDL_GetTicks() - ticks;
    }
}

void Timer::Continue()
{
    if(started == true && paused == true){
        paused = false;
        ticks = SDL_GetTicks() - Pticks;
    }
}

int Timer::GetTime()
{
    int time = 0;

    if(started == true){
        if(paused == true) time = Pticks;
        else time = SDL_GetTicks() - ticks;
    }

    return time;
}

inputevent.h:

//InputEvent.h
#ifndef INPUT_HANDLER
#define INPUT_HANDLER

#include "SDL/SDL.h"

class KeyBoardInput{
    public:
        KeyBoardInput();
        ~KeyBoardInput();

        void RefreshKeyState();

        bool WindowClose();
        bool GetKeyState(SDLKey Key);

    private:
        SDL_Event Input;
        Uint8* KeyState;
};

#endif // INPUT_HANDLER

inputevent.cpp

//InputEvent.cpp
#include "InputEvent.h"

KeyBoardInput::~KeyBoardInput(){ /* destructor */ }
KeyBoardInput::KeyBoardInput() : KeyState(SDL_GetKeyState(NULL)) { /* constructor */}

void KeyBoardInput::RefreshKeyState()
{
    //update input information
    SDL_PollEvent(&Input);
}
bool KeyBoardInput::WindowClose()
{
    if(Input.type == SDL_QUIT) return true;

    return false;
}

bool KeyBoardInput::GetKeyState(SDLKey Key)
{
    if(KeyState[Key] == 1) return true;

    return false;
}

graphics.h:

//Graphics.h
#ifndef GRAPHICS_CONTROL
#define GRAPHICS_CONTROL

#include "SDL/SDL.h"
#include "SDL/SDL_image.h"

struct Gposition{

    int X;
    int Y;
};

struct Gclip{

    int X, Y;
    int H, W;
};

struct Gsprite{

    Gclip* Crop;
    Gposition* Pos;
    SDL_Surface* Sprite;
};

class Graphics{
    public:
        enum{SUCCESS, ERROR};

        ~Graphics();
        Graphics();

        int Load(int ScreenWidth, int ScreenHeight, int BPP, const char* WindowTitle, const char* WindowIcon);
        void SetBackgroundColour(int Red, int Green, int Blue);

        int LoadImage(Gsprite& Image, const char* ImagePath);
        void CloseImage(Gsprite& Image);

        void DrawImage(const Gsprite& Image);
        void StartFrame();
        void ShowFrame();

        //group camera set up or adjust
        void SetCameraPosition(int X, int Y);
        void SetCameraDimensions(int W, int H);
        void SetUpCamera(int X, int Y, int W, int H);

        //return a copy of the camera
        Gclip GetCamera();

    private:
        //variables to store, modify and display the screen
        SDL_Surface* Screen;
        SDL_Rect ImagePos;
        SDL_Rect Crop;

        //camera to adjust image positions based on camera position
        Gclip Camera;

        //red, green, blue variables to hold the background colour
        int BackgroundR;
        int BackgroundG;
        int BackgroundB;
};

#endif // GRAPHICS_CONTROL

graphics.cpp:

//Graphics.cpp
#include "Graphics.h"

Graphics::~Graphics(){ SDL_Quit(); }
Gclip Graphics::GetCamera(){ return Camera; }
void Graphics::ShowFrame(){ SDL_Flip(Screen); }


Graphics::Graphics()
{
    //initialize SDL video and set defaults
    Screen = NULL;
    SDL_Init(SDL_INIT_VIDEO);

    Camera.X = 0;
    Camera.Y = 0;
    Camera.H = 0;
    Camera.W = 0;
}

int Graphics::Load(int ScreenWidth, int ScreenHeight, int BPP,
                   const char* WindowTitle, const char* WindowIcon)
{
    //create the window
    Screen = SDL_SetVideoMode(ScreenWidth, ScreenHeight, BPP, SDL_HWSURFACE | SDL_DOUBLEBUF);

    //if the window failed to create then exit
    if(Screen == NULL) return ERROR;

    SDL_WM_SetCaption(WindowTitle, WindowIcon);

    //set camera width and height to the screens dimensions
    Camera.W = ScreenWidth;
    Camera.H = ScreenHeight;

    return SUCCESS;
}

void Graphics::SetBackgroundColour(int Red, int Green, int Blue)
{
    BackgroundR = Red;
    BackgroundG = Green;
    BackgroundB = Blue;
}

int Graphics::LoadImage(Gsprite& Image, const char* ImagePath)
{
    //load images using the alpha channel
    Image.Sprite = IMG_Load(ImagePath);
    if(Image.Sprite != NULL) Image.Sprite = SDL_DisplayFormatAlpha(Image.Sprite);
    else return ERROR;

    return SUCCESS;
}

void Graphics::CloseImage(Gsprite& Image)
{
    //wrapper for SDL freesurface to close images
    SDL_FreeSurface(Image.Sprite);
}

void Graphics::DrawImage(const Gsprite& Image)
{

    //adjusts image position based on the camera position
    //the camera doesn't actually move but the world does
    //which works by subtracting camera position from the image position
    ImagePos.x = Image.Pos->X - Camera.X;
    ImagePos.y = Image.Pos->Y - Camera.Y;

    //if there image is cropped then
    //convert my Gclip struct to SDL_rect struct
    //and then attach the image to the screen else
    //attach the image to the screen without the crop
    if(Image.Crop != NULL){
        Crop.x = Image.Crop->X;
        Crop.y = Image.Crop->Y;
        Crop.h = Image.Crop->H;
        Crop.w = Image.Crop->W;

        SDL_BlitSurface(Image.Sprite, &Crop, Screen, &ImagePos); //with crop
    }else SDL_BlitSurface(Image.Sprite, NULL, Screen, &ImagePos); //without crop
}

void Graphics::StartFrame()
{
    //fill the screen with the background colour
    //best for erasing what was on the screen in the previous frame
    SDL_FillRect(Screen, NULL,
                 SDL_MapRGB(Screen->format, BackgroundR, BackgroundG, BackgroundB));
}

void Graphics::SetCameraPosition(int X, int Y)
{
    Camera.X = X;
    Camera.Y = Y;
}

void Graphics::SetCameraDimensions(int W, int H)
{
    Camera.W = W;
    Camera.H = H;
}

void Graphics::SetUpCamera(int X, int Y, int W, int H)
{
    Camera.X = X;
    Camera.Y = Y;
    Camera.W = W;
    Camera.H = H;
}

physics.h:

//Physics.h
#ifndef PHYSICS_H
#define PHYSICS_H

#include <iostream>
#include <fstream>
#include "SDL/SDL.h" //for Unit32

struct CollBox{
    float MinX, MinY;
    float MaxX, MaxY;
};

class Physics{
    public:
        enum Dir{LEFT, RIGHT, UP, DOWN};

        Physics();
        ~Physics();

        void SetSpeed(int Left, int Right, int Up, int Down);
        void SetUpPolygon(int MinX, int MinY, int MaxX, int MaxY);
        void SetAcceleration(int Left, int Right, int Up, int Down);

        void Update();
        void Move(Dir Direction, Uint32 Dtime);
        void StopMoving(Uint32 Dtime);

        void StopMovingUp(Uint32 Dtime);
        void StopMovingLeft(Uint32 Dtime);
        void StopMovingDown(Uint32 Dtime);
        void StopMovingRight(Uint32 Dtime);

        /*void MoveVertical(Dir, Uint32);
        void MoveHorizontal(Dir, Uint32);*/

        void SpringBack(const CollBox& Object);
        bool CheckCollision(const CollBox& Object);

        CollBox& GetPolygon();

    private:
        CollBox Polygon;

        float Xvel, Yvel;

        int MaxL, MaxR;
        int MaxU, MaxD;

        int AccelL, AccelR;
        int AccelU, AccelD;

        int SpeedHoriz, SpeedVertic;

        static const float Secs = 1000.0f;

        std::ofstream Log;
};
#endif // PHYSICS_H

physics.cpp:

//Physics.h
#include <iostream>
#include <fstream>
#include "Physics/Physics.h"
#include "SDL/SDL.h" //for Uint32

Physics::~Physics(){ if(Log.is_open()) Log.close(); }
CollBox& Physics::GetPolygon(){ return Polygon; }

Physics::Physics() : Xvel(0.0f), Yvel(0.0f),
                     MaxL(0), MaxR(0), MaxU(0), MaxD(0),
                     AccelL(0), AccelR(0), AccelU(0), AccelD(0), SpeedHoriz(0), SpeedVertic(0)
{
    Polygon.MinX = 0.0f;
    Polygon.MinY = 0.0f;
    Polygon.MaxX = 0.0f;
    Polygon.MaxY = 0.0f;

    Log.open("Log.txt");
}

void Physics::SetSpeed(int Left, int Right, int Up, int Down)
{
    MaxL = Left;
    MaxR = Right;
    MaxU = Up;
    MaxD = Down;
}

void Physics::SetUpPolygon(int MinX, int MinY, int MaxX, int MaxY)
{
    Polygon.MaxX = MaxX;
    Polygon.MaxY = MaxY;
    Polygon.MinX = MinX;
    Polygon.MinY = MinY;
}

void Physics::SetAcceleration(int Left, int Right, int Up, int Down)
{
    AccelL = Left;
    AccelR = Right;
    AccelU = Up;
    AccelD = Down;
}

void Physics::Update()
{
    Log << "Before:" << std::endl
        << "Xvel: " << Xvel << " " << "Yvel: " << Yvel << std::endl
        << "MinX: " << Polygon.MinX << " " << "MinY: " << Polygon.MinY << std::endl;

    //left or right
    Polygon.MinX += Xvel;
    Polygon.MaxX += Xvel;

    //up or down
    Polygon.MinY += Yvel;
    Polygon.MaxY += Yvel;

    Log << "After:" << std::endl
        << "Xvel: " << Xvel << " " << "Yvel: " << Yvel << std::endl
        << "MinX: " << Polygon.MinX << " " << "MinY: " << Polygon.MinY << std::endl << std::endl;

    //Log << Xvel << " " << Yvel << std::endl;
}

void Physics::Move(Dir Direction, Uint32 Dtime)
{
    if(Dtime == 0) return;

    switch(Direction){
        case UP : {
            //move up, if speed is to high reduce to max
            if(SpeedVertic != MaxU) SpeedVertic += AccelU;
            if(SpeedVertic < MaxU) SpeedVertic = MaxU;

            Yvel = SpeedVertic * (Dtime / Secs);

            /*Yvel += AccelU * (Dtime / Secs);
            if(Yvel < (MaxU * (Dtime / Secs))) Yvel = MaxU * (Dtime / Secs);*/
            break;
        }

        case DOWN : {
            //move down, if speed is to high reduce to max
            if(SpeedVertic != MaxD) SpeedVertic += AccelD;
            if(SpeedVertic > MaxD) SpeedVertic = MaxD;

            Yvel = SpeedVertic * (Dtime / Secs);

            /*Yvel += AccelD * (Dtime / Secs);
            if(Yvel > (MaxD * (Dtime / Secs))) Yvel = MaxD * (Dtime / Secs);*/
            break;
        }

        case LEFT : {
            //move left, if speed is to high reduce to max
            if(SpeedHoriz != MaxL) SpeedHoriz += AccelL;
            if(SpeedHoriz < MaxL) SpeedHoriz = MaxL;

            Xvel = SpeedHoriz * (Dtime / Secs);

            /*Xvel += AccelL * (Dtime / Secs);
            if(Xvel < (MaxL * (Dtime  / Secs))) Xvel = MaxL * (Dtime / Secs);*/
            break;
        }

        case RIGHT : {
            //move right, if speed is to high reduce to max
            if(SpeedHoriz != MaxR) SpeedHoriz += AccelR;
            if(SpeedHoriz > MaxR) SpeedHoriz = MaxR;

            Xvel = SpeedHoriz * (Dtime / Secs);

            /*Xvel += AccelR * (Dtime / Secs);
            if(Xvel > (MaxR * (Dtime / Secs))) Xvel = MaxR * (Dtime / Secs);*/
            break;
        }

        default : break;
    }

    //Log << "Xvel: " << Xvel << " " << "Yvel: " << Yvel << std::endl;
}

void Physics::StopMoving(Uint32 Dtime)
{
    if(Xvel == 0.0f && Yvel == 0.0f) return;
    if(Dtime == 0) return;

    StopMovingUp(Dtime);
    StopMovingLeft(Dtime);
    StopMovingDown(Dtime);
    StopMovingRight(Dtime);
}

void Physics::StopMovingUp(Uint32 Dtime)
{
    //if already not moving then return
    if(Yvel == 0.0f) return;
    if(Dtime == 0) return;

    //if moving up decelerate down
    if(SpeedVertic < 0){
        SpeedVertic += AccelD;
        Yvel = SpeedVertic * (Dtime / Secs);

        if(Yvel > 0.0f) Yvel = 0.0f;
        if(SpeedVertic > 0) SpeedVertic = 0;
    }

    /*if(Yvel < 0.0f){
        Yvel += AccelD * (Dtime / Secs);
        if(Yvel > 0.0f) Yvel = 0.0f;
    }*/
}

void Physics::StopMovingLeft(Uint32 Dtime)
{
    //if already not moving then return
    if(Xvel == 0.0f) return;
    if(Dtime == 0) return;

    //if moving left decelerate right
    if(SpeedHoriz < 0){
        SpeedHoriz += AccelR;
        Xvel = SpeedHoriz * (Dtime / Secs);

        if(Xvel > 0.0f) Xvel = 0.0f;
        if(SpeedHoriz > 0) SpeedHoriz = 0;
    }

    /*if(Xvel < 0.0f){
        Xvel += AccelR * (Dtime / Secs);
        if(Xvel > 0.0f) Xvel = 0.0f;
    }*/
}

void Physics::StopMovingDown(Uint32 Dtime)
{
    //if already not moving then return
    if(Yvel == 0.0f) return;
    if(Dtime == 0) return;

    //if moving down decelerate up
    if(SpeedVertic > 0){
        SpeedVertic += AccelU;
        Yvel = SpeedVertic * (Dtime / Secs);

        if(Yvel < 0.0f) Yvel = 0.0f;
        if(SpeedVertic < 0) SpeedVertic = 0;
    }

    /*if(Yvel > 0.0f){
        Yvel += AccelU * (Dtime / Secs);
        if(Yvel < 0.0f) Yvel = 0.0f;
    }*/
}

void Physics::StopMovingRight(Uint32 Dtime)
{
    //if already not moving then return
    if(Xvel == 0.0f) return;
    if(Dtime == 0) return;

    //if moving right decelerate left
    if(SpeedHoriz > 0){
        SpeedHoriz += AccelL;
        Xvel = SpeedHoriz * (Dtime / Secs);

        if(Xvel < 0.0f) Xvel = 0.0f;
        if(SpeedHoriz < 0) SpeedHoriz = 0;
    }

     /*if(Xvel > 0.0f){
        Xvel += AccelL * (Dtime / Secs);
        if(Xvel < 0.0f) Xvel = 0.0f;
    }*/
}

void Physics::SpringBack(const CollBox& Object)
{
    float X = 0;
    float Y = 0;

    //if the object is to the left or right else its above or below
    if(Polygon.MinY <= Object.MaxY && Polygon.MaxY >= Object.MinY){
        if(Xvel < 0.0f && Polygon.MinX < Object.MaxX) X = Object.MaxX - Polygon.MinX; //spring right
        if(Xvel > 0.0f && Polygon.MaxX > Object.MinX) X = Object.MinX - Polygon.MaxX; //spring left

        Xvel = 0;

    }else if(Polygon.MinX <= Object.MaxX && Polygon.MaxX >= Object.MinX){
        if(Yvel < 0.0f && Polygon.MinY < Object.MaxY) Y = Object.MaxY - Polygon.MinY; //spring down
        if(Yvel > 0.0f && Polygon.MaxY > Object.MinY) Y = Object.MinY - Polygon.MaxY; //spring up

        Yvel = 0;
    }

    Polygon.MinX += X;
    Polygon.MaxX += X;
    Polygon.MinY += Y;
    Polygon.MaxY += Y;
}

bool Physics::CheckCollision(const CollBox& Object)
{
    if(Polygon.MinX > Object.MaxX || Polygon.MaxX < Object.MinX) return false;
    if(Polygon.MinY > Object.MaxY || Polygon.MaxY < Object.MinY) return false;

    return true;
}

main.cpp:

//man.cpp
#include <iostream>
#include <fstream>
#include "SDL/SDL.h"
#include "timer/timer.h"
#include "SDL/SDL_image.h"
#include "Physics/Physics.h"
#include "Graphics/Graphics.h"
#include "input_handler/InputEvent.h"

class Character{
    public:
        Character(Graphics* Display, KeyBoardInput* input);
        ~Character();

        void UpdatePos();
        void Draw();

    private:
        Timer Delta;
        Physics Box;
        Gsprite Sprite;

        Graphics* Screen;
        KeyBoardInput* Input;

        std::ofstream imgpos;
        std::ofstream boxpos;
};

Character::Character(Graphics* Display, KeyBoardInput* input) : Screen(Display), Input(input)
{
    Sprite.Crop = NULL;
    Sprite.Sprite = NULL;
    Sprite.Pos = NULL;

    Sprite.Crop = new Gclip;
    Sprite.Pos = new Gposition;

    Sprite.Crop->H = 50;
    Sprite.Crop->W = 40;
    Sprite.Crop->Y = 1;
    Sprite.Crop->X = 143;

    Sprite.Pos->X = 0;
    Sprite.Pos->Y = 0;

    Box.SetSpeed(-300, 300, -300, 300);
    Box.SetAcceleration(-50, 50, -50, 50);
    Box.SetUpPolygon(0, 0, 40, 50);

    Screen->LoadImage(Sprite, "images/grunt walking alpha.png");

    imgpos.open("SpritePos.txt");
    boxpos.open("BoxPos.txt");
}

Character::~Character()
{
    delete Sprite.Crop;
    delete Sprite.Pos;

    Screen->CloseImage(Sprite);

    imgpos.close();
    boxpos.close();
}

void Character::UpdatePos()
{
    Uint32 Dtime = Delta.GetTime();
    //if(Dtime > 0) Log << Dtime << std::endl;

    if(Input->GetKeyState(SDLK_UP) == true) Box.Move(Physics::UP, Dtime);
    if(Input->GetKeyState(SDLK_UP) == false) Box.StopMovingUp(Dtime);

    if(Input->GetKeyState(SDLK_LEFT) == true) Box.Move(Physics::LEFT, Dtime);
    if(Input->GetKeyState(SDLK_LEFT) == false) Box.StopMovingLeft(Dtime);

    if(Input->GetKeyState(SDLK_RIGHT) == true) Box.Move(Physics::RIGHT, Dtime);
    if(Input->GetKeyState(SDLK_RIGHT) == false) Box.StopMovingRight(Dtime);

    if(Input->GetKeyState(SDLK_DOWN) == true) Box.Move(Physics::DOWN, Dtime);
    if(Input->GetKeyState(SDLK_DOWN) == false) Box.StopMovingDown(Dtime);

    Box.Update();
    Delta.Start();
}

void Character::Draw()
{
    const CollBox& ImgPos = Box.GetPolygon();

    Sprite.Pos->X = (int)ImgPos.MinX;
    Sprite.Pos->Y = (int)ImgPos.MinY;

    Screen->DrawImage(Sprite);

    imgpos << "X: " << Sprite.Pos->X << " " << "Y: " << Sprite.Pos->Y << std::endl;
    boxpos << "MinX: " << ImgPos.MinX << " " << "MinY: " << ImgPos.MinY << std::endl;
}

int main(int argC, char** argV)
{
    Graphics* Screen = new Graphics;
    if(Screen->Load(640, 480, 0, "Game Project", NULL) == Graphics::ERROR){
        delete Screen;
        return 1;
    }

    Screen->SetBackgroundColour(255, 255, 255);

    KeyBoardInput* Input = new KeyBoardInput();
    Character Player(Screen, Input);

    Screen->StartFrame();
    Player.Draw();
    Screen->ShowFrame();

    //30 frames per second
    const int FPS = 1000 / 30;
    Timer Clock;
    Clock.Start();

    do{
        Input->RefreshKeyState();
        Player.UpdatePos();

        while(Clock.GetTime() < FPS){}

        if(Clock.GetTime() >= FPS){
            Screen->StartFrame();
            Player.Draw();
            Screen->ShowFrame();
            Clock.Start();
        }

    }while(Input->WindowClose() != true);

    delete Screen;
    delete Input;

    return 0;
}

thats my full source code, when i comment out "Clock.Start();" in main.cpp it works but it doesn't regulate framerate so its trying to draw a frame as soon as possible, without it commented the object just goes straight off the screen. i've also tried putting in a awhile loop just to pause things untill its time to draw a frame and it all works again so i wonder if this has to do with how fast the program is running? just a wild guess.

once the movement is working i'll be testing the collision detection then figuring out a map set up and file structure.

i posted the full source just incase there is something i'm missing, thanks for the help in advance.

Recommended Answers

All 3 Replies

if(Clock.GetTime() >= FPS){
            Screen->StartFrame();
            Player.Draw();
            Screen->ShowFrame();
            Clock.Start();
        }

Why do you do Clock.start() here again? A better way to handle it is like so:

if(Clock.GetTime() >= FPS){
    Clock::TimeDiff diffTime = Clock.GetTime() - FPS;
    updatePhysics(diffTime);
    updateEntities(diffTime);
}

Check out a full tutoial here

ah, gafferongames, still trying to wrap my head around that article, i came across it a few days ago before i started the physics class

i do clock.start() to re-start the clock to time the next frame, in you're example wouldn't it work better if the condition statement was "if(Clock.GetTime() <= FPS)"? when its greater or equal to FPS then its time to draw the frame.

also, what would Clock::TimeDiff be defined as? would it be public "typedef Uint32 TimeDiff" in the physics class?

well, i'm not one to bump but it seems that i can't edit my previous reply.

thanks firstperson, i feel like an idiot for atleast not trying that before, it was right infront of me and i never saw it or considered it. now that i've tried it i think i understand it a lot better then just reading through it.

changed the main loop to:

do{
        const int Ntime = Clock.GetTime();
        int Dtime = Ntime - Ctime;
        Ctime = Ntime;

        if(Dtime > FPS) Dtime = FPS;
        Accum += Dtime;

        while(Accum >= FPS){
                Input->RefreshKeyState();
                Player.UpdatePos();
                Accum -= FPS;
        }

        Screen->StartFrame();
        Player.Draw();
        Screen->ShowFrame();

    }while(Input->WindowClose() != true);

and it all works fine now, just out of interest, why wasn't it working before? that i would really like to understand to get a better understanding of how things work.

thanks again, i spent a whole day trying to puzzle this out.

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.