You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
577 lines
20 KiB
C
577 lines
20 KiB
C
//------------------------------------------------------------------------------
|
|
// Take flight
|
|
//------------------------------------------------------------------------------
|
|
|
|
#define SOKOL_IMPL
|
|
#define SOKOL_D3D11
|
|
#include "sokol_gfx.h"
|
|
#include "sokol_gp.h"
|
|
#include "sokol_app.h"
|
|
#include "sokol_glue.h"
|
|
#include "sokol_time.h"
|
|
#include <enet/enet.h>
|
|
#include <process.h> // starting server thread
|
|
|
|
#include "types.h"
|
|
|
|
#include <inttypes.h>
|
|
|
|
static struct GameState gs = {0};
|
|
static int myplayer = -1;
|
|
static bool right_mouse_down = false;
|
|
static bool keydown[SAPP_KEYCODE_MENU] = {0};
|
|
typedef struct KeyPressed
|
|
{
|
|
bool pressed;
|
|
uint64_t frame;
|
|
} KeyPressed;
|
|
static KeyPressed keypressed[SAPP_KEYCODE_MENU] = {0};
|
|
static V2 mouse_pos = {0};
|
|
static bool mouse_pressed = false;
|
|
static uint64_t mouse_pressed_frame = 0;
|
|
static bool mouse_frozen = false; // @BeforeShip make this debug only thing
|
|
static float funval = 0.0f; // easy to play with value controlled by left mouse button when held down @BeforeShip remove on release builds
|
|
static struct ClientToServer client_to_server = {0}; // buffer of inputs
|
|
static ENetHost *client;
|
|
static ENetPeer *peer;
|
|
static float zoom_target = 300.0f;
|
|
static float zoom = 300.0f;
|
|
|
|
static void init(void)
|
|
{
|
|
// @BeforeShip make all fprintf into logging to file, warning dialog grids on failure instead of exit(-1), replace the macros in sokol with this as well, like assert
|
|
|
|
initialize(&gs);
|
|
|
|
sg_desc sgdesc = {.context = sapp_sgcontext()};
|
|
sg_setup(&sgdesc);
|
|
if (!sg_isvalid())
|
|
{
|
|
fprintf(stderr, "Failed to create Sokol GFX context!\n");
|
|
exit(-1);
|
|
}
|
|
|
|
sgp_desc sgpdesc = {0};
|
|
sgp_setup(&sgpdesc);
|
|
if (!sgp_is_valid())
|
|
{
|
|
fprintf(stderr, "Failed to create Sokol GP context: %s\n", sgp_get_error_message(sgp_get_last_error()));
|
|
exit(-1);
|
|
}
|
|
|
|
// socket initialization
|
|
{
|
|
if (enet_initialize() != 0)
|
|
{
|
|
fprintf(stderr, "An error occurred while initializing ENet.\n");
|
|
exit(-1);
|
|
}
|
|
client = enet_host_create(NULL /* create a client host */,
|
|
1 /* only allow 1 outgoing connection */,
|
|
2 /* allow up 2 channels to be used, 0 and 1 */,
|
|
0 /* assume any amount of incoming bandwidth */,
|
|
0 /* assume any amount of outgoing bandwidth */);
|
|
if (client == NULL)
|
|
{
|
|
fprintf(stderr,
|
|
"An error occurred while trying to create an ENet client host.\n");
|
|
exit(-1);
|
|
}
|
|
ENetAddress address;
|
|
ENetEvent event;
|
|
|
|
enet_address_set_host(&address, SERVER_ADDRESS);
|
|
address.port = SERVER_PORT;
|
|
peer = enet_host_connect(client, &address, 2, 0);
|
|
if (peer == NULL)
|
|
{
|
|
fprintf(stderr,
|
|
"No available peers for initiating an ENet connection.\n");
|
|
exit(-1);
|
|
}
|
|
// the timeout is the third parameter here
|
|
if (enet_host_service(client, &event, 5000) > 0 &&
|
|
event.type == ENET_EVENT_TYPE_CONNECT)
|
|
{
|
|
Log("Connected\n");
|
|
}
|
|
else
|
|
{
|
|
/* Either the 5 seconds are up or a disconnect event was */
|
|
/* received. Reset the peer in the event the 5 seconds */
|
|
/* had run out without any significant event. */
|
|
enet_peer_reset(peer);
|
|
fprintf(stderr, "Connection to server failed.");
|
|
exit(-1);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void drawbox(V2 gridpos, float rot, V2 bpos, float damage, bool offset_from_grid)
|
|
{
|
|
float halfbox = BOX_SIZE / 2.0f;
|
|
sgp_push_transform();
|
|
if (offset_from_grid)
|
|
{
|
|
sgp_rotate_at(rot, gridpos.x, gridpos.y);
|
|
}
|
|
else
|
|
{
|
|
sgp_rotate_at(rot, bpos.x, bpos.y);
|
|
}
|
|
sgp_draw_line(bpos.x - halfbox, bpos.y - halfbox, bpos.x - halfbox, bpos.y + halfbox); // left
|
|
sgp_draw_line(bpos.x - halfbox, bpos.y - halfbox, bpos.x + halfbox, bpos.y - halfbox); // top
|
|
sgp_draw_line(bpos.x + halfbox, bpos.y - halfbox, bpos.x + halfbox, bpos.y + halfbox); // right
|
|
sgp_draw_line(bpos.x - halfbox, bpos.y + halfbox, bpos.x + halfbox, bpos.y + halfbox); // bottom
|
|
sgp_draw_line(bpos.x - halfbox, bpos.y - halfbox, bpos.x + halfbox, bpos.y + halfbox); // diagonal
|
|
|
|
if (damage > 0.0f)
|
|
{
|
|
sgp_set_color(0.5f, 0.1f, 0.1f, damage);
|
|
sgp_draw_filled_rect(bpos.x - halfbox, bpos.y - halfbox, BOX_SIZE, BOX_SIZE);
|
|
}
|
|
|
|
sgp_pop_transform();
|
|
}
|
|
|
|
static void draw_circle(V2 point, float radius)
|
|
{
|
|
#define POINTS 64
|
|
sgp_line lines[POINTS];
|
|
for (int i = 0; i < POINTS; i++)
|
|
{
|
|
float progress = (float)i / (float)POINTS;
|
|
float next_progress = (float)(i + 1) / (float)POINTS;
|
|
lines[i].a = (V2){.x = cos(progress * 2.0f * PI) * radius, .y = sin(progress * 2.0f * PI) * radius};
|
|
lines[i].b = (V2){.x = cos(next_progress * 2.0f * PI) * radius, .y = sin(next_progress * 2.0f * PI) * radius};
|
|
lines[i].a = V2add(lines[i].a, point);
|
|
lines[i].b = V2add(lines[i].b, point);
|
|
}
|
|
sgp_draw_lines(lines, POINTS);
|
|
}
|
|
|
|
static void frame(void)
|
|
{
|
|
int width = sapp_width(), height = sapp_height();
|
|
float ratio = width / (float)height;
|
|
double time = sapp_frame_count() * sapp_frame_duration();
|
|
float dt = sapp_frame_duration();
|
|
|
|
// pressed input management
|
|
{
|
|
for (int i = 0; i < SAPP_KEYCODE_MENU; i++)
|
|
{
|
|
if (keypressed[i].frame < sapp_frame_count())
|
|
{
|
|
keypressed[i].pressed = false;
|
|
}
|
|
}
|
|
if (mouse_pressed_frame < sapp_frame_count())
|
|
{
|
|
mouse_pressed = false;
|
|
}
|
|
}
|
|
|
|
// networking
|
|
{
|
|
ENetEvent event;
|
|
while (true)
|
|
{
|
|
int enet_status = enet_host_service(client, &event, 0);
|
|
if (enet_status > 0)
|
|
{
|
|
switch (event.type)
|
|
{
|
|
case ENET_EVENT_TYPE_CONNECT:
|
|
Log("New client from host %x\n", event.peer->address.host);
|
|
break;
|
|
case ENET_EVENT_TYPE_RECEIVE:
|
|
// @Robust @BeforeShip use some kind of serialization strategy that checks for out of bounds
|
|
// and other validation instead of just casting to a struct
|
|
// "Alignment of structure members can be different even among different compilers on the same platform, let alone different platforms."
|
|
// ^^ need serialization strategy that accounts for this if multiple platforms is happening https://stackoverflow.com/questions/28455163/how-can-i-portably-send-a-c-struct-through-a-network-socket
|
|
struct ServerToClient msg = {
|
|
.cur_gs = &gs,
|
|
};
|
|
// @Robust @BeforeShip maximum acceptable message size?
|
|
from_bytes(&msg, event.packet->data, event.packet->dataLength);
|
|
myplayer = msg.your_player;
|
|
enet_packet_destroy(event.packet);
|
|
break;
|
|
case ENET_EVENT_TYPE_DISCONNECT:
|
|
fprintf(stderr, "Disconnected from server\n");
|
|
exit(-1);
|
|
break;
|
|
}
|
|
}
|
|
else if (enet_status == 0)
|
|
{
|
|
break;
|
|
}
|
|
else if (enet_status < 0)
|
|
{
|
|
fprintf(stderr, "Error receiving enet events: %d\n", enet_status);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// gameplay
|
|
V2 build_target_pos = {0};
|
|
float build_target_rotation = 0.0f;
|
|
V2 camera_pos = {0};
|
|
V2 world_mouse_pos = mouse_pos;
|
|
struct BuildPreviewInfo
|
|
{
|
|
V2 grid_pos;
|
|
float grid_rotation;
|
|
V2 pos;
|
|
} build_preview = {0};
|
|
bool hand_at_arms_length = false;
|
|
{
|
|
// interpolate zoom
|
|
zoom = lerp(zoom, zoom_target, dt*12.0f);
|
|
|
|
// calculate world position and camera
|
|
{
|
|
if (myplayer != -1)
|
|
{
|
|
camera_pos = gs.players[myplayer].pos;
|
|
}
|
|
world_mouse_pos = V2sub(world_mouse_pos, (V2){.x = width / 2.0f, .y = height / 2.0f});
|
|
world_mouse_pos.x /= zoom;
|
|
world_mouse_pos.y /= -zoom;
|
|
world_mouse_pos = V2add(world_mouse_pos, (V2){.x = camera_pos.x, .y = camera_pos.y});
|
|
}
|
|
|
|
// calculate build preview stuff
|
|
int grid_index = -1;
|
|
if (myplayer != -1)
|
|
{
|
|
V2 hand_pos = V2sub(world_mouse_pos, gs.players[myplayer].pos);
|
|
float hand_len = V2length(hand_pos);
|
|
if (hand_len > MAX_HAND_REACH)
|
|
{
|
|
hand_at_arms_length = true;
|
|
hand_len = MAX_HAND_REACH;
|
|
}
|
|
else
|
|
{
|
|
hand_at_arms_length = false;
|
|
}
|
|
hand_pos = V2scale(V2normalize(hand_pos), hand_len);
|
|
hand_pos = V2add(hand_pos, gs.players[myplayer].pos);
|
|
|
|
struct Grid *placing_grid = closest_to_point_in_radius(&gs, hand_pos, 0.35f);
|
|
if (placing_grid == NULL)
|
|
{
|
|
build_preview = (struct BuildPreviewInfo){
|
|
.grid_pos = hand_pos,
|
|
.grid_rotation = 0.0f,
|
|
.pos = hand_pos,
|
|
};
|
|
}
|
|
else
|
|
{
|
|
for (int i = 0; i < MAX_GRIDS; i++)
|
|
{
|
|
if (&gs.grids[i] == placing_grid)
|
|
{
|
|
grid_index = i;
|
|
break;
|
|
}
|
|
}
|
|
V2 pos = grid_snapped_box_pos(placing_grid, hand_pos);
|
|
build_preview = (struct BuildPreviewInfo){
|
|
.grid_pos = grid_pos(placing_grid),
|
|
.grid_rotation = grid_rotation(placing_grid),
|
|
.pos = pos};
|
|
}
|
|
}
|
|
|
|
// Create and send input packet
|
|
{
|
|
// @Robust accumulate total time and send input at rate like 20 hz, not every frame
|
|
|
|
struct InputFrame cur_input_frame = {0};
|
|
V2 input = (V2){
|
|
.x = (float)keydown[SAPP_KEYCODE_D] - (float)keydown[SAPP_KEYCODE_A],
|
|
.y = (float)keydown[SAPP_KEYCODE_W] - (float)keydown[SAPP_KEYCODE_S],
|
|
};
|
|
cur_input_frame.movement = input;
|
|
cur_input_frame.inhabit = keypressed[SAPP_KEYCODE_G].pressed;
|
|
cur_input_frame.dobuild = mouse_pressed;
|
|
cur_input_frame.grid_index = grid_index;
|
|
if (cur_input_frame.dobuild)
|
|
{
|
|
if (grid_index != -1)
|
|
{
|
|
cur_input_frame.build = grid_world_to_local(&gs.grids[cur_input_frame.grid_index], build_preview.pos);
|
|
V2 untransformed = grid_local_to_world(&gs.grids[cur_input_frame.grid_index], cur_input_frame.build);
|
|
untransformed.x += 5.0f;
|
|
}
|
|
else
|
|
{
|
|
cur_input_frame.build = build_preview.pos;
|
|
}
|
|
}
|
|
|
|
struct InputFrame latest = client_to_server.inputs[0];
|
|
// if they're not the same
|
|
if (
|
|
!V2cmp(cur_input_frame.movement, latest.movement, 0.01f) ||
|
|
cur_input_frame.inhabit != latest.inhabit ||
|
|
cur_input_frame.dobuild != latest.dobuild ||
|
|
cur_input_frame.grid_index != latest.grid_index ||
|
|
!V2cmp(cur_input_frame.build, latest.build, 0.01f))
|
|
{
|
|
for (int i = 0; i < INPUT_BUFFER - 1; i++)
|
|
{
|
|
client_to_server.inputs[i + 1] = client_to_server.inputs[i];
|
|
}
|
|
cur_input_frame.tick = gs.tick;
|
|
client_to_server.inputs[0] = cur_input_frame;
|
|
}
|
|
|
|
static double last_input_sent_time = 0.0;
|
|
if (fabs(last_input_sent_time - time) > TIME_BETWEEN_INPUT_PACKETS)
|
|
{
|
|
ENetPacket *packet = enet_packet_create((void *)&client_to_server, sizeof(client_to_server), ENET_PACKET_FLAG_UNRELIABLE_FRAGMENT);
|
|
enet_peer_send(peer, 0, packet);
|
|
last_input_sent_time = time;
|
|
}
|
|
}
|
|
|
|
// @BeforeShip client side prediction and rollback to previous server authoritative state, then replay inputs
|
|
// no need to store copies of game state, just player input frame to frame. Then know how many frames ago the server game state arrived, it's that easy!
|
|
// process(&gs, (float)sapp_frame_duration());
|
|
}
|
|
|
|
// drawing
|
|
{
|
|
sgp_begin(width, height);
|
|
sgp_viewport(0, 0, width, height);
|
|
sgp_project(0.0f, width, 0.0f, height);
|
|
sgp_set_blend_mode(SGP_BLENDMODE_BLEND);
|
|
|
|
// Draw background color
|
|
sgp_set_color(0.1f, 0.1f, 0.1f, 1.0f);
|
|
sgp_clear();
|
|
|
|
// draw spice bar
|
|
if (myplayer != -1)
|
|
{
|
|
sgp_set_color(0.5f, 0.5f, 0.5f, 1.0f);
|
|
float margin = width * 0.1;
|
|
float bar_width = width - margin * 2.0f;
|
|
sgp_draw_filled_rect(margin, 80.0f, bar_width, 30.0f);
|
|
sgp_set_color(1.0f, 1.0f, 1.0f, 1.0f);
|
|
sgp_draw_filled_rect(margin, 80.0f, bar_width * (1.0f - gs.players[myplayer].spice_taken_away), 30.0f);
|
|
}
|
|
|
|
// sokol drawing library draw in world space
|
|
// world space coordinates are +Y up, -Y down. Like normal cartesian coords
|
|
{
|
|
sgp_translate(width / 2, height / 2);
|
|
sgp_scale_at(zoom, -zoom, 0.0f, 0.0f);
|
|
|
|
// camera go to player
|
|
|
|
sgp_translate(-camera_pos.x, -camera_pos.y);
|
|
}
|
|
|
|
if (myplayer != -1)
|
|
{
|
|
static float hand_reach_alpha = 1.0f;
|
|
hand_reach_alpha = lerp(hand_reach_alpha, hand_at_arms_length ? 1.0f : 0.0f, dt * 5.0);
|
|
sgp_set_color(1.0f, 1.0f, 1.0f, hand_reach_alpha);
|
|
draw_circle(gs.players[myplayer].pos, MAX_HAND_REACH);
|
|
}
|
|
|
|
// stars
|
|
sgp_set_color(1.0f, 1.0f, 1.0f, 1.0f);
|
|
const int num = 50;
|
|
for (int x = -num; x < num; x++)
|
|
{
|
|
for (int y = -num; y < num; y++)
|
|
{
|
|
sgp_draw_point((float)x * 0.1f, (float)y * 0.1f);
|
|
}
|
|
}
|
|
|
|
float halfbox = BOX_SIZE / 2.0f;
|
|
|
|
// player
|
|
for (int i = 0; i < MAX_PLAYERS; i++)
|
|
{
|
|
struct Player *p = &gs.players[i];
|
|
if (!p->connected)
|
|
continue;
|
|
static float opacities[MAX_PLAYERS] = {1.0f};
|
|
opacities[i] = lerp(opacities[i], p->currently_inhabiting_index == -1 ? 1.0f : 0.1f, dt * 7.0f);
|
|
Color col_to_draw = Collerp(WHITE, GOLD, p->goldness);
|
|
col_to_draw.a = opacities[i];
|
|
|
|
set_color(col_to_draw);
|
|
sgp_push_transform();
|
|
float psize = 0.1f;
|
|
sgp_draw_filled_rect(p->pos.x - psize / 2.0f, p->pos.y - psize / 2.0f, psize, psize);
|
|
sgp_pop_transform();
|
|
// sgp_rotate_at(grid_rotation(p->grid), grid_pos(p->grid).x, grid_pos(p->grid).y);
|
|
// V2 bpos = grid_pos(p->grid);
|
|
// sgp_draw_filled_rect(grid_pos(p->grid).x - halfbox, grid_pos(p->grid).y - halfbox, BOX_SIZE, BOX_SIZE);
|
|
// sgp_pop_transform();
|
|
|
|
// sgp_set_color(1.0f, 0.0f, 0.0f, 1.0f);
|
|
// V2 vel = grid_vel(p->grid);
|
|
// V2 to = V2add(grid_pos(p->grid), vel);
|
|
// sgp_draw_line(grid_pos(p->grid).x, grid_pos(p->grid).y, to.x, to.y);
|
|
}
|
|
|
|
// mouse
|
|
if (mouse_frozen)
|
|
{
|
|
sgp_set_color(1.0f, 0.0f, 0.0f, 0.5f);
|
|
sgp_draw_filled_rect(world_mouse_pos.x, world_mouse_pos.y, 0.1f, 0.1f);
|
|
}
|
|
|
|
// building preview
|
|
{
|
|
sgp_set_color(0.5f, 0.5f, 0.5f, (sin(time * 9.0f) + 1.0) / 3.0f + 0.2);
|
|
drawbox(build_preview.grid_pos, build_preview.grid_rotation, build_preview.pos, 0.0f, false);
|
|
}
|
|
|
|
// grids
|
|
{
|
|
for (int i = 0; i < MAX_GRIDS; i++)
|
|
{
|
|
SKIPNULL(gs.grids[i].body);
|
|
struct Grid *g = &gs.grids[i];
|
|
for (int ii = 0; ii < MAX_BOXES_PER_GRID; ii++)
|
|
{
|
|
SKIPNULL(g->boxes[ii].shape);
|
|
struct Box *b = &g->boxes[ii];
|
|
sgp_set_color(0.5f, 0.5f, 0.5f, 1.0f);
|
|
drawbox(grid_pos(g), grid_rotation(g), box_pos(b), b->damage, true);
|
|
}
|
|
sgp_set_color(1.0f, 0.0f, 0.0f, 1.0f);
|
|
V2 vel = grid_vel(&gs.grids[i]);
|
|
V2 to = V2add(grid_com(g), vel);
|
|
sgp_draw_line(grid_com(g).x, grid_com(g).y, to.x, to.y);
|
|
}
|
|
}
|
|
|
|
// gold target
|
|
set_color(GOLD);
|
|
sgp_draw_filled_rect(gs.goldpos.x, gs.goldpos.y, 0.1f, 0.1f);
|
|
|
|
sgp_set_color(1.0f, 1.0f, 1.0f, 1.0f);
|
|
dbg_drawall();
|
|
|
|
sg_pass_action pass_action = {0};
|
|
sg_begin_default_pass(&pass_action, width, height);
|
|
sgp_flush();
|
|
sgp_end();
|
|
sg_end_pass();
|
|
sg_commit();
|
|
}
|
|
}
|
|
|
|
void cleanup(void)
|
|
{
|
|
destroy(&gs);
|
|
sgp_shutdown();
|
|
sg_shutdown();
|
|
enet_deinitialize();
|
|
}
|
|
|
|
void event(const sapp_event *e)
|
|
{
|
|
switch (e->type)
|
|
{
|
|
case SAPP_EVENTTYPE_KEY_DOWN:
|
|
if (e->key_code == SAPP_KEYCODE_T)
|
|
{
|
|
mouse_frozen = !mouse_frozen;
|
|
}
|
|
if (!mouse_frozen)
|
|
{
|
|
keydown[e->key_code] = true;
|
|
if (keypressed[e->key_code].frame == 0)
|
|
{
|
|
keypressed[e->key_code].pressed = true;
|
|
keypressed[e->key_code].frame = e->frame_count;
|
|
}
|
|
}
|
|
break;
|
|
case SAPP_EVENTTYPE_KEY_UP:
|
|
if (!mouse_frozen)
|
|
{
|
|
keydown[e->key_code] = false;
|
|
keypressed[e->key_code].pressed = false;
|
|
|
|
keypressed[e->key_code].frame = 0;
|
|
}
|
|
break;
|
|
case SAPP_EVENTTYPE_MOUSE_SCROLL:
|
|
printf("%f\n", e->scroll_y);
|
|
zoom_target *= 1.0f + (e->scroll_y/4.0f) * 0.1f;
|
|
zoom_target = clamp(zoom_target, 0.5f, 900.0f);
|
|
break;
|
|
case SAPP_EVENTTYPE_MOUSE_DOWN:
|
|
if (e->mouse_button == SAPP_MOUSEBUTTON_LEFT)
|
|
{
|
|
mouse_pressed = true;
|
|
mouse_pressed_frame = e->frame_count;
|
|
}
|
|
if (e->mouse_button == SAPP_MOUSEBUTTON_RIGHT)
|
|
{
|
|
right_mouse_down = true;
|
|
}
|
|
break;
|
|
case SAPP_EVENTTYPE_MOUSE_UP:
|
|
if (e->mouse_button == SAPP_MOUSEBUTTON_LEFT)
|
|
{
|
|
mouse_pressed = false;
|
|
mouse_pressed_frame = 0;
|
|
}
|
|
if (e->mouse_button == SAPP_MOUSEBUTTON_RIGHT)
|
|
{
|
|
right_mouse_down = false;
|
|
}
|
|
break;
|
|
case SAPP_EVENTTYPE_MOUSE_MOVE:
|
|
if (!mouse_frozen)
|
|
{
|
|
mouse_pos = (V2){.x = e->mouse_x, .y = e->mouse_y};
|
|
}
|
|
if (right_mouse_down)
|
|
{
|
|
funval += e->mouse_dx;
|
|
Log("Funval %f\n", funval);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
sapp_desc sokol_main(int argc, char *argv[])
|
|
{
|
|
if (argc > 1)
|
|
{
|
|
_beginthread(server, 0, NULL);
|
|
}
|
|
(void)argv;
|
|
return (sapp_desc){
|
|
.init_cb = init,
|
|
.frame_cb = frame,
|
|
.cleanup_cb = cleanup,
|
|
.width = 640,
|
|
.height = 480,
|
|
.gl_force_gles2 = true,
|
|
.window_title = "Flight",
|
|
.icon.sokol_default = true,
|
|
.event_cb = event,
|
|
.win32_console_attach = true,
|
|
.sample_count = 4, // anti aliasing
|
|
};
|
|
} |