diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2c8e978 --- /dev/null +++ b/Makefile @@ -0,0 +1,31 @@ +SOURCES = $(wildcard *.cpp) +OBJECTS = $(SOURCES:.cpp=.o) +DEPENDS = $(SOURCES:.cpp=.d) +LDFLAGS = $(shell pkg-config --libs gtkmm-2.4 gtkglextmm-1.2) +CPPFLAGS = $(shell pkg-config --cflags gtkmm-2.4 gtkglextmm-1.2) +CXXFLAGS = $(CPPFLAGS) -W -Wall -g +CXX = g++ +MAIN = game488 + +all: $(MAIN) + +depend: $(DEPENDS) + +clean: + rm -f *.o *.d $(MAIN) + +$(MAIN): $(OBJECTS) + @echo Creating $@... + @$(CXX) -arch i386 -o $@ $(OBJECTS) $(LDFLAGS) + +%.o: %.cpp + @echo Compiling $<... + @$(CXX) -arch i386 -o $@ -c $(CXXFLAGS) $< + +%.d: %.cpp + @echo Building $@... + @set -e; $(CC) -M $(CPPFLAGS) $< \ + | sed 's/\($*\)\.o[ :]*/\1.o $@ : /g' > $@; \ + [ -s $@ ] || rm -f $@ + +include $(DEPENDS) diff --git a/algebra.cpp b/algebra.cpp new file mode 100644 index 0000000..f19d5d5 --- /dev/null +++ b/algebra.cpp @@ -0,0 +1,143 @@ +//--------------------------------------------------------------------------- +// +// CS488 -- Introduction to Computer Graphics +// +// algebra.hpp/algebra.cpp +// +// Classes and functions for manipulating points, vectors, matrices, +// and colours. You probably won't need to modify anything in these +// two files. +// +// University of Waterloo Computer Graphics Lab / 2003 +// +//--------------------------------------------------------------------------- + +#include "algebra.hpp" + +double Vector3D::normalize() +{ + double denom = 1.0; + double x = (v_[0] > 0.0) ? v_[0] : -v_[0]; + double y = (v_[1] > 0.0) ? v_[1] : -v_[1]; + double z = (v_[2] > 0.0) ? v_[2] : -v_[2]; + + if(x > y) { + if(x > z) { + if(1.0 + x > 1.0) { + y = y / x; + z = z / x; + denom = 1.0 / (x * sqrt(1.0 + y*y + z*z)); + } + } else { /* z > x > y */ + if(1.0 + z > 1.0) { + y = y / z; + x = x / z; + denom = 1.0 / (z * sqrt(1.0 + y*y + x*x)); + } + } + } else { + if(y > z) { + if(1.0 + y > 1.0) { + z = z / y; + x = x / y; + denom = 1.0 / (y * sqrt(1.0 + z*z + x*x)); + } + } else { /* x < y < z */ + if(1.0 + z > 1.0) { + y = y / z; + x = x / z; + denom = 1.0 / (z * sqrt(1.0 + y*y + x*x)); + } + } + } + + if(1.0 + x + y + z > 1.0) { + v_[0] *= denom; + v_[1] *= denom; + v_[2] *= denom; + return 1.0 / denom; + } + + return 0.0; +} + +/* + * Define some helper functions for matrix inversion. + */ + +static void swaprows(Matrix4x4& a, size_t r1, size_t r2) +{ + std::swap(a[r1][0], a[r2][0]); + std::swap(a[r1][1], a[r2][1]); + std::swap(a[r1][2], a[r2][2]); + std::swap(a[r1][3], a[r2][3]); +} + +static void dividerow(Matrix4x4& a, size_t r, double fac) +{ + a[r][0] /= fac; + a[r][1] /= fac; + a[r][2] /= fac; + a[r][3] /= fac; +} + +static void submultrow(Matrix4x4& a, size_t dest, size_t src, double fac) +{ + a[dest][0] -= fac * a[src][0]; + a[dest][1] -= fac * a[src][1]; + a[dest][2] -= fac * a[src][2]; + a[dest][3] -= fac * a[src][3]; +} + +/* + * invertMatrix + * + * I lifted this code from the skeleton code of a raytracer assignment + * from a different school. I taught that course too, so I figured it + * would be okay. + */ +Matrix4x4 Matrix4x4::invert() const +{ + /* The algorithm is plain old Gauss-Jordan elimination + with partial pivoting. */ + + Matrix4x4 a(*this); + Matrix4x4 ret; + + /* Loop over cols of a from left to right, + eliminating above and below diag */ + + /* Find largest pivot in column j among rows j..3 */ + for(size_t j = 0; j < 4; ++j) { + size_t i1 = j; /* Row with largest pivot candidate */ + for(size_t i = j + 1; i < 4; ++i) { + if(fabs(a[i][j]) > fabs(a[i1][j])) { + i1 = i; + } + } + + /* Swap rows i1 and j in a and ret to put pivot on diagonal */ + swaprows(a, i1, j); + swaprows(ret, i1, j); + + /* Scale row j to have a unit diagonal */ + if(a[j][j] == 0.0) { + // Theoretically throw an exception. + return ret; + } + + dividerow(ret, j, a[j][j]); + dividerow(a, j, a[j][j]); + + /* Eliminate off-diagonal elems in col j of a, doing identical + ops to b */ + for(size_t i = 0; i < 4; ++i) { + if(i != j) { + submultrow(ret, i, j, a[i][j]); + submultrow(a, i, j, a[i][j]); + } + } + } + + return ret; +} diff --git a/algebra.hpp b/algebra.hpp new file mode 100644 index 0000000..9848adf --- /dev/null +++ b/algebra.hpp @@ -0,0 +1,490 @@ +//--------------------------------------------------------------------------- +// +// CS488 -- Introduction to Computer Graphics +// +// algebra.hpp/algebra.cpp +// +// Classes and functions for manipulating points, vectors, matrices, +// and colours. You probably won't need to modify anything in these +// two files. +// +// University of Waterloo Computer Graphics Lab / 2003 +// +//--------------------------------------------------------------------------- + +#ifndef CS488_ALGEBRA_HPP +#define CS488_ALGEBRA_HPP + +#include +#include +#include + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +class Point2D +{ +public: + Point2D() + { + v_[0] = 0.0; + v_[1] = 0.0; + } + Point2D(double x, double y) + { + v_[0] = x; + v_[1] = y; + } + Point2D(const Point2D& other) + { + v_[0] = other.v_[0]; + v_[1] = other.v_[1]; + } + + Point2D& operator =(const Point2D& other) + { + v_[0] = other.v_[0]; + v_[1] = other.v_[1]; + return *this; + } + + double& operator[](size_t idx) + { + return v_[ idx ]; + } + double operator[](size_t idx) const + { + return v_[ idx ]; + } + +private: + double v_[2]; +}; + +class Point3D +{ +public: + Point3D() + { + v_[0] = 0.0; + v_[1] = 0.0; + v_[2] = 0.0; + } + Point3D(double x, double y, double z) + { + v_[0] = x; + v_[1] = y; + v_[2] = z; + } + Point3D(const Point3D& other) + { + v_[0] = other.v_[0]; + v_[1] = other.v_[1]; + v_[2] = other.v_[2]; + } + + Point3D& operator =(const Point3D& other) + { + v_[0] = other.v_[0]; + v_[1] = other.v_[1]; + v_[2] = other.v_[2]; + return *this; + } + + double& operator[](size_t idx) + { + return v_[ idx ]; + } + double operator[](size_t idx) const + { + return v_[ idx ]; + } + +private: + double v_[3]; +}; + +class Vector3D +{ +public: + Vector3D() + { + v_[0] = 0.0; + v_[1] = 0.0; + v_[2] = 0.0; + } + Vector3D(double x, double y, double z) + { + v_[0] = x; + v_[1] = y; + v_[2] = z; + } + Vector3D(const Vector3D& other) + { + v_[0] = other.v_[0]; + v_[1] = other.v_[1]; + v_[2] = other.v_[2]; + } + + Vector3D& operator =(const Vector3D& other) + { + v_[0] = other.v_[0]; + v_[1] = other.v_[1]; + v_[2] = other.v_[2]; + return *this; + } + + double& operator[](size_t idx) + { + return v_[ idx ]; + } + double operator[](size_t idx) const + { + return v_[ idx ]; + } + + double dot(const Vector3D& other) const + { + return v_[0]*other.v_[0] + v_[1]*other.v_[1] + v_[2]*other.v_[2]; + } + + double length2() const + { + return v_[0]*v_[0] + v_[1]*v_[1] + v_[2]*v_[2]; + } + double length() const + { + return sqrt(length2()); + } + + double normalize(); + + Vector3D cross(const Vector3D& other) const + { + return Vector3D( + v_[1]*other[2] - v_[2]*other[1], + v_[2]*other[0] - v_[0]*other[2], + v_[0]*other[1] - v_[1]*other[0]); + } + +private: + double v_[3]; +}; + +inline Vector3D operator *(double s, const Vector3D& v) +{ + return Vector3D(s*v[0], s*v[1], s*v[2]); +} + +inline Vector3D operator +(const Vector3D& a, const Vector3D& b) +{ + return Vector3D(a[0]+b[0], a[1]+b[1], a[2]+b[2]); +} + +inline Point3D operator +(const Point3D& a, const Vector3D& b) +{ + return Point3D(a[0]+b[0], a[1]+b[1], a[2]+b[2]); +} + +inline Vector3D operator -(const Point3D& a, const Point3D& b) +{ + return Vector3D(a[0]-b[0], a[1]-b[1], a[2]-b[2]); +} + +inline Vector3D operator -(const Vector3D& a, const Vector3D& b) +{ + return Vector3D(a[0]-b[0], a[1]-b[1], a[2]-b[2]); +} + +inline Vector3D operator -(const Vector3D& a) +{ + return Vector3D(-a[0], -a[1], -a[2]); +} + +inline Point3D operator -(const Point3D& a, const Vector3D& b) +{ + return Point3D(a[0]-b[0], a[1]-b[1], a[2]-b[2]); +} + +inline Vector3D cross(const Vector3D& a, const Vector3D& b) +{ + return a.cross(b); +} + +inline std::ostream& operator <<(std::ostream& os, const Point2D& p) +{ + return os << "p<" << p[0] << "," << p[1] << ">"; +} + +inline std::ostream& operator <<(std::ostream& os, const Point3D& p) +{ + return os << "p<" << p[0] << "," << p[1] << "," << p[2] << ">"; +} + +inline std::ostream& operator <<(std::ostream& os, const Vector3D& v) +{ + return os << "v<" << v[0] << "," << v[1] << "," << v[2] << ">"; +} + +class Matrix4x4; + +class Vector4D +{ +public: + Vector4D() + { + v_[0] = 0.0; + v_[1] = 0.0; + v_[2] = 0.0; + v_[3] = 0.0; + } + Vector4D(double x, double y, double z, double w) + { + v_[0] = x; + v_[1] = y; + v_[2] = z; + v_[3] = w; + } + Vector4D(const Vector4D& other) + { + v_[0] = other.v_[0]; + v_[1] = other.v_[1]; + v_[2] = other.v_[2]; + v_[3] = other.v_[3]; + } + + Vector4D& operator =(const Vector4D& other) + { + v_[0] = other.v_[0]; + v_[1] = other.v_[1]; + v_[2] = other.v_[2]; + v_[3] = other.v_[3]; + return *this; + } + + double& operator[](size_t idx) + { + return v_[ idx ]; + } + double operator[](size_t idx) const + { + return v_[ idx ]; + } + +private: + double v_[4]; +}; + +class Matrix4x4 +{ +public: + Matrix4x4() + { + // Construct an identity matrix + std::fill(v_, v_+16, 0.0); + v_[0] = 1.0; + v_[5] = 1.0; + v_[10] = 1.0; + v_[15] = 1.0; + } + Matrix4x4(const Matrix4x4& other) + { + std::copy(other.v_, other.v_+16, v_); + } + Matrix4x4(const Vector4D row1, const Vector4D row2, const Vector4D row3, + const Vector4D row4) + { + v_[0] = row1[0]; + v_[1] = row1[1]; + v_[2] = row1[2]; + v_[3] = row1[3]; + + v_[4] = row2[0]; + v_[5] = row2[1]; + v_[6] = row2[2]; + v_[7] = row2[3]; + + v_[8] = row3[0]; + v_[9] = row3[1]; + v_[10] = row3[2]; + v_[11] = row3[3]; + + v_[12] = row4[0]; + v_[13] = row4[1]; + v_[14] = row4[2]; + v_[15] = row4[3]; + } + Matrix4x4(double *vals) + { + std::copy(vals, vals + 16, (double*)v_); + } + + Matrix4x4& operator=(const Matrix4x4& other) + { + std::copy(other.v_, other.v_+16, v_); + return *this; + } + + Vector4D getRow(size_t row) const + { + return Vector4D(v_[4*row], v_[4*row+1], v_[4*row+2], v_[4*row+3]); + } + double *getRow(size_t row) + { + return (double*)v_ + 4*row; + } + + Vector4D getColumn(size_t col) const + { + return Vector4D(v_[col], v_[4+col], v_[8+col], v_[12+col]); + } + + Vector4D operator[](size_t row) const + { + return getRow(row); + } + double *operator[](size_t row) + { + return getRow(row); + } + + Matrix4x4 transpose() const + { + return Matrix4x4(getColumn(0), getColumn(1), + getColumn(2), getColumn(3)); + } + Matrix4x4 invert() const; + + const double *begin() const + { + return (double*)v_; + } + const double *end() const + { + return begin() + 16; + } + +private: + double v_[16]; +}; + +inline Matrix4x4 operator *(const Matrix4x4& a, const Matrix4x4& b) +{ + Matrix4x4 ret; + + for(size_t i = 0; i < 4; ++i) { + Vector4D row = a.getRow(i); + + for(size_t j = 0; j < 4; ++j) { + ret[i][j] = row[0] * b[0][j] + row[1] * b[1][j] + + row[2] * b[2][j] + row[3] * b[3][j]; + } + } + + return ret; +} + +inline Vector3D operator *(const Matrix4x4& M, const Vector3D& v) +{ + return Vector3D( + v[0] * M[0][0] + v[1] * M[0][1] + v[2] * M[0][2], + v[0] * M[1][0] + v[1] * M[1][1] + v[2] * M[1][2], + v[0] * M[2][0] + v[1] * M[2][1] + v[2] * M[2][2]); +} + +inline Point3D operator *(const Matrix4x4& M, const Point3D& p) +{ + return Point3D( + p[0] * M[0][0] + p[1] * M[0][1] + p[2] * M[0][2] + M[0][3], + p[0] * M[1][0] + p[1] * M[1][1] + p[2] * M[1][2] + M[1][3], + p[0] * M[2][0] + p[1] * M[2][1] + p[2] * M[2][2] + M[2][3]); +} + +inline Vector3D transNorm(const Matrix4x4& M, const Vector3D& n) +{ + return Vector3D( + n[0] * M[0][0] + n[1] * M[1][0] + n[2] * M[2][0], + n[0] * M[0][1] + n[1] * M[1][1] + n[2] * M[2][1], + n[0] * M[0][2] + n[1] * M[1][2] + n[2] * M[2][2]); +} + +inline std::ostream& operator <<(std::ostream& os, const Matrix4x4& M) +{ + return os << "[" << M[0][0] << " " << M[0][1] << " " + << M[0][2] << " " << M[0][3] << "]" << std::endl + << "[" << M[1][0] << " " << M[1][1] << " " + << M[1][2] << " " << M[1][3] << "]" << std::endl + << "[" << M[2][0] << " " << M[2][1] << " " + << M[2][2] << " " << M[2][3] << "]" << std::endl + << "[" << M[3][0] << " " << M[3][1] << " " + << M[3][2] << " " << M[3][3] << "]"; +} + +class Colour +{ +public: + Colour(double r, double g, double b) + : r_(r) + , g_(g) + , b_(b) + {} + Colour(double c) + : r_(c) + , g_(c) + , b_(c) + {} + Colour(const Colour& other) + : r_(other.r_) + , g_(other.g_) + , b_(other.b_) + {} + + Colour& operator =(const Colour& other) + { + r_ = other.r_; + g_ = other.g_; + b_ = other.b_; + return *this; + } + + double R() const + { + return r_; + } + double G() const + { + return g_; + } + double B() const + { + return b_; + } + +private: + double r_; + double g_; + double b_; +}; + +inline Colour operator *(double s, const Colour& a) +{ + return Colour(s*a.R(), s*a.G(), s*a.B()); +} + +inline Colour operator *(const Colour& a, const Colour& b) +{ + return Colour(a.R()*b.R(), a.G()*b.G(), a.B()*b.B()); +} + +inline Colour operator +(const Colour& a, const Colour& b) +{ + return Colour(a.R()+b.R(), a.G()+b.G(), a.B()+b.B()); +} + +inline std::ostream& operator <<(std::ostream& os, const Colour& c) +{ + return os << "c<" << c.R() << "," << c.G() << "," << c.B() << ">"; +} + +#endif // CS488_ALGEBRA_HPP diff --git a/appwindow.cpp b/appwindow.cpp new file mode 100644 index 0000000..6630445 --- /dev/null +++ b/appwindow.cpp @@ -0,0 +1,101 @@ +#include "appwindow.hpp" +#include +#include + +AppWindow::AppWindow() +{ + set_title("488 Tetrominoes on the Wall"); + + // A utility class for constructing things that go into menus, which + // we'll set up next. + using Gtk::Menu_Helpers::MenuElem; + using Gtk::Menu_Helpers::RadioMenuElem; + using Gtk::Menu_Helpers::CheckMenuElem; + + // Slots to connect to functions + sigc::slot1 draw_slot = sigc::mem_fun(m_viewer, &Viewer::setDrawMode); + sigc::slot1 speed_slot = sigc::mem_fun(m_viewer, &Viewer::setSpeed); + sigc::slot0 buffer_slot = sigc::mem_fun(m_viewer, &Viewer::toggleBuffer); + + // Set up the application menu + // The slot we use here just causes AppWindow::hide() on this, + // which shuts down the application. + m_menu_app.items().push_back(MenuElem("_New Game", Gtk::AccelKey("n"), sigc::mem_fun(m_viewer, &Viewer::newGame ) ) ); + m_menu_app.items().push_back(MenuElem("_Reset", Gtk::AccelKey("r"), sigc::mem_fun(m_viewer, &Viewer::resetView ) ) ); + m_menu_app.items().push_back(MenuElem("_Quit", Gtk::AccelKey("q"), + sigc::mem_fun(*this, &AppWindow::hide))); + + m_menu_drawMode.items().push_back(MenuElem("_Wire-Frame", Gtk::AccelKey("w"), sigc::bind( draw_slot, Viewer::WIRE ) ) ); + m_menu_drawMode.items().push_back(MenuElem("_Face", Gtk::AccelKey("f"), sigc::bind( draw_slot, Viewer::FACE ) ) ); + m_menu_drawMode.items().push_back(MenuElem("_Multicoloured", Gtk::AccelKey("m"), sigc::bind( draw_slot, Viewer::MULTICOLOURED ) ) ); + + m_menu_speed.items().push_back(RadioMenuElem(m_group_speed, "_Slow", sigc::bind( speed_slot, Viewer::SLOW ) ) ); + m_menu_speed.items().push_back(RadioMenuElem(m_group_speed, "_Medium", sigc::bind( speed_slot, Viewer::MEDIUM ) ) ); + m_menu_speed.items().push_back(RadioMenuElem(m_group_speed, "_Fast", sigc::bind( speed_slot, Viewer::FAST ) ) ); + + m_menu_buffer.items().push_back(CheckMenuElem("_Double Buffer", Gtk::AccelKey("b"), buffer_slot )); + + // Set up the menu bar + m_menubar.items().push_back(Gtk::Menu_Helpers::MenuElem("_File", m_menu_app)); + m_menubar.items().push_back(Gtk::Menu_Helpers::MenuElem("_Draw Mode", m_menu_drawMode)); + m_menubar.items().push_back(Gtk::Menu_Helpers::MenuElem("_Speed", m_menu_speed)); + m_menubar.items().push_back(Gtk::Menu_Helpers::MenuElem("_Buffer", m_menu_buffer)); + + // Set up the score label + scoreLabel.set_text("Score:\t0"); + linesClearedLabel.set_text("Lines Cleared:\t0"); + + m_viewer.setScoreWidgets(&scoreLabel, &linesClearedLabel); + + // Pack in our widgets + + // First add the vertical box as our single "top" widget + add(m_vbox); + + // Put the menubar on the top, and make it as small as possible + m_vbox.pack_start(m_menubar, Gtk::PACK_SHRINK); + m_vbox.pack_start(linesClearedLabel, Gtk::PACK_EXPAND_PADDING); + m_vbox.pack_start(scoreLabel, Gtk::PACK_EXPAND_PADDING); + + // Put the viewer below the menubar. pack_start "grows" the widget + // by default, so it'll take up the rest of the window. + + //m_viewer.set_size_request(300, 600); + m_viewer.set_size_request(600, 900); + m_vbox.pack_start(m_viewer); + + show_all(); +} + +bool AppWindow::on_key_press_event( GdkEventKey *ev ) +{ + // This is a convenient place to handle non-shortcut + // keys. You'll want to look at ev->keyval. + + // An example key; delete and replace with the + // keys you want to process + // GDK_Left, _up, Right, Down + if (ev->keyval == GDK_Shift_L || ev->keyval == GDK_Shift_R) + { + m_viewer.startScale(); + return Gtk::Window::on_key_press_event( ev ); + } + else + { + m_viewer.on_key_press_event(ev); + return Gtk::Window::on_key_press_event( ev ); + } +} + +bool AppWindow::on_key_release_event (GdkEventKey *ev) +{ + m_viewer.endScale(); + return Gtk::Window::on_key_release_event( ev );; +} + +void AppWindow::updateScore(int newScore) +{ + scoreLabel.set_text("Score:\t" + newScore); +} + + diff --git a/appwindow.hpp b/appwindow.hpp new file mode 100644 index 0000000..e9a3167 --- /dev/null +++ b/appwindow.hpp @@ -0,0 +1,36 @@ +#ifndef APPWINDOW_HPP +#define APPWINDOW_HPP + +#include +#include "viewer.hpp" + +class AppWindow : public Gtk::Window { +public: + AppWindow(); + void updateScore(int newScore); + void updateLinesCleared(int linesCleared); + +protected: + virtual bool on_key_press_event( GdkEventKey *ev ); + virtual bool on_key_release_event (GdkEventKey *ev); + +private: + // A "vertical box" which holds everything in our window + Gtk::VBox m_vbox; + + // The menubar, with all the menus at the top of the window + Gtk::MenuBar m_menubar; + + // Each menu itself + Gtk::Menu m_menu_app; + Gtk::Menu m_menu_drawMode; + Gtk::Menu m_menu_buffer; + Gtk::Menu m_menu_speed; + Gtk::RadioButtonGroup m_group_speed; + // The main OpenGL area + Viewer m_viewer; + + // Label widgets + Gtk::Label scoreLabel, linesClearedLabel; +}; +#endif diff --git a/game.cpp b/game.cpp new file mode 100644 index 0000000..25f4f61 --- /dev/null +++ b/game.cpp @@ -0,0 +1,562 @@ +//--------------------------------------------------------------------------- +// +// CS488 -- Introduction to Computer Graphics +// +// game.hpp/game.cpp +// +// An engine that implements a falling blocks game. You won't need to +// modify these files unless you decide to enhance the underlying game +// logic. +// +// University of Waterloo Computer Graphics Lab / 2003 +// +//--------------------------------------------------------------------------- + +#include + +#include "game.hpp" + +static const Piece PIECES[] = { + Piece( + "...." + ".xx." + ".xx." + "....", 0, 1,1,1,1), // Blue + Piece( + "...." + ".xx." + ".xo." + "....", 1, 1,1,1,1), // purple + Piece( + "...." + ".xx." + ".oo." + "....", 2, 1,1,1,1), // orange + Piece( + "...." + ".ox." + ".oo." + "....", 3, 1,1,1,1), // green + Piece( + "...." + ".oo." + ".oo." + "....", 4, 1,1,1,1), // red + Piece( + "...." + ".xo." + ".ox." + "....", 5, 1,1,1,1), // pink +/* + Piece( + "...." + ".xx." + ".xx." + "....", 6, 1,1,1,1) // yellow*/ +}; + +Piece::Piece(const char *desc, int cindex, + int left, int top, int right, int bottom) +{ + std::copy(desc, desc + 16, desc_); + cindex_ = cindex; + margins_[0] = left; + margins_[1] = top; + margins_[2] = right; + margins_[3] = bottom; +} + +Piece::Piece() +{} + +Piece& Piece::operator =(const Piece& other) +{ + std::copy(other.desc_, other.desc_ + 16, desc_); + std::copy(other.margins_, other.margins_ + 4, margins_); + cindex_ = other.cindex_; + return *this; +} + +int Piece::getLeftMargin() const +{ + return margins_[0]; +} + +int Piece::getTopMargin() const +{ + return margins_[1]; +} + +int Piece::getRightMargin() const +{ + return margins_[2]; +} + +int Piece::getBottomMargin() const +{ + return margins_[3]; +} + +int Piece::getColourIndex(int row, int col) const +{ + if (desc_[ row*4 + col ] == 'x') + return 1; + else if (desc_[ row*4 + col ] == 'o') + return 2; + + return 0; +} + +Piece Piece::rotateCW() const +{ + char ndesc[16]; + getColumnRev(0, (char*)ndesc); + getColumnRev(1, (char*)(ndesc+4)); + getColumnRev(2, (char*)(ndesc+8)); + getColumnRev(3, (char*)(ndesc+12)); + + return Piece(ndesc, cindex_, + margins_[3], margins_[0], margins_[1], margins_[2]); +} + +Piece Piece::rotateCCW() const +{ + char ndesc[16]; + getColumn(3, (char*)ndesc); + getColumn(2, (char*)(ndesc+4)); + getColumn(1, (char*)(ndesc+8)); + getColumn(0, (char*)(ndesc+12)); + + return Piece(ndesc, cindex_, + margins_[1], margins_[2], margins_[3], margins_[0]); +} + +bool Piece::isOn(int row, int col) const +{ + return desc_[ row*4 + col ] == 'x' || desc_[ row*4 + col ] == 'o'; +} + +void Piece::getColumn(int col, char *buf) const +{ + buf[0] = desc_[col]; + buf[1] = desc_[col+4]; + buf[2] = desc_[col+8]; + buf[3] = desc_[col+12]; +} + +void Piece::getColumnRev(int col, char *buf) const +{ + buf[0] = desc_[col+12]; + buf[1] = desc_[col+8]; + buf[2] = desc_[col+4]; + buf[3] = desc_[col]; +} + +Game::Game(int width, int height) + : board_width_(width) + , board_height_(height) + , stopped_(false) + , linesCleared_(0) + , score_(0) +{ + int sz = board_width_ * (board_height_+4); + + board_ = new int[ sz ]; + std::fill(board_, board_ + sz, -1); + generateNewPiece(); +} + +void Game::reset() +{ + stopped_ = false; + std::fill(board_, board_ + (board_width_*(board_height_+4)), -1); + linesCleared_ = 0; + score_ = 0; + generateNewPiece(); +} + +Game::~Game() +{ + delete [] board_; +} + +int Game::get(int r, int c) const +{ + return board_[ r*board_width_ + c ]; +} + +int& Game::get(int r, int c) +{ + return board_[ r*board_width_ + c ]; +} + +bool Game::doesPieceFit(const Piece& p, int x, int y) const +{ + if(x + p.getLeftMargin() < 0) { + return false; + } + + if(x + 3 - p.getRightMargin() >= board_width_) { + return false; + } + + if(y + p.getBottomMargin() < 3) { + return false; + } + + for(int r = 0; r < 4; ++r) { + for(int c = 0; c < 4; ++c) { + if(p.isOn(r, c)) { + if(get(y-r, x+c) != -1) { + return false; + } + } + } + } + + return true; +} + +void Game::removePiece(const Piece& p, int x, int y) +{ + for(int r = 0; r < 4; ++r) { + for(int c = 0; c < 4; ++c) { + if(p.isOn(r, c)) { + get(y-r, x+c) = -1; + } + } + } +} + +void Game::removeRow(int y) +{ + for(int r = y + 1; r < board_height_ + 4; ++r) { + for(int c = 0; c < board_width_; ++c) { + get(r-1, c) = get(r, c); + } + } + + for(int c = 0; c < board_width_; ++c) { + get(board_height_+3, c) = -1; + } +} + +int Game::collapse() +{ + // This method is implemented in a brain-dead way. Repeatedly + // walk up from the bottom of the well, removing the first full + // row, stopping when there are no more full rows. It could be + // made much faster. Sue me. + + int removed = 0; + + for (int r = 0; r= board_height_) + { + // you lose. + stopped_ = true; + return -1; + } + else + { + // break piece and keep moving down if need be + + // The right side can drop more + if(get(ny-2, px_+1) != -1 && get(ny-2, px_+2) == -1) + { + /* Piece temp = piece_; + temp.removeHalf(1); + removePiece(piece_, px_, py_); + placePiece(temp, px_, py_); + while(true) + { + if(get(ny-2, px_+2) != -1) + { + break; + } + --ny; + } + piece_.removeHalf(0); + ++ny; + placePiece(piece_, px_, ny);*/ + dropPiece(0); + } + else if(get(ny-2, px_+1) == -1 && get(ny-2, px_+2) != -1) + { + dropPiece(1); + } + + + int rm = collapse(); +/* int level = 1 + linesCleared_ / 10; + switch (rm) + { + case 0: + score_ += 10 * level; + break; + case 1: + score_ += rm * 100 * level; + break; + case 2: + score_ += rm * 300 * level; + break; + case 3: + score_ += rm * 500 * level; + break; + case 4: + score_ += rm * 800 * level; + break; + } + linesCleared_ += rm;*/ + generateNewPiece(); + return rm; + } + } + else + { + placePiece(piece_, px_, ny); + py_ = ny; + return 0; + } +} + +void Game::dropPiece(int side) +{ + int ny = py_ - 1; + Piece temp = piece_; + temp.removeHalf((side + 1)%2); + removePiece(piece_, px_, py_); + placePiece(temp, px_, py_); + while(true) + { + if(get(ny-2, px_ + 1 + (side + 1)%2) != -1) + { + break; + } + --ny; + } + piece_.removeHalf(side); + ++ny; + placePiece(piece_, px_, ny); +} +bool Game::moveLeft() +{ + // Most of the piece movement methods work like this: + // 1. remove the piece from the board. + // 2. does the piece fit in its new configuration? + // 3a. if yes, add it to the board in its new configuration. + // 3b. if no, put it back where it was. + // Simple and sort of silly, but satisfactory. + + int nx = px_ - 1; + + removePiece(piece_, px_, py_); + if(doesPieceFit(piece_, nx, py_)) { + placePiece(piece_, nx, py_); + px_ = nx; + return true; + } else { + placePiece(piece_, px_, py_); + return false; + } +} + +bool Game::moveRight() +{ + int nx = px_ + 1; + + removePiece(piece_, px_, py_); + if(doesPieceFit(piece_, nx, py_)) { + placePiece(piece_, nx, py_); + px_ = nx; + return true; + } else { + placePiece(piece_, px_, py_); + return false; + } +} + +bool Game::drop() +{ + removePiece(piece_, px_, py_); + int ny = py_; + + while(true) + { + --ny; + score_ += 1 + (linesCleared_ / 10); + if(!doesPieceFit(piece_, px_, ny)) + { + break; + } + } + + ++ny; + placePiece(piece_, px_, ny); + + if(ny == py_) + { + return false; + } + else + { + py_ = ny; + return true; + } +} + +bool Game::rotateCW() +{ + removePiece(piece_, px_, py_); + //removePiece(shadowPiece_, sx_, sy_); + Piece npiece = piece_.rotateCW(); + + if(doesPieceFit(npiece, px_, py_)) + { + shadowPiece_ = npiece; + placePiece(npiece, px_, py_); + piece_ = npiece; + return true; + } + else + { + placePiece(piece_, px_, py_); + return false; + } +} + +bool Game::rotateCCW() +{ + removePiece(piece_, px_, py_); +// removePiece(shadowPiece_, sx_, sy_); + Piece npiece = piece_.rotateCCW(); + if(doesPieceFit(npiece, px_, py_)) + { + shadowPiece_ = npiece; + placePiece(npiece, px_, py_); + piece_ = npiece; + return true; + } + else + { + placePiece(piece_, px_, py_); + return false; + } +} + +void Game::dropShadowPiece() +{ + removePiece(shadowPiece_, sx_, sy_); + int ny = sy_; + sx_ = px_; + while(true) + { + --ny; + if(!doesPieceFit(shadowPiece_, sx_, ny)) + break; + } + + ++ny; + + for(int r = 0; r < 4; ++r) + { + for(int c = 0; c < 4; ++c) + { + if(shadowPiece_.isOn(r, c)) + get(ny-r, sx_+c) = shadowPiece_.getColourIndex(r, c); + } + } + + if(ny != sy_) + sy_ = ny; +} diff --git a/game.hpp b/game.hpp new file mode 100644 index 0000000..179a308 --- /dev/null +++ b/game.hpp @@ -0,0 +1,165 @@ +//--------------------------------------------------------------------------- +// +// CS488 -- Introduction to Computer Graphics +// +// game.hpp/game.cpp +// +// An engine that implements a falling blocks game. You won't need to +// modify these files unless you decide to enhance the underlying game +// logic. +// +// University of Waterloo Computer Graphics Lab / 2003 +// +//--------------------------------------------------------------------------- + +#ifndef CS488_GAME_HPP +#define CS488_GAME_HPP + +#include + +class Piece { +public: + Piece(); + Piece(const char *desc, int cindex, + int left, int top, int right, int bottom); + + Piece& operator =(const Piece& other); + + int getLeftMargin() const; + int getTopMargin() const; + int getRightMargin() const; + int getBottomMargin() const; + int getColourIndex(int row, int col) const; + + Piece rotateCW() const; + Piece rotateCCW() const; + + bool isOn(int row, int col) const; + + int margins_[4]; + void removeHalf(int side) + { + // Remove left half + if (side == 0) + { + desc_[ 1*4 + 1 ] = '.'; + desc_[ 2*4 + 1 ] = '.'; + } + else + { + desc_[ 1*4 + 2 ] = '.'; + desc_[ 2*4 + 2 ] = '.'; + } + } + +private: + void getColumn(int col, char *buf) const; + void getColumnRev(int col, char *buf) const; + + char desc_[16]; + int cindex_; +// int margins_[4]; +}; + +class Game +{ +public: + // Create a new game instance with a well of the given dimensions. + // Note that internally, the board has four extra rows, to hold a + // piece that has just begun to fall. + Game(int width, int height); + + ~Game(); + + // Set the game to an initial state -- empty well, one piece waiting + // on top. + void reset(); + + // Advance the game by one tick. This usually just pushes the + // currently falling piece down by one row. It can sometimes cause + // one or more rows to be filled and removed. This method returns + // three kinds of values: + // <0: game over! + // 0: business as usual. + // {1,2,3,4}: the most recent piece removed 1,2,3 or 4 rows, and + // a new piece has started to fall. + int tick(); + + // Move the currently falling piece left or right by one unit. + // Returns whether the move was successful. + bool moveLeft(); + bool moveRight(); + + // Drop the current piece to the lowest position it can legally + // occupy. Returns whether anything happened. + bool drop(); + + // Rotate the piece clockwise or counter-clockwise. Returns whether + // the rotation was successful. + bool rotateCW(); + bool rotateCCW(); + +void dropShadowPiece(); + + int getWidth() const + { + return board_width_; + } + int getHeight() const + { + return board_height_; + } + + int getLinesCleared() const + { + return linesCleared_; + } + + int getScore() const + { + return score_; + } + // Get the contents of the cell at row r and column c. Returns + // the following values: + // -1: Cell is empty. + // {0,1,2,3,4,5,6}: Cell contains a piece with the given ID. Use + // this ID to choose a colour when drawing this cell. + // NOTE! You can (and should) actually call this method with values + // for r in [0,board_height_+4), not [0,board_height_]. The top four + // rows are added on to accommodate new pieces that are falling into + // the well. + int get(int r, int c) const; + int& get(int r, int c); + +private: + bool doesPieceFit(const Piece& p, int x, int y) const; + + void removeRow(int y); + int collapse(); + + void removePiece(const Piece& p, int x, int y); + void placePiece(const Piece& p, int x, int y); + + void generateNewPiece(); + + void dropPiece(int side); + +private: + int board_width_; + int board_height_; + + bool stopped_; + + Piece piece_; + Piece shadowPiece_; + int px_, sx_; + int py_, sy_; + + int* board_; + + // Extra stuff + int score_, linesCleared_; + +}; + +#endif // CS488_GAME_HPP diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..3568e43 --- /dev/null +++ b/main.cpp @@ -0,0 +1,19 @@ +#include +#include +#include "appwindow.hpp" + +int main(int argc, char** argv) +{ + // Construct our main loop + Gtk::Main kit(argc, argv); + + // Initialize OpenGL + Gtk::GL::init(argc, argv); + + // Construct our (only) window + AppWindow window; + + // And run the application! + Gtk::Main::run(window); +} + diff --git a/viewer.cpp b/viewer.cpp new file mode 100644 index 0000000..7556d1e --- /dev/null +++ b/viewer.cpp @@ -0,0 +1,730 @@ +#include "viewer.hpp" +#include +#include +#include +#include +#include +#include "appwindow.hpp" + +#define DEFAULT_GAME_SPEED 500 +Viewer::Viewer() +{ + + // Set all rotationAngles to 0 + rotationAngleX = 0; + rotationAngleY = 0; + rotationAngleZ = 0; + rotationSpeed = 0; + + // Default draw mode is multicoloured + currentDrawMode = Viewer::MULTICOLOURED; + + // Default scale factor is 1 + scaleFactor = 1; + + // Assume no buttons are held down at start + shiftIsDown = false; + mouseB1Down = false; + mouseB2Down = false; + mouseB3Down = false; + rotateAboutX = false; + rotateAboutY = false; + rotateAboutZ = false; + + + // Game starts at a slow pace of 500ms + gameSpeed = 500; + + // By default turn double buffer on + doubleBuffer = false; + + gameOver = false; + + Glib::RefPtr glconfig; + + // Ask for an OpenGL Setup with + // - red, green and blue component colour + // - a depth buffer to avoid things overlapping wrongly + // - double-buffered rendering to avoid tearing/flickering + // - Multisample rendering to smooth out edges + glconfig = Gdk::GL::Config::create( Gdk::GL::MODE_RGB | + Gdk::GL::MODE_DEPTH | + Gdk::GL::MODE_DOUBLE | + Gdk::GL::MODE_MULTISAMPLE ); + if (glconfig == 0) { + // If we can't get this configuration, die + abort(); + } + + // Accept the configuration + set_gl_capability(glconfig); + + // Register the fact that we want to receive these events + add_events( Gdk::BUTTON1_MOTION_MASK | + Gdk::BUTTON2_MOTION_MASK | + Gdk::BUTTON3_MOTION_MASK | + Gdk::BUTTON_PRESS_MASK | + Gdk::BUTTON_RELEASE_MASK | + Gdk::KEY_PRESS_MASK | + Gdk::VISIBILITY_NOTIFY_MASK); + + // Create Game + game = new Game(10, 20); + + // Start game tick timer + tickTimer = Glib::signal_timeout().connect(sigc::mem_fun(*this, &Viewer::gameTick), gameSpeed); +} + +Viewer::~Viewer() +{ + delete(game); + // Nothing to do here right now. +} + +void Viewer::invalidate() +{ + //Force a rerender + Gtk::Allocation allocation = get_allocation(); + get_window()->invalidate_rect( allocation, false); + +} + +void Viewer::on_realize() +{ + // Do some OpenGL setup. + // First, let the base class do whatever it needs to + Gtk::GL::DrawingArea::on_realize(); + + Glib::RefPtr gldrawable = get_gl_drawable(); + + if (!gldrawable) + return; + + if (!gldrawable->gl_begin(get_gl_context())) + return; + + // Just enable depth testing and set the background colour. + glEnable(GL_DEPTH_TEST); + glClearColor(0.7, 0.7, 1.0, 0.0); + + gldrawable->gl_end(); +} + +bool Viewer::on_expose_event(GdkEventExpose* event) +{ + Glib::RefPtr gldrawable = get_gl_drawable(); + + if (!gldrawable) return false; + + if (!gldrawable->gl_begin(get_gl_context())) + return false; + + // Decide which buffer to write to + if (doubleBuffer) + glDrawBuffer(GL_BACK); + else + glDrawBuffer(GL_FRONT); + + + // Clear the screen + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + // Modify the current projection matrix so that we move the + // camera away from the origin. We'll draw the game at the + // origin, and we need to back up to see it. + + glMatrixMode(GL_PROJECTION); + glPushMatrix(); + glTranslated(0.0, 0.0, -40.0); + + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); + + // set up lighting (if necessary) + // Followed the tutorial found http://www.falloutsoftware.com/tutorials/gl/gl8.htm + // to implement lighting + + // Initialize lighting settings + glShadeModel(GL_SMOOTH); + glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); + glEnable(GL_LIGHTING); + + // Create one light source + glEnable(GL_LIGHT0); + glEnable(GL_COLOR_MATERIAL); + glColorMaterial(GL_FRONT, GL_AMBIENT_AND_DIFFUSE); + // Define properties of light + float ambientLight0[] = { 0.3f, 0.3f, 0.3f, 1.0f }; + float diffuseLight0[] = { 0.8f, 0.8f, 0.8f, 1.0f }; + float specularLight0[] = { 0.6f, 0.6f, 0.6f, 1.0f }; + float position0[] = { 5.0f, 0.0f, 0.0f, 1.0f }; + glLightfv(GL_LIGHT0, GL_AMBIENT, ambientLight0); + glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuseLight0); + glLightfv(GL_LIGHT0, GL_SPECULAR, specularLight0); + glLightfv(GL_LIGHT0, GL_POSITION, position0); + + // Scale and rotate the scene + + if (scaleFactor != 1) + glScaled(scaleFactor, scaleFactor, scaleFactor); + + if (rotationAngleX != 0) + glRotated(rotationAngleX, 1, 0, 0); + + if (rotationAngleY != 0) + glRotated(rotationAngleY, 0, 1, 0); + + if (rotationAngleZ != 0) + glRotated(rotationAngleZ, 0, 0, 1); + + // Increment rotation angles for next render + if ((mouseB1Down && !shiftIsDown) || rotateAboutX) + { + rotationAngleX += rotationSpeed; + if (rotationAngleX > 360) + rotationAngleX -= 360; + } + if ((mouseB2Down && !shiftIsDown) || rotateAboutY) + { + rotationAngleY += rotationSpeed; + if (rotationAngleY > 360) + rotationAngleY -= 360; + } + if ((mouseB3Down && !shiftIsDown) || rotateAboutZ) + { + rotationAngleZ += rotationSpeed; + if (rotationAngleZ > 360) + rotationAngleZ -= 360; + } + + // You'll be drawing unit cubes, so the game will have width + // 10 and height 24 (game = 20, stripe = 4). Let's translate + // the game so that we can draw it starting at (0,0) but have + // it appear centered in the window. + glTranslated(-5.0, -12.0, 0.0); + + + + // Draw Border + for (int y = -1;y< 20;y++) + { + drawCube(y, -1, 7, GL_LINE_LOOP); + + drawCube(y, 10, 7, GL_LINE_LOOP); + } + for (int x = 0;x < 10; x++) + { + drawCube (-1, x, 7, GL_LINE_LOOP); + } + + // Draw current state of tetris + if (currentDrawMode == Viewer::WIRE) + { + for (int i = 23;i>=0;i--) // row + { + for (int j = 9; j>=0;j--) // column + { + drawCube (i, j, game->get(i, j), GL_LINE_LOOP ); + } + } + } + else if (currentDrawMode == Viewer::MULTICOLOURED) + { + for (int i = 23;i>=0;i--) // row + { + for (int j = 9; j>=0;j--) // column + { + // Draw outline for cube + if (game->get(i, j) != -1) + drawCube(i, j, 7, GL_LINE_LOOP); + + drawCube (i, j, game->get(i, j), GL_QUADS, true ); + } + } + } + else if (currentDrawMode == Viewer::FACE) + { + for (int i = 23;i>=0;i--) // row + { + for (int j = 9; j>=0;j--) // column + { + // Draw outline for cube + if (game->get(i, j) != -1) + drawCube(i, j, 7, GL_LINE_LOOP); + + drawCube (i, j, game->get(i, j), GL_QUADS ); + } + } + } + + if (gameOver) + { + // Some game over animation + } + + // We pushed a matrix onto the PROJECTION stack earlier, we + // need to pop it. + + glMatrixMode(GL_PROJECTION); + glPopMatrix(); + + // Swap the contents of the front and back buffers so we see what we + // just drew. This should only be done if double buffering is enabled. + if (doubleBuffer) + gldrawable->swap_buffers(); + else + glFlush(); + + gldrawable->gl_end(); + + return true; +} + +bool Viewer::on_configure_event(GdkEventConfigure* event) +{ + Glib::RefPtr gldrawable = get_gl_drawable(); + + if (!gldrawable) return false; + + if (!gldrawable->gl_begin(get_gl_context())) + return false; + + // Set up perspective projection, using current size and aspect + // ratio of display + + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + glViewport(0, 0, event->width, event->height); + gluPerspective(40.0, (GLfloat)event->width/(GLfloat)event->height, 0.1, 1000.0); + + // Reset to modelview matrix mode + + glMatrixMode(GL_MODELVIEW); + + gldrawable->gl_end(); + + return true; +} + +bool Viewer::on_button_press_event(GdkEventButton* event) +{ + startPos[0] = event->x; + startPos[1] = event->y; + mouseDownPos[0] = event->x; + mouseDownPos[1] = event->y; + + // Stop rotating if a mosue button was clicked and the shift button is not down + if ((rotateAboutX || rotateAboutY || rotateAboutZ) && !shiftIsDown) + { + rotationSpeed = 0; + rotateTimer.disconnect(); + } + + // Set our appropriate flags to true + if (event->button == 1) + mouseB1Down = true; + if (event->button == 2) + mouseB2Down = true; + if (event->button == 3) + mouseB3Down = true; + + // If shift is not held down we can stop rotating + if (!shiftIsDown) + { + rotateAboutX = false; + rotateAboutY = false; + rotateAboutZ = false; + } + return true; +} + +bool Viewer::on_button_release_event(GdkEventButton* event) +{ + startScalePos[0] = 0; + startScalePos[1] = 0; + + if (!shiftIsDown) + { + // Set the rotation speed based on how far the cursor has moved since the mouse down event + long difference = (long)timeOfLastMotionEvent - (long)event->time; + if (difference > -50) + rotationSpeed = (event->x - mouseDownPos[0]) / 10; + } + + // Set the appropriate flags to true and false + if (event->button == 1) + { + mouseB1Down = false; + if (!shiftIsDown) + rotateAboutX = true; + } + if (event->button == 2) + { + mouseB2Down = false; + if (!shiftIsDown) + rotateAboutY = true; + } + + if (event->button == 3) + { + mouseB3Down = false; + if (!shiftIsDown) + rotateAboutZ = true; + } + + invalidate(); + return true; +} + +bool Viewer::on_motion_notify_event(GdkEventMotion* event) +{ + double x2x1; + if (shiftIsDown) // Start Scaling + { + // Store some initial values + if (startScalePos[0] == 0 && startScalePos[1] == 0) + { + startScalePos[0] = event->x; + startScalePos[1] = event->y; + return true; + } + + // See how much we have moved + x2x1 = (event->x - startScalePos[0]); + + // Create a scale factor based on that value + if (x2x1 != 0) + { + x2x1 /= 500; + scaleFactor += x2x1; + } + + if (scaleFactor < 0.5) + scaleFactor = 0.5; + else if (scaleFactor > 2) + scaleFactor = 2; + + startScalePos[0] = event->x; + startScalePos[1] = event->y; + if (rotateTimer.connected()) + return true; + } + else // Start rotating + { + // Keep track of the time of the last motion event + timeOfLastMotionEvent = event->time; + + // Determine change in distance. This is used to calculate the rotation angle + x2x1 = event->x - startPos[0]; + x2x1 /= 10; + + if (mouseB1Down) // Rotate x + rotationAngleX += x2x1; + if (mouseB2Down) // Rotate y + rotationAngleY += x2x1; + if (mouseB3Down) // Rotate z + rotationAngleZ += x2x1; + + // Reset the tickTimer + if (!rotateTimer.connected()) + rotateTimer = Glib::signal_timeout().connect(sigc::bind(sigc::mem_fun(*this, &Viewer::on_expose_event), (GdkEventExpose *)NULL), 100); + } + + // Store the position of the cursor + startPos[0] = event->x; + startPos[1] = event->y; + invalidate(); + return true; +} + +void Viewer::drawCube(int y, int x, int colourId, GLenum mode, bool multiColour) +{ + if (mode == GL_LINE_LOOP) + glLineWidth (2); + + double r, g, b; + r = 0; + g = 0; + b = 0; + switch (colourId) + { + case 0: // blue + r = 0.514; + g = 0.839; + b = 0.965; + break; + case 1: // purple + r = 0.553; + g = 0.6; + b = 0.796; + break; + case 2: // orange + r = 0.988; + g = 0.627; + b = 0.373; + break; + case 3: // green + r = 0.69; + g = 0.835; + b = 0.529; + break; + case 4: // red + r = 1.00; + g = 0.453; + b = 0.339; + break; + case 5: // pink + r = 0.949; + g = 0.388; + b = 0.639; + break; + case 6: // yellow + r = 1; + g = 0.792; + b = 0.204; + break; + case 7: // black + r = 0; + g = r; + b = g; + break; + default: + return; + } + + double innerXMin = 0; + double innerYMin = 0; + double innerXMax = 1; + double innerYMax = 1; + double zMax = 1; + double zMin = 0; + + // Front face + glNormal3d(1, 0, 0); + + glColor3d(r, g, b); + glBegin(mode); + glVertex3d(innerXMin + x, innerYMin + y, zMax); + glVertex3d(innerXMax + x, innerYMin + y, zMax); + glVertex3d(innerXMax + x, innerYMax + y, zMax); + glVertex3d(innerXMin + x, innerYMax + y, zMax); + glEnd(); + + // top face + glNormal3d(0, 1, 0); + + if (multiColour) + glColor3d(g, r, b); + + glBegin(mode); + glVertex3d(innerXMin + x, innerYMax + y, zMin); + glVertex3d(innerXMax + x, innerYMax + y, zMin); + glVertex3d(innerXMax + x, innerYMax + y, zMax); + glVertex3d(innerXMin + x, innerYMax + y, zMax); + glEnd(); + + // left face + glNormal3d(0, 0, 1); + + if (multiColour) + glColor3d(b, g, r); + + glBegin(mode); + glVertex3d(innerXMin + x, innerYMin + y, zMin); + glVertex3d(innerXMin + x, innerYMax + y, zMin); + glVertex3d(innerXMin + x, innerYMax + y, zMax); + glVertex3d(innerXMin + x, innerYMin + y, zMax); + glEnd(); + + // bottom face + glNormal3d(0, 1, 0); + + if (multiColour) + glColor3d(r, b, g); + + glBegin(mode); + glVertex3d(innerXMin + x, innerYMin + y, zMin); + glVertex3d(innerXMax + x, innerYMin + y, zMin); + glVertex3d(innerXMax + x, innerYMin + y, zMax); + glVertex3d(innerXMin + x, innerYMin + y, zMax); + glEnd(); + + // right face + glNormal3d(0, 0, 1); + + if (multiColour) + glColor3d(b, r, g); + + glBegin(mode); + glVertex3d(innerXMax + x, innerYMin + y, zMin); + glVertex3d(innerXMax + x, innerYMax + y, zMin); + glVertex3d(innerXMax + x, innerYMax + y, zMax); + glVertex3d(innerXMax + x, innerYMin + y, zMax); + glEnd(); + + // Back of front face + glNormal3d(1, 0, 0); + + if (multiColour) + glColor3d(g, b, r); + + glBegin(mode); + glVertex3d(innerXMin + x, innerYMin + y, zMin); + glVertex3d(innerXMax + x, innerYMin + y, zMin); + glVertex3d(innerXMax + x, innerYMax + y, zMin); + glVertex3d(innerXMin + x, innerYMax + y, zMin); + glEnd(); +} + +void Viewer::startScale() +{ + shiftIsDown = true; +} + +void Viewer::endScale() +{ + shiftIsDown = false; + startScalePos[0] = 0; + startScalePos[1] = 0; +} + +void Viewer::setSpeed(Speed newSpeed) +{ + // Keep track of the current speed menu value + speed = newSpeed; + switch (newSpeed) + { + case SLOW: + gameSpeed = 500; + break; + case MEDIUM: + gameSpeed = 250; + break; + case FAST: + gameSpeed = 100; + break; + } + + // Update game tick timer + tickTimer.disconnect(); + tickTimer = Glib::signal_timeout().connect(sigc::mem_fun(*this, &Viewer::gameTick), gameSpeed); +} + +void Viewer::toggleBuffer() +{ + // Toggle the value of doubleBuffer + doubleBuffer = !doubleBuffer; + invalidate(); +} + +void Viewer::setDrawMode(DrawMode mode) +{ + currentDrawMode = mode; + invalidate(); +} + +bool Viewer::on_key_press_event( GdkEventKey *ev ) +{ + // Don't process movement keys if its game over + if (gameOver) + return true; + + if (ev->keyval == GDK_Left) + game->moveLeft(); + else if (ev->keyval == GDK_Right) + game->moveRight(); + else if (ev->keyval == GDK_Up) + game->rotateCCW(); + else if (ev->keyval == GDK_Down) + game->rotateCW(); + else if (ev->keyval == GDK_space) + game->drop(); + + invalidate(); + return true; +} + +bool Viewer::gameTick() +{ + int returnVal = game->tick(); + + // String streams used to print score and lines cleared + std::stringstream scoreStream, linesStream; + std::string s; + + // Update the score + scoreStream << game->getScore(); + scoreLabel->set_text("Score:\t" + scoreStream.str()); + + // If a line was cleared update the linesCleared widget + if (returnVal > 0) + { + linesStream << game->getLinesCleared(); + linesClearedLabel->set_text("Lines Cleared:\t" + linesStream.str()); + } + + if (game->getLinesCleared() / 10 > (DEFAULT_GAME_SPEED - gameSpeed) / 50 && gameSpeed > 75) + { + // Increase the game speed + gameSpeed -= 50; + + // Update game tick timer + tickTimer.disconnect(); + tickTimer = Glib::signal_timeout().connect(sigc::mem_fun(*this, &Viewer::gameTick), gameSpeed); + } + + if (returnVal < 0) + { + gameOver = true; + tickTimer.disconnect(); + } + + invalidate(); + return true; +} + +void Viewer::resetView() +{ + // Reset all the rotations and scale factor + rotationSpeed = 0; + rotationAngleX = 0; + rotationAngleY = 0; + rotationAngleZ = 0; + rotateAboutX = false; + rotateAboutY = false; + rotateAboutZ = false; + rotateTimer.disconnect(); + + scaleFactor = 1; + invalidate(); +} + +void Viewer::newGame() +{ + gameOver = false; + game->reset(); + + // Restore gamespeed to whatever was set in the menu + setSpeed(speed); + + // Reset the tickTimer + tickTimer.disconnect(); + tickTimer = Glib::signal_timeout().connect(sigc::mem_fun(*this, &Viewer::gameTick), gameSpeed); + + std::stringstream scoreStream, linesStream; + std::string s; + + // Update the score + scoreStream << game->getScore(); + scoreLabel->set_text("Score:\t" + scoreStream.str()); + linesStream << game->getLinesCleared(); + linesClearedLabel->set_text("Lines Cleared:\t" + linesStream.str()); + + invalidate(); +} + +void Viewer::setScoreWidgets(Gtk::Label *score, Gtk::Label *linesCleared) +{ + scoreLabel = score; + linesClearedLabel = linesCleared; +} diff --git a/viewer.hpp b/viewer.hpp new file mode 100644 index 0000000..48ae786 --- /dev/null +++ b/viewer.hpp @@ -0,0 +1,139 @@ +#ifndef CS488_VIEWER_HPP +#define CS488_VIEWER_HPP + +#include "algebra.hpp" +#include +#include +#include "game.hpp" + +// The "main" OpenGL widget +class Viewer : public Gtk::GL::DrawingArea { +public: + Viewer(); + virtual ~Viewer(); + enum DrawMode { + WIRE, + FACE, + MULTICOLOURED + }; + + enum Speed { + SLOW, + MEDIUM, + FAST + }; + + // A useful function that forces this widget to rerender. If you + // want to render a new frame, do not call on_expose_event + // directly. Instead call this, which will cause an on_expose_event + // call when the time is right. + void invalidate(); + void setDrawMode(DrawMode newDrawMode); + + void setRotationAngle (int angle); + int getRotationAngle(); + + void startScale(); + void endScale(); + + void setSpeed(Speed newSpeed); + + void toggleBuffer(); + + bool gameTick(); + + virtual bool on_key_press_event( GdkEventKey *ev ); + + void resetView(); + void newGame(); + + void makeRasterFont(); + void printString(const char *s); + + void setScoreWidgets(Gtk::Label *score, Gtk::Label *linesCleared); + +protected: + + // Events we implement + // Note that we could use gtkmm's "signals and slots" mechanism + // instead, but for many classes there's a convenient member + // function one just needs to define that'll be called with the + // event. + + // Called when GL is first initialized + virtual void on_realize(); + // Called when our window needs to be redrawn + virtual bool on_expose_event(GdkEventExpose* event); + // Called when the window is resized + virtual bool on_configure_event(GdkEventConfigure* event); + // Called when a mouse button is pressed + virtual bool on_button_press_event(GdkEventButton* event); + // Called when a mouse button is released + virtual bool on_button_release_event(GdkEventButton* event); + // Called when the mouse moves + virtual bool on_motion_notify_event(GdkEventMotion* event); + + + +private: + + void drawCube(int y, int x, int colourId, GLenum mode, bool multiColour = false); + + DrawMode currentDrawMode; + + // The angle at which we are currently rotated + double rotationAngleX, rotationAngleY, rotationAngleZ; + + // How fast we are rotating the window + double rotationSpeed; + + // Flags to denote which mouse buttons are down + bool mouseB1Down, mouseB2Down, mouseB3Down; + + // Flags to denote which axis to rotate around after mouse up event + bool rotateAboutX, rotateAboutY, rotateAboutZ; + + // What factor we are scaling the game by currently + double scaleFactor; + + + Point2D startPos, mouseDownPos, startScalePos, endScalePos; + + // Flag used to denote that the shift key is held down + bool shiftIsDown; + + // Flag that determines when to use doubleBuffer + bool doubleBuffer; + + // Timer used to call the tick method + sigc::connection tickTimer; + + // Timer for rotate + sigc::connection rotateTimer; + + // Timer for persistant rotations + guint32 timeOfLastMotionEvent; + + // Used to keep track of when to increase game speed + int linesLeftTillNextLevel; + + // The number of milliseconds before the next game tick + int gameSpeed; + + // Speed value set by menu + Speed speed; + + // Pointer to the actual game + Game *game; + + // Game over flag + bool gameOver; + + // Lighting flag + bool lightingFlag; + + // Label widgets + Gtk::Label *scoreLabel, *linesClearedLabel; +}; + +#endif