Steam
If you want to put your game on Steam, you might also want to add some Steam specific features, like achievements or multiplayer. There are many LÖVE games on Steam already, so yes, it is possible to have these features when using LÖVE.
luasteam #
To connect with the Steam API, we will be using luasteam. Get the .dll
or .so
for your system, and put this file in your project folder. In this chapter we will assume you placed it at the root of your project. Also at the root create a steam_appid.txt
with either your app's ID, or with 480
, the id for the example game Spacewar.
Download the steamworks_sdk.zip (you need a Steamworks account). Navigate to sdk -> redistributable_bin -> [your OS]
and copy the steam_api64.dll
/ libsteam_api.so
. Place this not at your project folder, but in your LOVE folder (i.e. where love.exe
is).
Tip
You might want to set yourself to invisible on Steam, or hide notifications another way. Otherwise your friends will start to asking what Spacewar is, and why you're restarting it every 5 minutes.
Generally, loading Steam is the first thing you want to do in your game. At the top of your main.lua, place the following code:
local Steam = require "luasteam"
Steam.init()
Note
You might get the following messages:
Setting breakpad minidump AppID = 480
SteamInternal_SetMinidumpSteamID: Caching Steam ID: 0123456789 [API loaded no]
This is because you don't load your game through stream. You can ignore it.
You can now use the Steam API, but not all of Steam API has been implemented in luasteam. Check the luasteam docs to see what is implemented and how it works.
For example, we can use Steam.friends.activateGameOverlay("community")
to open the community tab, and use Steam.friends.setAchievement(name)
to unlock an achievement for the player.
Multiplayer #
We can use the Steam API to create a multiplayer game. We're going to assume you know the basics of online multiplayer and networking. We will be using the P2P API, where we have one server and one or more connecting clients. At the end of this chapter is a full example implementation.
Socket #
We can let one player, the server, create a P2P socket with createListenSocketP2P()
. Other players can then connect to this socket with connectP2P(steamID)
. The steamID
is the ID of the player who created the server.
To get this ID, the server can
- Invite the player using
inviteUserToGame(connect_string)
. - Enable the "Join game" button on their profile with
setRichPresence("connect", connect_string)
.
The connect_string
in this case is the steamID
of the player who hosts the server, which we can get with getSteamID()
. Upon accepting the connection the callback onGameRichPresenceJoinRequested
is called. In here, we can connect to the server using connectP2P(steamID)
.
Using the callback onConnectionChanged
we capture whenever there is a change in a connection. It comes with a data
table, which tells us the state of the connection. If it's a client connecting to the server, we can accept the connection with acceptConnection(connection)
.
Because we use callbacks, we need to call runCallbacks()
every frame.
Steam = require "luasteam"
Steam.init()
local server = true -- Change to false for the client
local connectionId
if server then
connectionId = Steam.networkingSockets.createListenSocketP2P(0)
Steam.friends.setRichPresence("connect", tostring(Steam.user.getSteamID()))
end
function Steam.friends.onGameRichPresenceJoinRequested(data)
Steam.networkingSockets.connectP2P(Steam.extra.parseUint64(data.connect), 0)
-- This way we allow friends of this person to join the server as well.
Steam.friends.setRichPresence("connect", data.connect)
end
function Steam.networkingSockets.onConnectionChanged(data)
local state = data.state
local conn = data.connection
if state == "Connecting" then
if server then
Steam.networkingSockets.acceptConnection(conn)
end
elseif state == "Connected" then
-- As the server we accept the connection, so we can ignore this.
if not server then
print("Connected!")
connectionId = conn
end
elseif state == "ClosedByPeer" then
print("The client ended the connection (e.g. closing the game).")
elseif state == "ProblemDetectedLocally" then
print("I'm sure it's nothing...")
end
end
function love.update(dt)
Steam.runCallbacks()
end
Messaging #
Now that we have a connection, we want to send information back and forth using messages. We do this by usign a poll group.
Along with creating a socket, we also create a poll group using createPollGroup()
. Every time a client connects, we assign it to the poll group using setConnectionPollGroup(connection, pollGroup)
. This way we can use the poll group to poll for messages of all clients with receiveMessagesOnPollGroup(pollGroup)
. A client only needs to receive messages from the server, so it can use receiveMessagesOnConnection(connectionId)
instead. We receive these messages as a table in the following format:
{
1 = {
conn = 5235,
msg = "A message"
},
2 = {
conn = 5235,
msg = "Another message"
},
3 = {
conn = 5678,
msg = "Yet another message"
}
}
We can send a message with sendMessageToConnection(connection, message, flag)
. The flag
decides what method is used when sending the message (i.e. reliable means it will make sure the message is received). Check the documentation for more information.
Summary #
Below is an example of a P2P implementation, with commentary on what the functions do, and links to the relevant documentation.
local server = true -- Change to false for the client
local connectionId
local pollGroup
Steam = require "luasteam"
Steam.init()
-- We call this to let Steam know that we want to use the networking sockets API.
-- Which probably won't do much in this example, but it can save you a delay.
-- https://luasteam.readthedocs.io/en/stable/networking_sockets.html#networkingSockets.initAuthentication
Steam.networkingSockets.initAuthentication()
if server then
-- We create a P2P server.
-- https://luasteam.readthedocs.io/en/stable/networking_sockets.html#networkingSockets.createListenSocketP2P
connectionId = Steam.networkingSockets.createListenSocketP2P(0)
-- This will be used to poll for incoming messages from clients.
-- https://luasteam.readthedocs.io/en/stable/networking_sockets.html#networkingSockets.createPollGroup
pollGroup = Steam.networkingSockets.createPollGroup()
-- This allows friends to right click -> "Join game".
-- Or the player right clicks -> "Invite to play".
-- We set the steam ID of the server as the key.
-- When the player creates a P2P server, their steam ID is used as the ID to connect with the server.
-- https://luasteam.readthedocs.io/en/stable/friends.html#friends.setRichPresence
Steam.friends.setRichPresence("connect", tostring(Steam.user.getSteamID()))
end
-- Callback that is called when the player clicks on "Join game" or accepts the invite.
-- https://luasteam.readthedocs.io/en/stable/friends.html#friends.onGameRichPresenceJoinRequested
function Steam.friends.onGameRichPresenceJoinRequested(data)
-- We have two options here:
-- 1. Use data.steamIDFriend
-- 2. Use data.connect (the key that we passed)
-- By using option 2 we can have the client copy the key, so that people can join them as well.
-- https://luasteam.readthedocs.io/en/stable/networking_sockets.html#networkingSockets.connectP2P
Steam.networkingSockets.connectP2P(Steam.extra.parseUint64(data.connect), 0)
-- This way we allow friends of this person to join the server.
Steam.friends.setRichPresence("connect", data.connect)
end
-- Callback that is called when a connection arrives/changes.
-- https://luasteam.readthedocs.io/en/stable/networking_sockets.html#networkingSockets.onConnectionChanged
function Steam.networkingSockets.onConnectionChanged(data)
local state = data.state
local conn = data.connection
-- The state tells us what happened to the connection.
-- https://partner.steamgames.com/doc/api/ISteamNetworkingSockets#2
if state == "Connecting" then
if server then
-- A client is connecting. We accept the connection.
-- https://luasteam.readthedocs.io/en/stable/networking_sockets.html#networkingSockets.acceptConnection
Steam.networkingSockets.acceptConnection(conn)
-- We assign the connection to the poll group, so we can receive messages from it.
-- https://luasteam.readthedocs.io/en/stable/networking_sockets.html#networkingSockets.setConnectionPollGroup
Steam.networkingSockets.setConnectionPollGroup(conn, pollGroup)
end
elseif state == "Connected" then
-- As the server we accept the connection, so we can ignore this.
if not server then
print("Connected!")
connectionId = conn
end
elseif state == "ClosedByPeer" then
print("The client ended the connection (e.g. closing the game).")
elseif state == "ProblemDetectedLocally" then
print("I'm sure it's nothing...")
end
end
function love.update()
-- Call this every frame to run the callbacks when an event happened.
-- https://luasteam.readthedocs.io/en/stable/steam_api.html#runCallbacks
Steam.runCallbacks()
local n, messages
if server then
-- We poll for incoming messages from clients.
-- https://luasteam.readthedocs.io/en/stable/networking_sockets.html#networkingSockets.receiveMessagesOnPollGroup
n, messages = Steam.networkingSockets.receiveMessagesOnPollGroup(pollGroup)
else
-- We poll for incoming messages from the server.
-- https://luasteam.readthedocs.io/en/stable/networking_sockets.html#networkingSockets.receiveMessagesOnConnection
n, messages = Steam.networkingSockets.receiveMessagesOnConnection(connectionId)
end
if n == 0 then
-- No messages.
return
end
for _, data in ipairs(messages) do
-- data.msg is the string that was passed.
print(data.msg)
end
end
function love.keypressed(k)
if server or not connectionId then return end
if k == "space" then
-- We send a message to the server.
-- https://luasteam.readthedocs.io/en/stable/networking_sockets.html#networkingSockets.sendMessageToConnection
local method = Steam.networkingSockets.flags.Send_Reliable
Steam.networkingSockets.sendMessageToConnection(connectionId, "Hello world!", method)
end
end