#include <vectrex.h> #define PREFER_ACCURATE 1 /* don't define, to avoid longs and take some shortcuts in the arithmetic */ #define SLOWMO 1 /* change to 20 to slow down rotation for debugging */ /* * The graphics here are an approximation to the missing game Cube Quest. It was * not one of the original GCE games but did come from around that time. The * developer, Paul Allen Newell <pnewell@cs.cmu.edu>, prototyped a game for a * laserdisc system using the Vectrex, but the Vectrex version was far from * complete and not really very playable. * Cube Quest was later realized in 1983/84 as a hybrid vector/raster arcade * game by Simutrek, but the author was not entirely happy with it, as it was * rushed out before it had been fully developed. * There are videos of the Vectrex version of Cube Quest such as: * https://www.youtube.com/watch?v=n8vRh4XAsDg * From the latter video it's clear that the intention was to make it a multigame, * with each movement across the cube being permitted only if you complete * another mission, with the subgames being like Black Widow, Terror Tubes * etc - in fact also looking a lot like some of ClockworkRobot's smaller games. * Several Youtube videos of the laser version of Cube Quest are available, such as: * https://www.youtube.com/watch?v=nNGBFEeEvKM and https://www.youtube.com/watch?v=xsfq2PXTxSA * Also worth a read: * http://www.ataricompendium.com/archives/interviews/paul_allen_newell/interview_paul_allen_newell.html * This rewrite is currently very much a Work-In-Progress (WIP) - there is no game logic yet. * As discussed on the Facebook Vectrex forum, it would be nice to finish this rewrite to be * compatible with the Vectrex Cube Quest (which was partially released at a retro gaming show, * but has never been dumped and released to the public), and then to go on and add new original * minigames in the spirit of the arcade Cube Quest. * This must be compiled in the Vide environment using gcc's -O option in order to acheive acceptable * frame rates. * The first version of this code used pre-computed lookup tables for the animation, but * after some experimentation, it was found that the benefit in speed did not justify the * added complexity or amount of RAM used. Table-driving the animations may still be an * option but if we go that route it needs to be done differently from before. * */ #define MAX_BRIGHTNESS 0x7fU #define BRIGHT 0x7fU #define NORMAL 0x3fU #define DIM 0x28U // adjusted for my vectrex, not for the emulator #define MIN_SCALE 0x0fU #define PREFERRED_SCALE 0x78U #define MAX_SCALE 0xf0U #define SCREEN_X_ORIGIN (-80) /* where to place the lower left corner of the cube */ static int beam_x = 0, beam_y = 0; static long int target_x = 0L, target_y = 0L; static int vectors_drawn = 0; static unsigned int Old_Intensity = 0; // wrap move and draw procedures to allow automatic insertion of Reset0Ref // in order to keep image stable. static inline void Move(int y, int x) { target_x += (long)x; target_y += (long)y; } static void Line(long SuppressMask, long BelongsIn, unsigned int Intensity, int y, int x) { // If the line belongs in a particular plane, and the suppression mask is set for // that plane, then the line is not drawn. Planes are defined perpendicular to // the given axis. long int dx, dy; if (Old_Intensity != Intensity) { Intensity_a(Old_Intensity = Intensity); // repeated procedure call overhead is worth avoiding } if ((beam_x != target_x) || (beam_y != target_y)) { if (vectors_drawn >= 2) {// 150 cycles saved per increment. optimum appearance is 2 Reset0Ref(); beam_x = 0; beam_y = 0; vectors_drawn = 0; } dy = target_y-(long)beam_y; dx = target_x-(long)beam_x; if (dx > 127L || dx < -128L || dy > 127L || dy < -128L) { // Too big for a single move. So halve the distance and move twice. Moveto_d((int)(dy>>1), (int)(dx>>1)); dx -= dx>>1; dy -= dy>>1; // bring the move back into range } Moveto_d((int)dy, (int)dx); beam_x = (int)target_x; beam_y = (int)target_y; } if ((SuppressMask&BelongsIn) == 0) Draw_Line_d(y, x); else Moveto_d(y, x); // might be able to optimise this by calling Move() vectors_drawn += 1; beam_x = (int)(target_x)+x; beam_y = (int)target_y+y; target_x = (long)beam_x; target_y = (long)beam_y; } // The rot sets in... #define ROTATION_STEPS 32 int rotation = (0==0); // This is a dimetric projection. If we want a trimetric projection, // to copy the slight tilt of GCE's Cube Quest, then we'll need a Z parameter // as well; but for now this is good enough... #ifdef PREFER_ACCURATE #define ONE 16384 typedef long POINT; typedef long long WPOINT; /* Wide point */ #define S 0 const long sine[ROTATION_STEPS+1] = { #define fpsin(theta) sine[theta] #define fpcos(theta) sine[(int)ROTATION_STEPS-theta] #define DIV 14 /* sin/cos now in range 0..16k-1 */ #else #define ONE 16383 typedef int POINT; // in case we need to switch to long ... typedef long WPOINT; #define S 7 const int sine[ROTATION_STEPS+1] = { #define fpsin(theta) sine[theta] #define fpcos(theta) sine[(int)ROTATION_STEPS-theta] #define DIV 7 /* sin/cos now in range 0..63 */ #endif 0>>S, 803>>S, 1605>>S, 2404>>S, 3196>>S, 3980>>S, 4756>>S, 5519>>S, 6269>>S, 7005>>S, 7723>>S, 8423>>S, 9102>>S, 9759>>S, 10393>>S, 11002>>S, 11585>>S, 12139>>S, 12665>>S, 13159>>S, 13622>>S, 14053>>S, 14449>>S, 14810>>S, 15136>>S, 15426>>S, 15678>>S, 15892>>S, 16069>>S, 16206>>S, 16305>>S, 16364>>S, ONE>>S /* scaled 0:1 -> 0:16384 */ }; // because of the way that the sin table was created, it's possible that the calculated cos // value is off by 1/64th of 90 degrees! A mismatch between sin and cos for the same angle // could explain the 'wobble' seen during rotations. Need to re-calculate this table... static inline void map2dYX_to_dimetric(POINT x, POINT y, POINT *screeny, POINT *screenx) { //for every 20 x added, X' increases by 50. //for every 20 y added, X' increases by 30 //for every 20 y added, Y' increases by 20 //for every 20 x added, Y' increases by -10 // X' = x*50/20 + y*30/20 // Y' = y*20/20 - x*10/20 // X' = x*5/2 + y*3/2 // Y' = y - x/2 POINT tmp = x>>1L; // accept 1 pt of rounding error for now... #ifdef PREFER_ACCURATE *screenx = (int)(((long)x*5L)>>1L) + y + (y>>1); *screeny = y - tmp; #else *screenx = tmp*5 + y + (y>>1); *screeny = y - tmp; #endif } // generated similarly... static inline void map2dZY_to_dimetric(POINT z, POINT y, POINT *screeny, POINT *screenx) { *screenx = (y>>1)+y; *screeny = (z<<1)+y; } static inline void map2dZX_to_dimetric(POINT z, POINT x, POINT *screeny, POINT *screenx) { *screeny = (z<<1) - (x>>1); //*screenx = (x*5)>>1; #ifdef PREFER_ACCURATE *screenx = (int)(((long)x*5L)>>1L); #else *screenx = (x<<1) + (x>>1); //I think that's right. Untested so far. #endif } #define BASE_Y (0) #define BASE_X (0) #define FORTY_Y (40) #define SIXTY_X (60) #define MINUSTWENTY_Y (-20) #define HUNDRED_X (100) #define X0 1L #define X1 2L #define X2 4L #define Y0 8L #define Y1 16L #define Y2 32L #define Z0 64L #define Z1 128L #define Z2 256L static inline long Suppress(int axis, int plane) { int planebit = 1<<plane; // group of 3 bits: 210 return (long)planebit << ((long)(axis-'X')*3L); // 3 groups: ZYX } static void draw_cube(long SuppressMask) { // The suppress mask tells the cube drawing code to not draw specific planes, // It defines an axis and a plane within that axis. Every line-drawing call // in the cube drawing procedure checks to see if it is disabled, which it // will be if the corresponding bit is set. A line may belong to multiple // planes and faces, so it will act on the OR of three separate tests, which // it can do efficiently with a single bitmask. // The face rotation will now be done separately from the cube drawing. // Calling 'draw cube' with Suppress == 0 will draw the entire cube. // X Y Z // Suppress mask: 111 111 111 // 012 012 012 int screen_x_origin = SCREEN_X_ORIGIN; int z = 0; // precompute commonly used values for speed... // although doing these on each plane is a bit wasteful! Could be done // once and stored in tables for each rotation angle... #define Hidden DIM #define Visible NORMAL // I know. At one point these were variables. I'll fix it uo soon... #define forty_y FORTY_Y #define sixty_x SIXTY_X #define minustwenty_y MINUSTWENTY_Y #define hundred_x HUNDRED_X #define minusforty_y (-forty_y) #define minussixty_x (-sixty_x) #define twenty_y (-minustwenty_y) #define minushundred_x (-hundred_x) #define half_of_forty_y (forty_y>>1) #define half_of_sixty_x (sixty_x>>1) #define half_of_minustwenty_y (minustwenty_y>>1) #define half_of_hundred_x (hundred_x>>1) #define half_of_minusforty_y (minusforty_y>>1) #define half_of_minussixty_x (minussixty_x>>1) #define half_of_twenty_y (twenty_y>>1) #define half_of_minushundred_x (minushundred_x>>1) /////////////////////////////////////////////////////////////////// Z = 0 z = 0; Reset0Ref(); target_x = beam_x = 0; target_y = beam_y = 0; vectors_drawn = 0; Move(40,screen_x_origin); Move(BASE_Y, BASE_X); // Key: 'Z0' means the plane that can rotate around the Z axis, i.e. the X/Y plane, level 0 (top) Line(SuppressMask, X0|Z0, Visible, forty_y, sixty_x); // horizontal front left corner to rear left corner Line(SuppressMask, Y2|Z0, Visible, minustwenty_y, hundred_x); // horizontal rear left corner to rear right corner Move(half_of_twenty_y, half_of_minushundred_x); Line(SuppressMask, X1|Z0, Visible, minusforty_y, minussixty_x); Move(forty_y,sixty_x); // back to front horizontal cross-member Move(half_of_minustwenty_y, half_of_hundred_x); Line(SuppressMask, X2|Z0, Visible, minusforty_y, minussixty_x); // horizontal rear right corner to front right corner Move(half_of_forty_y, half_of_sixty_x); Line(SuppressMask, Y1|Z0, Visible, twenty_y, minushundred_x); Move(minustwenty_y,hundred_x); //right to left cross-member Move(half_of_minusforty_y, half_of_minussixty_x); Line(SuppressMask, Y0|Z0, Visible, twenty_y, minushundred_x); // horizontal front (right to left) Move(minustwenty_y, hundred_x); /////////////////////////////////////////////////////////////////// Z = 1 z = 1; Reset0Ref(); target_x = beam_x = 0; target_y = beam_y = 0; vectors_drawn = 0; Move(0,screen_x_origin); Move(BASE_Y, BASE_X); Line(SuppressMask, X0|Z1, Hidden, forty_y, sixty_x); // horizontal front left corner to rear left corner Line(SuppressMask, Y2|Z1, Hidden, minustwenty_y, hundred_x); // horizontal rear left corner to rear right corner Move(half_of_twenty_y, half_of_minushundred_x); Line(SuppressMask, X1|Z1, Hidden, minusforty_y, minussixty_x); Move(forty_y,sixty_x); // back to front horizontal cross-member Move(half_of_minustwenty_y, half_of_hundred_x); Line(SuppressMask, X2|Z1, Visible, minusforty_y, minussixty_x); // horizontal rear right corner to front right corner Move(half_of_forty_y, half_of_sixty_x); Line(SuppressMask, Y1|Z1, Hidden, twenty_y, minushundred_x); Move(minustwenty_y,hundred_x); //right to left cross-member Move(half_of_minusforty_y, half_of_minussixty_x); Line(SuppressMask, Y0|Z1, Visible, twenty_y, minushundred_x); // horizontal front (right to left) Move(minustwenty_y, hundred_x); /////////////////////////////////////////////////////////////////// Z = 2 z = 2; Reset0Ref(); target_x = beam_x = 0; target_y = beam_y = 0; vectors_drawn = 0; Move(-40,screen_x_origin); Move(BASE_Y, BASE_X); { // VERTICALS Line(SuppressMask, X0|Y0, Visible, 80, 0); Move(-80, 0); // Leftmost pillar } Line(SuppressMask, X0|Z2, Hidden, forty_y, sixty_x); // horizontal front left corner to rear left corner { // VERTICALS Line(SuppressMask, X0|Y2, Hidden, 80, 0); Move(-80, 0); // back-left corner vertical Move(-20, -30); Line(SuppressMask, X0|Y1, Hidden, 80, 0); Move(-80, 0); // hidden left face center vertical Move(20, 30); } Line(SuppressMask, Y2|Z2, Hidden, minustwenty_y, hundred_x); // horizontal rear left corner to rear right corner Move(half_of_twenty_y, half_of_minushundred_x); Line(SuppressMask, X1|Z2, Hidden, minusforty_y, minussixty_x); Move(forty_y,sixty_x); // back to front horizontal cross-member { // VERTICALS Line(SuppressMask, X1|Y2, Hidden, 80, 0); Move(-80, 0); // rear face center vertical Move(-20,-30); Line(SuppressMask, X1|Y1, Hidden, 80, 0); Move(-80, 0); Move(20,30); // center vertical } Move(half_of_minustwenty_y, half_of_hundred_x); { // VERTICALS Line(SuppressMask, Y2|X2, Visible, 80, 0); Move(-80, 0); // back right corner vertical } Line(SuppressMask, X2|Z2, Visible, minusforty_y, minussixty_x); // horizontal rear right corner to front right corner Move(half_of_forty_y, half_of_sixty_x); Line(SuppressMask, Y1|Z2, Hidden, twenty_y, minushundred_x); Move(minustwenty_y,hundred_x); //right to left cross-member if (z == 2) {Line(SuppressMask, X2|Y1, Visible, 80, 0); Move(-80, 0);} // right face center vertical Move(half_of_minusforty_y, half_of_minussixty_x); if (z == 2) {Line(SuppressMask, X2|Y0, Visible, 80, 0); Move(-80, 0);} // front right corner vertical Line(SuppressMask, Y0|Z2, Visible, twenty_y, minushundred_x); // horizontal front (right to left) Move(half_of_minustwenty_y, half_of_hundred_x); if (z == 2) {Line(SuppressMask, X1|Y0, Visible, 80, 0); Move(-80, 0);} // front center vertical Move(half_of_minustwenty_y, half_of_hundred_x); /////////////////////////////////////////////////////////////////////////////// end } static int delay=0; static int theta = 0; static int turn_direction = 1; void draw_rotating_plane(int axis, int plane) { int screen_x_origin = SCREEN_X_ORIGIN; int X, Y, X_ll = -20, Y_ll = -20, X_ul = -20, Y_ul = 20, X_ur = 20, Y_ur = 20; POINT p0x,p0y,p1x,p1y,p2x,p2y; Reset0Ref(); target_x = beam_x = 0; target_y = beam_y = 0; vectors_drawn = 0; if (axis == 'X') { Move(plane*-10-40, screen_x_origin+plane*50); } else if (axis == 'Y') { Move(-40+plane*20, screen_x_origin+plane*30); } else { Move(40-plane*40, -80); } // calculating this on the fly is a bit more expensive than the pre-computed version // (42K cycles per frame) but I'm tempted to live with it. It only reduces the frame // rate below optimum during a short animation sequence which in the game will not run // very often... // (and if you cheat by doing the rotation only during the scaling-up, it always // keeps to around 30K cycles per frame - if necessary, design that into the game...) // (X_ll,Y_ll) -> X = (int)((((WPOINT)X_ll * (WPOINT)fpcos(theta)) >> DIV) - (((WPOINT)Y_ll * (WPOINT)fpsin(theta)) >> DIV)); Y = (int)((((WPOINT)Y_ll * (WPOINT)fpcos(theta)) >> DIV) + (((WPOINT)X_ll * (WPOINT)fpsin(theta)) >> DIV)); X += 20; Y += 20; // and map to dimetric projection... if (axis == 'X') { map2dZY_to_dimetric(Y,X, &p0y,&p0x); } else if (axis == 'Y') { map2dZX_to_dimetric(Y,X, &p0y,&p0x); } else { map2dYX_to_dimetric(Y,X, &p0y,&p0x); } // (X_ul,Y_ul) -> X = (int)((((WPOINT)X_ul * (WPOINT)fpcos(theta)) >> DIV) - (((WPOINT)Y_ul * (WPOINT)fpsin(theta)) >> DIV)); Y = (int)((((WPOINT)Y_ul * (WPOINT)fpcos(theta)) >> DIV) + (((WPOINT)X_ul * (WPOINT)fpsin(theta)) >> DIV)); X += 20; Y += 20; if (axis == 'X') { map2dZY_to_dimetric(Y,X, &p1y,&p1x); } else if (axis == 'Y') { map2dZX_to_dimetric(Y,X, &p1y,&p1x); } else { map2dYX_to_dimetric(Y,X, &p1y,&p1x); } // (X_ur,Y_ur) -> X = (int)((((WPOINT)X_ur * (WPOINT)fpcos(theta)) >> DIV) - (((WPOINT)Y_ur * (WPOINT)fpsin(theta)) >> DIV)); Y = (int)((((WPOINT)Y_ur * (WPOINT)fpcos(theta)) >> DIV) + (((WPOINT)X_ur * (WPOINT)fpsin(theta)) >> DIV)); X += 20; Y += 20; if (axis == 'X') { map2dZY_to_dimetric(Y,X, &p2y,&p2x); } else if (axis == 'Y') { map2dZX_to_dimetric(Y,X, &p2y,&p2x); } else { map2dYX_to_dimetric(Y,X, &p2y,&p2x); } Move((int)p0y, (int)p0x); Line(0, 0, BRIGHT, (int)(p1y-p0y), (int)(p1x-p0x)); // p0 to p1 Move((int)(p0y-p1y)>>1, (int)(p0x-p1x)>>1); Line(0, 0, BRIGHT, (int)(p2y-p1y), (int)(p2x-p1x)); Move((int)(p1y-p2y), (int)(p1x-p2x)); Move((int)(p1y-p0y)>>1, (int)(p1x-p0x)>>1); Line(0, 0, BRIGHT, (int)(p2y-p1y), (int)(p2x-p1x)); // p1 to p2 Move((int)(p1y-p2y)>>1, (int)(p1x-p2x)>>1); Line(0, 0, BRIGHT, (int)(p0y-p1y), (int)(p0x-p1x)); Move((int)(p1y-p0y), (int)(p1x-p0x)); Move((int)(p2y-p1y)>>1, (int)(p2x-p1x)>>1); Line(0, 0, BRIGHT, (int)(p0y-p1y), (int)(p0x-p1x)); // p3 (-p1) to p0 Line(0, 0, BRIGHT, (int)(p1y-p2y), (int)(p1x-p2x)); // p2 to p3 (-p1) delay = delay+1; if (delay == SLOWMO) { // set delay to something like 20 to see the details of the rotation wobble theta = (theta + turn_direction) & 31; if (theta == 0) { rotation = (0 != 0); // tell the level above that we've finished a 90 degree rotation turn_direction = 0 - turn_direction; } delay = 0; } } int main(void) { static long countdown = 0L; static char *message = "X AXIS PLANE 0\x80"; int demo_axis = 'Z', demo_plane = 0; int rotate_level; unsigned int scale = MIN_SCALE; int delta_scale = 2; // Precompute the corners of the square under rotation and fill the table with the delta-moves. // from one corner to the next. rotate_level = 0; rotation = (0==0); theta = 0; for (;;) { Wait_Recal(); Intensity_a(Old_Intensity = BRIGHT); /* set some brightness */ message[0] = (char)demo_axis; message[14] = (char)demo_plane + '0'; Print_Str_d(-80, -84, message); VIA_t1_cnt_lo = scale; /* set scale factor */ if (rotation) { draw_cube(Suppress(demo_axis, demo_plane)); draw_rotating_plane(demo_axis, demo_plane); } else { draw_cube(0); // 0 means no suppression } scale = scale + (unsigned int)delta_scale; if (scale >= PREFERRED_SCALE) { if (countdown < 0L) { //delta_scale = 0-delta_scale; // for zooming in and zooming out... rotate_level = ((rotate_level + 1) % 3) - 10; // 0 1 2 delta_scale = 0; rotation = (0==0); countdown = 96L; } } else if (scale <= MIN_SCALE) { delta_scale = 0-delta_scale; } countdown -= 1L; if (countdown == 0L) { // this is just a trivial animation until the game logic is added... rotate_level += 10; delta_scale = 1; scale = MIN_SCALE; rotation = (0==0); demo_plane = (demo_plane + 1) % 3; if (demo_plane == 0) demo_axis = ((demo_axis-'X' + 1) % 3) + 'X'; // 3 for x y z } }; return 0; }